From af5482d774c8f4a1f41d77576ec782f19ca7239a Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Tue, 11 Aug 2026 23:06:13 +0800 Subject: [PATCH] style: cargo fmt with the workspace tab policy (hard_tabs) Whitespace only; the fork has no own rustfmt.toml so the Oak root policy applies. --- crates/gpui/build.rs | 26 +- crates/gpui/examples/bench/data_table.rs | 833 +- crates/gpui/examples/bench/paths_bench.rs | 146 +- crates/gpui/examples/bench/pattern.rs | 208 +- crates/gpui/examples/bench/shadow.rs | 180 +- crates/gpui/examples/learn/animation.rs | 444 +- crates/gpui/examples/learn/async_tasks.rs | 885 +- crates/gpui/examples/learn/blur.rs | 454 +- .../examples/learn/creating_components.rs | 626 +- crates/gpui/examples/learn/custom_drawing.rs | 752 +- crates/gpui/examples/learn/dock_layout.rs | 179 +- crates/gpui/examples/learn/effect_stack.rs | 338 +- crates/gpui/examples/learn/haptic_feedback.rs | 562 +- .../examples/learn/interactive_elements.rs | 888 +- crates/gpui/examples/learn/keyring.rs | 262 +- crates/gpui/examples/learn/layout.rs | 844 +- crates/gpui/examples/learn/node_graph.rs | 396 +- crates/gpui/examples/learn/styling.rs | 858 +- crates/gpui/examples/learn/text.rs | 1002 +- crates/gpui/examples/learn/timeline.rs | 272 +- crates/gpui/examples/learn/transition.rs | 140 +- crates/gpui/examples/legacy/focus_visible.rs | 380 +- crates/gpui/examples/legacy/gif_viewer.rs | 52 +- crates/gpui/examples/legacy/gradient.rs | 482 +- crates/gpui/examples/legacy/hello_world.rs | 190 +- crates/gpui/examples/legacy/image_loading.rs | 348 +- crates/gpui/examples/legacy/layer_shell.rs | 142 +- .../examples/legacy/on_window_close_quit.rs | 130 +- crates/gpui/examples/legacy/opacity.rs | 300 +- crates/gpui/examples/legacy/scrollable.rs | 96 +- crates/gpui/examples/legacy/svg/svg.rs | 132 +- crates/gpui/examples/legacy/tab_stop.rs | 262 +- crates/gpui/examples/legacy/tree.rs | 54 +- crates/gpui/examples/legacy/uniform_list.rs | 78 +- crates/gpui/examples/legacy/window.rs | 482 +- .../examples/legacy/window_positioning.rs | 372 +- crates/gpui/examples/legacy/window_shadow.rs | 416 +- crates/gpui/examples/shared/prelude.rs | 38 +- .../gpui/examples/text_transform_preview.rs | 148 +- crates/gpui/src/action.rs | 512 +- crates/gpui/src/app.rs | 5108 +++---- crates/gpui/src/app/async_context.rs | 894 +- crates/gpui/src/app/context.rs | 1578 +-- crates/gpui/src/app/entity_map.rs | 1814 +-- crates/gpui/src/app/headless_app_context.rs | 424 +- crates/gpui/src/app/test_app.rs | 954 +- crates/gpui/src/app/test_context.rs | 1978 +-- crates/gpui/src/app/visual_test_context.rs | 820 +- crates/gpui/src/arena.rs | 444 +- crates/gpui/src/asset_cache.rs | 82 +- crates/gpui/src/assets.rs | 166 +- crates/gpui/src/bounds_tree.rs | 797 +- crates/gpui/src/color.rs | 1406 +- crates/gpui/src/colors.rs | 152 +- crates/gpui/src/dock/dock_area.rs | 1845 +-- crates/gpui/src/dock/floating.rs | 82 +- crates/gpui/src/dock/layout.rs | 1920 +-- crates/gpui/src/dock/mod.rs | 2 +- crates/gpui/src/dock/panel.rs | 282 +- crates/gpui/src/dock/split_handle.rs | 334 +- crates/gpui/src/dock/tab_bar.rs | 503 +- crates/gpui/src/effect_stack/card.rs | 437 +- crates/gpui/src/effect_stack/data.rs | 194 +- crates/gpui/src/effect_stack/stack_view.rs | 962 +- crates/gpui/src/element.rs | 1262 +- crates/gpui/src/elements/anchored.rs | 618 +- crates/gpui/src/elements/animation.rs | 392 +- crates/gpui/src/elements/canvas.rs | 130 +- crates/gpui/src/elements/deferred.rs | 134 +- crates/gpui/src/elements/div.rs | 7187 +++++----- crates/gpui/src/elements/image_cache.rs | 492 +- crates/gpui/src/elements/img.rs | 1304 +- crates/gpui/src/elements/list.rs | 4270 +++--- crates/gpui/src/elements/surface.rs | 232 +- crates/gpui/src/elements/svg.rs | 426 +- crates/gpui/src/elements/text.rs | 2384 ++-- crates/gpui/src/elements/uniform_list.rs | 1440 +- crates/gpui/src/executor.rs | 794 +- crates/gpui/src/geometry.rs | 5768 ++++---- crates/gpui/src/global.rs | 72 +- crates/gpui/src/gpui.rs | 284 +- crates/gpui/src/http_client.rs | 126 +- crates/gpui/src/i18n.rs | 64 +- crates/gpui/src/input.rs | 312 +- crates/gpui/src/inspector.rs | 416 +- crates/gpui/src/interactive.rs | 982 +- crates/gpui/src/key_dispatch.rs | 1972 +-- crates/gpui/src/keymap.rs | 1592 +-- crates/gpui/src/keymap/binding.rs | 214 +- crates/gpui/src/keymap/collection.rs | 168 +- crates/gpui/src/keymap/context.rs | 1508 +- crates/gpui/src/lerp.rs | 506 +- crates/gpui/src/node_graph/data.rs | 234 +- crates/gpui/src/node_graph/graph_view.rs | 1726 +-- crates/gpui/src/node_graph/minimap.rs | 203 +- crates/gpui/src/node_graph/node_element.rs | 633 +- crates/gpui/src/node_graph/state.rs | 498 +- crates/gpui/src/node_graph/wire.rs | 404 +- crates/gpui/src/path_builder.rs | 538 +- crates/gpui/src/platform.rs | 3663 +++-- crates/gpui/src/platform/app_menu.rs | 630 +- crates/gpui/src/platform/keyboard.rs | 46 +- crates/gpui/src/platform/keystroke.rs | 1223 +- crates/gpui/src/platform/layer_shell.rs | 98 +- .../gpui/src/platform/scap_screen_capture.rs | 480 +- crates/gpui/src/platform/test/dispatcher.rs | 234 +- crates/gpui/src/platform/test/display.rs | 38 +- crates/gpui/src/platform/test/platform.rs | 806 +- crates/gpui/src/platform/test/window.rs | 616 +- crates/gpui/src/platform/visual_test.rs | 354 +- crates/gpui/src/platform_scheduler.rs | 729 +- crates/gpui/src/prelude.rs | 6 +- crates/gpui/src/profiler.rs | 520 +- crates/gpui/src/queue.rs | 616 +- crates/gpui/src/scene.rs | 2008 +-- crates/gpui/src/shared_uri.rs | 18 +- crates/gpui/src/style.rs | 2322 +-- crates/gpui/src/styled.rs | 1816 +-- crates/gpui/src/subscription.rs | 548 +- crates/gpui/src/svg_renderer.rs | 638 +- crates/gpui/src/tab_stop.rs | 1004 +- crates/gpui/src/taffy.rs | 1166 +- crates/gpui/src/test.rs | 250 +- crates/gpui/src/text_system.rs | 1932 +-- crates/gpui/src/text_system/font_fallbacks.rs | 16 +- crates/gpui/src/text_system/font_features.rs | 218 +- crates/gpui/src/text_system/line.rs | 1782 +-- crates/gpui/src/text_system/line_layout.rs | 1752 +-- crates/gpui/src/text_system/line_wrapper.rs | 2664 ++-- crates/gpui/src/timeline/clip.rs | 494 +- crates/gpui/src/timeline/data.rs | 268 +- crates/gpui/src/timeline/mod.rs | 4 +- crates/gpui/src/timeline/playhead.rs | 184 +- crates/gpui/src/timeline/ruler.rs | 548 +- crates/gpui/src/timeline/state.rs | 310 +- crates/gpui/src/timeline/time.rs | 881 +- crates/gpui/src/timeline/timeline_view.rs | 1900 ++- crates/gpui/src/timeline/track_header.rs | 279 +- crates/gpui/src/transition.rs | 1324 +- crates/gpui/src/util.rs | 350 +- crates/gpui/src/view.rs | 464 +- crates/gpui/src/window.rs | 11649 ++++++++-------- crates/gpui/src/window/a11y.rs | 436 +- crates/gpui/src/window/prompts.rs | 334 +- crates/gpui/tests/action_macros.rs | 78 +- crates/gpui_ce_util/src/arc_cow.rs | 164 +- crates/gpui_ce_util/src/lib.rs | 522 +- crates/gpui_collections/src/vecmap.rs | 250 +- crates/gpui_collections/src/vecmap_tests.rs | 228 +- .../src/derive_refineable.rs | 958 +- .../gpui_elements/examples/editable_text.rs | 108 +- .../src/editable_text/actions.rs | 554 +- .../gpui_elements/src/editable_text/caret.rs | 254 +- .../src/editable_text/element.rs | 1450 +- .../src/editable_text/history.rs | 224 +- .../gpui_elements/src/editable_text/layout.rs | 158 +- .../gpui_elements/src/editable_text/state.rs | 5154 +++---- .../src/editable_text/storage.rs | 382 +- crates/gpui_linux/src/linux.rs | 46 +- crates/gpui_linux/src/linux/dispatcher.rs | 598 +- .../gpui_linux/src/linux/headless/client.rs | 170 +- crates/gpui_linux/src/linux/keyboard.rs | 20 +- crates/gpui_linux/src/linux/platform.rs | 1950 +-- crates/gpui_linux/src/linux/wayland.rs | 46 +- crates/gpui_linux/src/linux/wayland/client.rs | 4489 +++--- .../gpui_linux/src/linux/wayland/clipboard.rs | 380 +- crates/gpui_linux/src/linux/wayland/cursor.rs | 236 +- .../gpui_linux/src/linux/wayland/display.rs | 44 +- .../src/linux/wayland/layer_shell.rs | 26 +- crates/gpui_linux/src/linux/wayland/scroll.rs | 704 +- crates/gpui_linux/src/linux/wayland/serial.rs | 88 +- crates/gpui_linux/src/linux/wayland/window.rs | 3046 ++-- crates/gpui_linux/src/linux/x11/client.rs | 5536 ++++---- crates/gpui_linux/src/linux/x11/clipboard.rs | 2060 +-- crates/gpui_linux/src/linux/x11/display.rs | 68 +- crates/gpui_linux/src/linux/x11/event.rs | 210 +- crates/gpui_linux/src/linux/x11/window.rs | 3546 ++--- .../gpui_linux/src/linux/x11/xim_handler.rs | 218 +- .../src/linux/xdg_desktop_portal.rs | 294 +- crates/gpui_macos/build.rs | 292 +- crates/gpui_macos/src/dispatcher.rs | 290 +- crates/gpui_macos/src/display.rs | 256 +- crates/gpui_macos/src/display_link.rs | 442 +- crates/gpui_macos/src/events.rs | 1008 +- crates/gpui_macos/src/gpui_macos.rs | 108 +- crates/gpui_macos/src/haptic_feedback.rs | 126 +- crates/gpui_macos/src/keyboard.rs | 2878 ++-- crates/gpui_macos/src/metal_atlas.rs | 548 +- crates/gpui_macos/src/metal_renderer.rs | 4032 +++--- crates/gpui_macos/src/open_type.rs | 234 +- crates/gpui_macos/src/pasteboard.rs | 846 +- crates/gpui_macos/src/platform.rs | 2588 ++-- crates/gpui_macos/src/screen_capture.rs | 502 +- crates/gpui_macos/src/text_system.rs | 1503 +- crates/gpui_macos/src/window.rs | 5016 +++---- crates/gpui_macos/src/window_appearance.rs | 46 +- crates/gpui_macros/src/derive_action.rs | 247 +- crates/gpui_macros/src/derive_app_context.rs | 194 +- .../src/derive_inspector_reflection.rs | 450 +- crates/gpui_macros/src/derive_into_element.rs | 30 +- crates/gpui_macros/src/derive_render.rs | 26 +- .../gpui_macros/src/derive_visual_context.rs | 110 +- crates/gpui_macros/src/gpui_macros.rs | 54 +- crates/gpui_macros/src/property_test.rs | 372 +- crates/gpui_macros/src/register_action.rs | 68 +- crates/gpui_macros/src/styles.rs | 2604 ++-- crates/gpui_macros/src/test.rs | 602 +- crates/gpui_macros/tests/derive_context.rs | 18 +- .../tests/derive_inspector_reflection.rs | 184 +- crates/gpui_macros/tests/render_test.rs | 6 +- crates/gpui_media/build.rs | 68 +- crates/gpui_media/src/media.rs | 620 +- crates/gpui_platform/src/gpui_platform.rs | 244 +- crates/gpui_refineable/src/refineable.rs | 134 +- crates/gpui_scheduler/src/clock.rs | 60 +- crates/gpui_scheduler/src/executor.rs | 836 +- crates/gpui_scheduler/src/scheduler.rs | 254 +- crates/gpui_scheduler/src/test_scheduler.rs | 1545 +- crates/gpui_scheduler/src/tests.rs | 1464 +- .../gpui_shared_string/gpui_shared_string.rs | 222 +- crates/gpui_sum_tree/src/cursor.rs | 1418 +- crates/gpui_sum_tree/src/property_test.rs | 26 +- crates/gpui_sum_tree/src/sum_tree.rs | 3388 ++--- crates/gpui_sum_tree/src/tree_map.rs | 742 +- crates/gpui_tokio/src/gpui_tokio.rs | 134 +- crates/gpui_web/src/dispatcher.rs | 536 +- crates/gpui_web/src/display.rs | 146 +- crates/gpui_web/src/events.rs | 1222 +- crates/gpui_web/src/http_client.rs | 112 +- crates/gpui_web/src/keyboard.rs | 18 +- crates/gpui_web/src/logging.rs | 48 +- crates/gpui_web/src/platform.rs | 678 +- crates/gpui_web/src/window.rs | 1158 +- crates/gpui_wgpu/benches/layout_line.rs | 118 +- crates/gpui_wgpu/src/cosmic_text_system.rs | 1804 +-- crates/gpui_wgpu/src/wgpu_atlas.rs | 772 +- crates/gpui_wgpu/src/wgpu_context.rs | 862 +- crates/gpui_wgpu/src/wgpu_renderer.rs | 4979 ++++--- crates/gpui_widgets/examples/controls.rs | 480 +- crates/gpui_widgets/examples/menus_dialogs.rs | 479 +- .../gpui_widgets/examples/project_explorer.rs | 154 +- crates/gpui_widgets/examples/scopes.rs | 222 +- crates/gpui_widgets/examples/themes.rs | 204 +- crates/gpui_widgets/examples/viewer.rs | 298 +- crates/gpui_widgets/src/audio_meter.rs | 450 +- crates/gpui_widgets/src/checkbox.rs | 499 +- crates/gpui_widgets/src/color/hsv.rs | 236 +- crates/gpui_widgets/src/color/mod.rs | 1113 +- crates/gpui_widgets/src/combo_box.rs | 803 +- crates/gpui_widgets/src/curve_editor/curve.rs | 475 +- crates/gpui_widgets/src/curve_editor/mod.rs | 913 +- crates/gpui_widgets/src/dialog/file_dialog.rs | 195 +- crates/gpui_widgets/src/dialog/message_box.rs | 283 +- crates/gpui_widgets/src/dialog/mod.rs | 563 +- crates/gpui_widgets/src/dialog/progress.rs | 236 +- crates/gpui_widgets/src/i18n.rs | 29 +- crates/gpui_widgets/src/icons.rs | 6 +- crates/gpui_widgets/src/keyable.rs | 256 +- crates/gpui_widgets/src/lib.rs | 78 +- crates/gpui_widgets/src/menu/mod.rs | 1492 +- crates/gpui_widgets/src/menu/model.rs | 409 +- crates/gpui_widgets/src/project_explorer.rs | 1034 +- crates/gpui_widgets/src/radio_group.rs | 394 +- crates/gpui_widgets/src/scopes/math.rs | 193 +- crates/gpui_widgets/src/scopes/mod.rs | 444 +- crates/gpui_widgets/src/slider/mod.rs | 1359 +- crates/gpui_widgets/src/slider/model.rs | 671 +- crates/gpui_widgets/src/spinbox.rs | 631 +- crates/gpui_widgets/src/theme.rs | 276 +- crates/gpui_widgets/src/tooltip.rs | 28 +- crates/gpui_widgets/src/value.rs | 652 +- crates/gpui_widgets/src/viewer/clock.rs | 12 +- crates/gpui_widgets/src/viewer/mod.rs | 1089 +- crates/gpui_widgets/src/viewer/transport.rs | 248 +- crates/gpui_windows/build.rs | 423 +- crates/gpui_windows/src/clipboard.rs | 554 +- crates/gpui_windows/src/destination_list.rs | 298 +- .../gpui_windows/src/direct_manipulation.rs | 568 +- crates/gpui_windows/src/direct_write.rs | 3444 ++--- crates/gpui_windows/src/directx_atlas.rs | 542 +- crates/gpui_windows/src/directx_devices.rs | 312 +- crates/gpui_windows/src/directx_renderer.rs | 4168 +++--- crates/gpui_windows/src/dispatcher.rs | 284 +- crates/gpui_windows/src/display.rs | 310 +- crates/gpui_windows/src/events.rs | 3220 +++-- crates/gpui_windows/src/keyboard.rs | 595 +- crates/gpui_windows/src/platform.rs | 2454 ++-- crates/gpui_windows/src/system_settings.rs | 116 +- crates/gpui_windows/src/util.rs | 258 +- crates/gpui_windows/src/vsync.rs | 108 +- crates/gpui_windows/src/window.rs | 2926 ++-- crates/gpui_windows/src/wrapper.rs | 38 +- crates/gpui_zed_util/src/archive.rs | 610 +- crates/gpui_zed_util/src/command.rs | 172 +- crates/gpui_zed_util/src/command/darwin.rs | 1478 +- crates/gpui_zed_util/src/disambiguate.rs | 320 +- crates/gpui_zed_util/src/fs.rs | 130 +- crates/gpui_zed_util/src/markdown.rs | 400 +- crates/gpui_zed_util/src/path_list.rs | 344 +- crates/gpui_zed_util/src/paths.rs | 6368 ++++----- crates/gpui_zed_util/src/process.rs | 140 +- crates/gpui_zed_util/src/redact.rs | 64 +- crates/gpui_zed_util/src/rel_path.rs | 896 +- crates/gpui_zed_util/src/schemars.rs | 62 +- crates/gpui_zed_util/src/serde.rs | 4 +- crates/gpui_zed_util/src/shell.rs | 1809 ++- crates/gpui_zed_util/src/shell_builder.rs | 552 +- crates/gpui_zed_util/src/shell_env.rs | 538 +- crates/gpui_zed_util/src/size.rs | 76 +- crates/gpui_zed_util/src/test.rs | 106 +- crates/gpui_zed_util/src/test/assertions.rs | 92 +- crates/gpui_zed_util/src/test/marked_text.rs | 384 +- crates/gpui_zed_util/src/time.rs | 46 +- crates/gpui_zed_util/src/util.rs | 1528 +- crates/oak_bridge/examples/surface_bridge.rs | 328 +- crates/oak_bridge/src/surface.rs | 612 +- tooling/perf/src/implementation.rs | 736 +- tooling/perf/src/main.rs | 880 +- 318 files changed, 132564 insertions(+), 132170 deletions(-) diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index b1bfd2194f..540b4dc79d 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -1,23 +1,23 @@ #![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] fn main() { - println!("cargo::rustc-check-cfg=cfg(gles)"); + println!("cargo::rustc-check-cfg=cfg(gles)"); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if target_os == "windows" { - #[cfg(feature = "windows-manifest")] - embed_resource(); - } + 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(); + 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/examples/bench/data_table.rs b/crates/gpui/examples/bench/data_table.rs index 04b507070f..83160a0b56 100644 --- a/crates/gpui/examples/bench/data_table.rs +++ b/crates/gpui/examples/bench/data_table.rs @@ -1,9 +1,9 @@ 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, + App, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, + SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas, div, point, + prelude::*, px, rgb, size, uniform_list, }; const TOTAL_ITEMS: usize = 10000; @@ -11,463 +11,462 @@ 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, + 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); + 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, - } - } + 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(&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 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 - } + fn turnover_ratio(&self) -> f64 { + self.volume as f64 / self.turnover * 100.0 + } } #[derive(IntoElement)] struct TableRow { - ix: usize, - quote: Rc, + ix: usize, + quote: Rc, } impl TableRow { - fn new(ix: usize, quote: Rc) -> Self { - Self { ix, quote } - } + 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("--"), - }) - } + 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.), + ("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))) - } + 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>, + /// 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 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 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 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_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 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"); - } + 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(); + 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; - } + 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; - }) - } - }); + 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; - } + window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| { + if !ev.dragging() { + return; + } - let Some(drag_pos) = entity.read(cx).drag_position else { - 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 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(), - ) - } + 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); + gpui_platform::application().run(|cx: &mut App| { + 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); - }); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/bench/paths_bench.rs b/crates/gpui/examples/bench/paths_bench.rs index 7a030a63ca..4a1e7c4feb 100644 --- a/crates/gpui/examples/bench/paths_bench.rs +++ b/crates/gpui/examples/bench/paths_bench.rs @@ -1,92 +1,92 @@ 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, + Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions, + Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, + prelude::*, px, rgb, size, }; 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, + default_lines: Vec<(Path, Background)>, + _painting: bool, } impl PaintingViewer { - fn new(_window: &mut Window, _cx: &mut Context) -> Self { - let mut lines = vec![]; + 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), - )); - } + // 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, - } - } + 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 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 main() { - gpui_platform::application().run(|cx| { - 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); - }); + gpui_platform::application().run(|cx| { + 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); + }); } diff --git a/crates/gpui/examples/bench/pattern.rs b/crates/gpui/examples/bench/pattern.rs index 3f11adb968..4ca0b4c781 100644 --- a/crates/gpui/examples/bench/pattern.rs +++ b/crates/gpui/examples/bench/pattern.rs @@ -1,115 +1,115 @@ use gpui::{ - App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop, - linear_gradient, pattern_slash, prelude::*, px, rgb, size, + App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop, + linear_gradient, pattern_slash, prelude::*, px, rgb, size, }; 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); + gpui_platform::application().run(|cx: &mut App| { + 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); - }); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/bench/shadow.rs b/crates/gpui/examples/bench/shadow.rs index f600364516..522c9ad2b7 100644 --- a/crates/gpui/examples/bench/shadow.rs +++ b/crates/gpui/examples/bench/shadow.rs @@ -1,97 +1,97 @@ use gpui::{ - App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div, - hsla, point, prelude::*, px, relative, rgb, size, + App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div, + hsla, point, prelude::*, px, relative, rgb, size, }; 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 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 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_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_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 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(); + 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), - ) + 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![ + 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)) @@ -610,21 +610,21 @@ impl Render for Shadow { ), ]), ])) - } + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); + gpui_platform::application().run(|cx: &mut App| { + 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); - }); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/learn/animation.rs b/crates/gpui/examples/learn/animation.rs index 0a538441e8..5421125bff 100644 --- a/crates/gpui/examples/learn/animation.rs +++ b/crates/gpui/examples/learn/animation.rs @@ -15,265 +15,265 @@ use std::time::Duration; use anyhow::Result; use gpui::colors::Colors; use gpui::{ - Animation, AnimationExt as _, App, AssetSource, Bounds, Context, Hsla, SharedString, - Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, linear, - percentage, prelude::*, px, rgb, size as gpui_size, svg, + Animation, AnimationExt as _, App, AssetSource, Bounds, Context, Hsla, SharedString, + Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, linear, + percentage, prelude::*, px, rgb, size as gpui_size, svg, }; 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 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::>()) - } + 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/legacy/image/arrow_circle.svg" + env!("CARGO_MANIFEST_DIR"), + "/examples/legacy/image/arrow_circle.svg" ); struct AnimationExample; impl Render for AnimationExample { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .id("main") - .size_full() - .p_6() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_6() - .max_w(px(600.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Animation Patterns"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Animations, easing, and transformations in GPUI"), - ), - ) - .child(section( - &colors, - "Rotation Animation", - rotation_example(&colors), - )) - .child(section(&colors, "Bounce Easing", bounce_example(&colors))) - .child(section(&colors, "Scale Animation", scale_example(&colors))) - .child(section( - &colors, - "Combined Animations", - combined_example(&colors), - )), - ) - } + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Animation Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Animations, easing, and transformations in GPUI"), + ), + ) + .child(section( + &colors, + "Rotation Animation", + rotation_example(&colors), + )) + .child(section(&colors, "Bounce Easing", bounce_example(&colors))) + .child(section(&colors, "Scale Animation", scale_example(&colors))) + .child(section( + &colors, + "Combined Animations", + combined_example(&colors), + )), + ) + } } fn rotation_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let accent = colors.selected; + let text_muted = colors.disabled; + let accent = colors.selected; - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Continuous rotation with ease_in_out easing"), - ) - .child( - div().flex().items_center().justify_center().h_24().child( - svg() - .size_16() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(accent) - .with_animation( - "rotation", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(ease_in_out), - |svg, delta| { - svg.with_transformation(Transformation::rotate(percentage(delta))) - }, - ), - ), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Continuous rotation with ease_in_out easing"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(accent) + .with_animation( + "rotation", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(ease_in_out), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage(delta))) + }, + ), + ), + ) } fn bounce_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let success = rgb(0x388e3c); + let text_muted = colors.disabled; + let success = rgb(0x388e3c); - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Bouncing rotation with bounce(ease_in_out)"), - ) - .child( - div().flex().items_center().justify_center().h_24().child( - svg() - .size_16() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(success) - .with_animation( - "bounce_rotation", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(bounce(ease_in_out)), - |svg, delta| { - svg.with_transformation(Transformation::rotate(percentage(delta))) - }, - ), - ), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Bouncing rotation with bounce(ease_in_out)"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(success) + .with_animation( + "bounce_rotation", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage(delta))) + }, + ), + ), + ) } fn scale_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let warning = rgb(0xf9a825); + let text_muted = colors.disabled; + let warning = rgb(0xf9a825); - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Scale pulsing with linear easing"), - ) - .child( - div().flex().items_center().justify_center().h_24().child( - svg() - .size_16() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(warning) - .with_animation( - "scale", - Animation::new(Duration::from_millis(1500)) - .repeat() - .with_easing(bounce(linear)), - |svg, delta| { - let scale = 0.8 + (delta * 0.4); - svg.with_transformation(Transformation::scale(gpui_size(scale, scale))) - }, - ), - ), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Scale pulsing with linear easing"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(warning) + .with_animation( + "scale", + Animation::new(Duration::from_millis(1500)) + .repeat() + .with_easing(bounce(linear)), + |svg, delta| { + let scale = 0.8 + (delta * 0.4); + svg.with_transformation(Transformation::scale(gpui_size(scale, scale))) + }, + ), + ), + ) } fn combined_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let error = rgb(0xd32f2f); + let text_muted = colors.disabled; + let error = rgb(0xd32f2f); - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Rotation + scale combined"), - ) - .child( - div().flex().items_center().justify_center().h_24().child( - svg() - .size_16() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(error) - .with_animation( - "combined", - Animation::new(Duration::from_secs(3)) - .repeat() - .with_easing(ease_in_out), - |svg, delta| { - let scale = 0.7 + (delta * 0.6); - svg.with_transformation( - Transformation::rotate(percentage(delta)) - .with_scaling(gpui_size(scale, scale)), - ) - }, - ), - ), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Rotation + scale combined"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(error) + .with_animation( + "combined", + Animation::new(Duration::from_secs(3)) + .repeat() + .with_easing(ease_in_out), + |svg, delta| { + let scale = 0.7 + (delta * 0.6); + svg.with_transformation( + Transformation::rotate(percentage(delta)) + .with_scaling(gpui_size(scale, scale)), + ) + }, + ), + ), + ) } fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { - let surface: Hsla = colors.container.into(); + let surface: Hsla = colors.container.into(); - div() - .flex() - .flex_col() - .gap_2() - .p_4() - .bg(surface.opacity(0.5)) - .rounded_lg() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child(content) + div() + .flex() + .flex_col() + .gap_2() + .p_4() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) } fn main() { - gpui_platform::application() - .with_assets(Assets {}) - .run(|cx: &mut App| { - let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| AnimationExample), - ) - .expect("Failed to open window"); + gpui_platform::application() + .with_assets(Assets {}) + .run(|cx: &mut App| { + let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| AnimationExample), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Animation"); - }); + example_prelude::init_example(cx, "Animation"); + }); } diff --git a/crates/gpui/examples/learn/async_tasks.rs b/crates/gpui/examples/learn/async_tasks.rs index 355c72ad66..f77bdc1925 100644 --- a/crates/gpui/examples/learn/async_tasks.rs +++ b/crates/gpui/examples/learn/async_tasks.rs @@ -14,8 +14,8 @@ use std::time::Duration; use gpui::colors::Colors; use gpui::{ - App, Bounds, Context, Entity, Render, Task, Window, WindowBounds, WindowOptions, div, - prelude::*, px, rgb, size, + App, Bounds, Context, Entity, Render, Task, Window, WindowBounds, WindowOptions, div, + prelude::*, px, rgb, size, }; // Example 1: Simple Foreground Task @@ -24,38 +24,38 @@ use gpui::{ // Use it when you need to perform async work that updates UI. struct ForegroundTaskDemo { - message: String, - is_loading: bool, + message: String, + is_loading: bool, } impl ForegroundTaskDemo { - fn new() -> Self { - Self { - message: "Click to start a foreground task".into(), - is_loading: false, - } - } + fn new() -> Self { + Self { + message: "Click to start a foreground task".into(), + is_loading: false, + } + } - fn start_task(&mut self, cx: &mut Context) { - self.is_loading = true; - self.message = "Loading...".into(); - cx.notify(); + fn start_task(&mut self, cx: &mut Context) { + self.is_loading = true; + self.message = "Loading...".into(); + cx.notify(); - cx.spawn(async move |this, cx| { - cx.background_spawn(async { - std::thread::sleep(Duration::from_secs(1)); - }) - .await; + cx.spawn(async move |this, cx| { + cx.background_spawn(async { + std::thread::sleep(Duration::from_secs(1)); + }) + .await; - this.update(cx, |this, cx| { - this.message = "Task completed!".into(); - this.is_loading = false; - cx.notify(); - }) - .ok(); - }) - .detach(); - } + this.update(cx, |this, cx| { + this.message = "Task completed!".into(); + this.is_loading = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } } // Example 2: Background Task with Progress @@ -64,51 +64,51 @@ impl ForegroundTaskDemo { // Use it for heavy computation that shouldn't block the UI. struct BackgroundTaskDemo { - progress: u32, - result: Option, - is_computing: bool, + progress: u32, + result: Option, + is_computing: bool, } impl BackgroundTaskDemo { - fn new() -> Self { - Self { - progress: 0, - result: None, - is_computing: false, - } - } + fn new() -> Self { + Self { + progress: 0, + result: None, + is_computing: false, + } + } - fn start_computation(&mut self, cx: &mut Context) { - self.is_computing = true; - self.progress = 0; - self.result = None; - cx.notify(); + fn start_computation(&mut self, cx: &mut Context) { + self.is_computing = true; + self.progress = 0; + self.result = None; + cx.notify(); - cx.spawn(async move |this, cx| { - for i in 0..100 { - let computation = cx.background_spawn(async move { - std::thread::sleep(Duration::from_millis(10)); - (i + 1) as u64 - }); + cx.spawn(async move |this, cx| { + for i in 0..100 { + let computation = cx.background_spawn(async move { + std::thread::sleep(Duration::from_millis(10)); + (i + 1) as u64 + }); - let partial_result = computation.await; + let partial_result = computation.await; - this.update(cx, |this, cx| { - this.progress = i as u32 + 1; - this.result = Some(partial_result); - cx.notify(); - }) - .ok(); - } + this.update(cx, |this, cx| { + this.progress = i as u32 + 1; + this.result = Some(partial_result); + cx.notify(); + }) + .ok(); + } - this.update(cx, |this, cx| { - this.is_computing = false; - cx.notify(); - }) - .ok(); - }) - .detach(); - } + this.update(cx, |this, cx| { + this.is_computing = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } } // Example 3: Cancellable Task @@ -117,50 +117,50 @@ impl BackgroundTaskDemo { // Store a task in a field to keep it running. struct CancellableTaskDemo { - counter: u32, - counting_task: Option>, + counter: u32, + counting_task: Option>, } impl CancellableTaskDemo { - fn new() -> Self { - Self { - counter: 0, - counting_task: None, - } - } + fn new() -> Self { + Self { + counter: 0, + counting_task: None, + } + } - fn is_running(&self) -> bool { - self.counting_task.is_some() - } + fn is_running(&self) -> bool { + self.counting_task.is_some() + } - fn toggle(&mut self, cx: &mut Context) { - if self.counting_task.is_some() { - self.counting_task = None; - cx.notify(); - } else { - self.counting_task = Some(cx.spawn(async move |this, cx| { - loop { - cx.background_spawn(async { - std::thread::sleep(Duration::from_millis(100)); - }) - .await; + fn toggle(&mut self, cx: &mut Context) { + if self.counting_task.is_some() { + self.counting_task = None; + cx.notify(); + } else { + self.counting_task = Some(cx.spawn(async move |this, cx| { + loop { + cx.background_spawn(async { + std::thread::sleep(Duration::from_millis(100)); + }) + .await; - let should_continue = this - .update(cx, |this, cx| { - this.counter += 1; - cx.notify(); - true - }) - .unwrap_or(false); + let should_continue = this + .update(cx, |this, cx| { + this.counter += 1; + cx.notify(); + true + }) + .unwrap_or(false); - if !should_continue { - break; - } - } - })); - cx.notify(); - } - } + if !should_continue { + break; + } + } + })); + cx.notify(); + } + } } // Example 4: Task with Return Value @@ -168,395 +168,394 @@ impl CancellableTaskDemo { // Tasks can return values that you can await. struct ReturnValueDemo { - numbers: Vec, - sum: Option, - is_calculating: bool, + numbers: Vec, + sum: Option, + is_calculating: bool, } impl ReturnValueDemo { - fn new() -> Self { - Self { - numbers: vec![1, 2, 3, 4, 5], - sum: None, - is_calculating: false, - } - } + fn new() -> Self { + Self { + numbers: vec![1, 2, 3, 4, 5], + sum: None, + is_calculating: false, + } + } - fn calculate_sum(&mut self, cx: &mut Context) { - self.is_calculating = true; - cx.notify(); + fn calculate_sum(&mut self, cx: &mut Context) { + self.is_calculating = true; + cx.notify(); - let numbers = self.numbers.clone(); + let numbers = self.numbers.clone(); - cx.spawn(async move |this, cx| { - let result = cx - .background_spawn(async move { - std::thread::sleep(Duration::from_millis(500)); - numbers.iter().sum::() - }) - .await; + cx.spawn(async move |this, cx| { + let result = cx + .background_spawn(async move { + std::thread::sleep(Duration::from_millis(500)); + numbers.iter().sum::() + }) + .await; - this.update(cx, |this, cx| { - this.sum = Some(result); - this.is_calculating = false; - cx.notify(); - }) - .ok(); - }) - .detach(); - } + this.update(cx, |this, cx| { + this.sum = Some(result); + this.is_calculating = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } - fn randomize(&mut self, cx: &mut Context) { - use std::collections::hash_map::RandomState; - use std::hash::{BuildHasher, Hasher}; - let hasher = RandomState::new().build_hasher().finish(); - self.numbers = (0..5) - .map(|i| ((hasher >> (i * 8)) & 0xFF) as i32 % 100) - .collect(); - self.sum = None; - cx.notify(); - } + fn randomize(&mut self, cx: &mut Context) { + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + let hasher = RandomState::new().build_hasher().finish(); + self.numbers = (0..5) + .map(|i| ((hasher >> (i * 8)) & 0xFF) as i32 % 100) + .collect(); + self.sum = None; + cx.notify(); + } } // Main Application struct AsyncTasksExample { - foreground_demo: Entity, - background_demo: Entity, - cancellable_demo: Entity, - return_demo: Entity, + foreground_demo: Entity, + background_demo: Entity, + cancellable_demo: Entity, + return_demo: Entity, } impl AsyncTasksExample { - fn new(cx: &mut Context) -> Self { - Self { - foreground_demo: cx.new(|_| ForegroundTaskDemo::new()), - background_demo: cx.new(|_| BackgroundTaskDemo::new()), - cancellable_demo: cx.new(|_| CancellableTaskDemo::new()), - return_demo: cx.new(|_| ReturnValueDemo::new()), - } - } + fn new(cx: &mut Context) -> Self { + Self { + foreground_demo: cx.new(|_| ForegroundTaskDemo::new()), + background_demo: cx.new(|_| BackgroundTaskDemo::new()), + cancellable_demo: cx.new(|_| CancellableTaskDemo::new()), + return_demo: cx.new(|_| ReturnValueDemo::new()), + } + } } impl Render for AsyncTasksExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let foreground = self.foreground_demo.read(cx); - let background = self.background_demo.read(cx); - let cancellable = self.cancellable_demo.read(cx); - let return_demo = self.return_demo.read(cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let foreground = self.foreground_demo.read(cx); + let background = self.background_demo.read(cx); + let cancellable = self.cancellable_demo.read(cx); + let return_demo = self.return_demo.read(cx); - div() - .id("main") - .size_full() - .p_6() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_6() - .max_w(px(500.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Async Tasks"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Spawning, background work, and task management"), - ), - ) - .child(demo_section( - &colors, - "1. Foreground Task (cx.spawn)", - "Runs async work on the UI thread. Good for sequential async operations.", - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_sm() - .text_color(colors.text) - .child(foreground.message.clone()), - ) - .child( - button(&colors, "foreground-btn", "Start Task", foreground.is_loading) - .on_click(cx.listener(|this, _, _, cx| { - this.foreground_demo.update(cx, |demo, cx| { - demo.start_task(cx); - }); - })), - ), - )) - .child(demo_section( - &colors, - "2. Background Task (cx.background_spawn)", - "Runs heavy computation off the UI thread with progress updates.", - div() - .flex() - .flex_col() - .gap_2() - .child(progress_bar(&colors, background.progress)) - .child( - div() - .text_sm() - .text_color(colors.text) - .child(format!( + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Async Tasks"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Spawning, background work, and task management"), + ), + ) + .child(demo_section( + &colors, + "1. Foreground Task (cx.spawn)", + "Runs async work on the UI thread. Good for sequential async operations.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .text_color(colors.text) + .child(foreground.message.clone()), + ) + .child( + button( + &colors, + "foreground-btn", + "Start Task", + foreground.is_loading, + ) + .on_click(cx.listener(|this, _, _, cx| { + this.foreground_demo.update(cx, |demo, cx| { + demo.start_task(cx); + }); + })), + ), + )) + .child(demo_section( + &colors, + "2. Background Task (cx.background_spawn)", + "Runs heavy computation off the UI thread with progress updates.", + div() + .flex() + .flex_col() + .gap_2() + .child(progress_bar(&colors, background.progress)) + .child(div().text_sm().text_color(colors.text).child(format!( "Progress: {}% | Result: {}", background.progress, background .result .map(|r| r.to_string()) .unwrap_or_else(|| "-".into()) - )), - ) - .child( - button(&colors, "background-btn", "Compute", background.is_computing) - .on_click(cx.listener(|this, _, _, cx| { - this.background_demo.update(cx, |demo, cx| { - demo.start_computation(cx); - }); - })), - ), - )) - .child(demo_section( - &colors, - "3. Cancellable Task", - "Store Task in a field to keep it running. Drop to cancel.", - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_2xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child(format!("{}", cancellable.counter)), - ) - .child({ - let is_running = cancellable.is_running(); - let (bg, bg_hover) = if is_running { - (rgb(0xd32f2f), rgb(0xe04545)) - } else { - (rgb(0x388e3c), rgb(0x43a047)) - }; - div() - .id("cancel-btn") - .px_3() - .py_1p5() - .rounded_md() - .text_sm() - .text_color(colors.text) - .cursor_pointer() - .bg(bg) - .hover(move |style| style.bg(bg_hover)) - .child(if is_running { "Stop" } else { "Start Counter" }) - .on_click(cx.listener(|this, _, _, cx| { - this.cancellable_demo.update(cx, |demo, cx| { - demo.toggle(cx); - }); - })) - }), - )) - .child(demo_section( - &colors, - "4. Task with Return Value", - "Tasks can return values that can be awaited or used in chained operations.", - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child(format!("Numbers: {:?}", return_demo.numbers)), - ) - .child( - div() - .text_sm() - .text_color(colors.text) - .child(format!( - "Sum: {}", - if return_demo.is_calculating { - "Calculating...".into() - } else { - return_demo - .sum - .map(|s| s.to_string()) - .unwrap_or_else(|| "Not calculated".into()) - } - )), - ) - .child( - div() - .flex() - .gap_2() - .child( - button( - &colors, - "sum-btn", - "Calculate Sum", - return_demo.is_calculating, - ) - .on_click(cx.listener(|this, _, _, cx| { - this.return_demo.update(cx, |demo, cx| { - demo.calculate_sum(cx); - }); - })), - ) - .child( - secondary_button(&colors, "random-btn", "Randomize") - .on_click(cx.listener(|this, _, _, cx| { - this.return_demo.update(cx, |demo, cx| { - demo.randomize(cx); - }); - })), - ), - ), - )) - - ) - } + ))) + .child( + button( + &colors, + "background-btn", + "Compute", + background.is_computing, + ) + .on_click(cx.listener(|this, _, _, cx| { + this.background_demo.update(cx, |demo, cx| { + demo.start_computation(cx); + }); + })), + ), + )) + .child(demo_section( + &colors, + "3. Cancellable Task", + "Store Task in a field to keep it running. Drop to cancel.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child(format!("{}", cancellable.counter)), + ) + .child({ + let is_running = cancellable.is_running(); + let (bg, bg_hover) = if is_running { + (rgb(0xd32f2f), rgb(0xe04545)) + } else { + (rgb(0x388e3c), rgb(0x43a047)) + }; + div() + .id("cancel-btn") + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(colors.text) + .cursor_pointer() + .bg(bg) + .hover(move |style| style.bg(bg_hover)) + .child(if is_running { "Stop" } else { "Start Counter" }) + .on_click(cx.listener(|this, _, _, cx| { + this.cancellable_demo.update(cx, |demo, cx| { + demo.toggle(cx); + }); + })) + }), + )) + .child(demo_section( + &colors, + "4. Task with Return Value", + "Tasks can return values that can be awaited or used in chained operations.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child(format!("Numbers: {:?}", return_demo.numbers)), + ) + .child(div().text_sm().text_color(colors.text).child(format!( + "Sum: {}", + if return_demo.is_calculating { + "Calculating...".into() + } else { + return_demo + .sum + .map(|s| s.to_string()) + .unwrap_or_else(|| "Not calculated".into()) + } + ))) + .child( + div() + .flex() + .gap_2() + .child( + button( + &colors, + "sum-btn", + "Calculate Sum", + return_demo.is_calculating, + ) + .on_click(cx.listener(|this, _, _, cx| { + this.return_demo.update(cx, |demo, cx| { + demo.calculate_sum(cx); + }); + })), + ) + .child( + secondary_button(&colors, "random-btn", "Randomize") + .on_click(cx.listener(|this, _, _, cx| { + this.return_demo.update(cx, |demo, cx| { + demo.randomize(cx); + }); + })), + ), + ), + )), + ) + } } // Helper Components fn demo_section( - colors: &Colors, - title: &'static str, - description: &'static str, - content: impl IntoElement, + colors: &Colors, + title: &'static str, + description: &'static str, + content: impl IntoElement, ) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(colors.container) - .border_1() - .border_color(colors.border) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child(description), - ), - ) - .child(content) + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.container) + .border_1() + .border_color(colors.border) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child(description), + ), + ) + .child(content) } fn button( - colors: &Colors, - id: impl Into, - label: &'static str, - disabled: bool, + colors: &Colors, + id: impl Into, + label: &'static str, + disabled: bool, ) -> gpui::Stateful { - let disabled_bg = colors.selected; - let bg = colors.selected; - let bg_hover = colors.selected; - let bg_active = colors.selected; - let text = colors.selected_text; + let disabled_bg = colors.selected; + let bg = colors.selected; + let bg_hover = colors.selected; + let bg_active = colors.selected; + let text = colors.selected_text; - div() - .id(id) - .px_3() - .py_1p5() - .rounded_md() - .text_sm() - .text_color(text) - .when(disabled, |el| { - el.bg(disabled_bg).cursor_not_allowed().opacity(0.6) - }) - .when(!disabled, |el| { - el.bg(bg) - .cursor_pointer() - .hover(move |style| style.bg(bg_hover)) - .active(move |style| style.bg(bg_active)) - }) - .child(label) + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(text) + .when(disabled, |el| { + el.bg(disabled_bg).cursor_not_allowed().opacity(0.6) + }) + .when(!disabled, |el| { + el.bg(bg) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .active(move |style| style.bg(bg_active)) + }) + .child(label) } fn secondary_button( - colors: &Colors, - id: impl Into, - label: &'static str, + colors: &Colors, + id: impl Into, + label: &'static str, ) -> gpui::Stateful { - let bg = colors.selected; - let bg_hover = colors.border; - let text = colors.text; + let bg = colors.selected; + let bg_hover = colors.border; + let text = colors.text; - div() - .id(id) - .px_3() - .py_1p5() - .rounded_md() - .text_sm() - .text_color(text) - .bg(bg) - .cursor_pointer() - .hover(move |style| style.bg(bg_hover)) - .child(label) + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(text) + .bg(bg) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .child(label) } fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement { - let clamped = progress.min(100); - let bar_bg = colors.selected; - let bar_fill = rgb(0x388e3c); + let clamped = progress.min(100); + let bar_bg = colors.selected; + let bar_fill = rgb(0x388e3c); - div() - .h_2() - .w_full() - .rounded_full() - .bg(bar_bg) - .overflow_hidden() - .child( - div() - .h_full() - .rounded_full() - .bg(bar_fill) - .w(gpui::relative(clamped as f32 / 100.0)), - ) + div() + .h_2() + .w_full() + .rounded_full() + .bg(bar_bg) + .overflow_hidden() + .child( + div() + .h_full() + .rounded_full() + .bg(bar_fill) + .w(gpui::relative(clamped as f32 / 100.0)), + ) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|cx| AsyncTasksExample::new(cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| AsyncTasksExample::new(cx)), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Async Tasks"); - }); + example_prelude::init_example(cx, "Async Tasks"); + }); } diff --git a/crates/gpui/examples/learn/blur.rs b/crates/gpui/examples/learn/blur.rs index 482d5d7bb3..9d66fcf57f 100644 --- a/crates/gpui/examples/learn/blur.rs +++ b/crates/gpui/examples/learn/blur.rs @@ -19,130 +19,130 @@ //! continuous and the seams are clean — the CSS "blur the wrapper" idiom). use gpui::{ - App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, deferred, div, point, - prelude::*, px, rgb, rgba, size, + App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, deferred, div, point, + prelude::*, px, rgb, rgba, size, }; struct BlurExample; /// A vivid, gap-free background so blur is obvious: a grid of saturated tiles filling the window. fn busy_background() -> impl IntoElement { - let palette = [ - 0xef4444, 0xf97316, 0xeab308, 0x22c55e, 0x06b6d4, 0x3b82f6, 0x8b5cf6, 0xec4899, - ]; - let mut next = 0usize; - div() - .absolute() - .inset_0() - .flex() - .flex_col() - .children((0..9).map(|_| { - div().flex().flex_1().children( - (0..8) - .map(|_| { - let hex = palette[next % palette.len()]; - next += 1; - div() - .flex_1() - .h_full() - .bg(rgb(hex)) - .flex() - .items_center() - .justify_center() - .text_color(rgb(0xffffff)) - .text_xl() - .child("◆") - }) - .collect::>(), - ) - })) + let palette = [ + 0xef4444, 0xf97316, 0xeab308, 0x22c55e, 0x06b6d4, 0x3b82f6, 0x8b5cf6, 0xec4899, + ]; + let mut next = 0usize; + div() + .absolute() + .inset_0() + .flex() + .flex_col() + .children((0..9).map(|_| { + div().flex().flex_1().children( + (0..8) + .map(|_| { + let hex = palette[next % palette.len()]; + next += 1; + div() + .flex_1() + .h_full() + .bg(rgb(hex)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(0xffffff)) + .text_xl() + .child("◆") + }) + .collect::>(), + ) + })) } /// A translucent rounded panel that frosts the content behind it. fn frosted_panel() -> impl IntoElement { - div() - .absolute() - .left(px(60.)) - .top(px(120.)) - .w(px(360.)) - .h(px(200.)) - .rounded_xl() - .bg(rgba(0xffffff30)) - .backdrop_blur(px(24.)) - .border_1() - .border_color(rgba(0xffffff60)) - .flex() - .items_center() - .justify_center() - .text_color(rgb(0x111111)) - .text_2xl() - .child("backdrop_blur(24px)") + div() + .absolute() + .left(px(60.)) + .top(px(120.)) + .w(px(360.)) + .h(px(200.)) + .rounded_xl() + .bg(rgba(0xffffff30)) + .backdrop_blur(px(24.)) + .border_1() + .border_color(rgba(0xffffff60)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(0x111111)) + .text_2xl() + .child("backdrop_blur(24px)") } /// A popover painted via `deferred()` so it sits above everything; its backdrop blur must /// still pick up the panel and background beneath it. fn deferred_popover() -> impl IntoElement { - deferred( - div() - .absolute() - .left(px(260.)) - .top(px(260.)) - .w(px(300.)) - .h(px(150.)) - .rounded_lg() - .bg(rgba(0x1e293b66)) - .backdrop_blur(px(12.)) - .border_1() - .border_color(rgba(0xffffffaa)) - .flex() - .items_center() - .justify_center() - .text_color(rgb(0xffffff)) - .text_xl() - .child("deferred + backdrop_blur"), - ) + deferred( + div() + .absolute() + .left(px(260.)) + .top(px(260.)) + .w(px(300.)) + .h(px(150.)) + .rounded_lg() + .bg(rgba(0x1e293b66)) + .backdrop_blur(px(12.)) + .border_1() + .border_color(rgba(0xffffffaa)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(0xffffff)) + .text_xl() + .child("deferred + backdrop_blur"), + ) } /// A self-blurred element (CSS `filter: blur`) — its own content is blurred as a group. fn content_blurred() -> impl IntoElement { - div() - .absolute() - .left(px(120.)) - .top(px(420.)) - .w(px(280.)) - .h(px(120.)) - .blur(px(5.)) - .bg(rgb(0x0f172a)) - .rounded_lg() - .flex() - .items_center() - .justify_center() - .text_color(rgb(0xfacc15)) - .text_3xl() - .child("blur(5px) content") + div() + .absolute() + .left(px(120.)) + .top(px(420.)) + .w(px(280.)) + .h(px(120.)) + .blur(px(5.)) + .bg(rgb(0x0f172a)) + .rounded_lg() + .flex() + .items_center() + .justify_center() + .text_color(rgb(0xfacc15)) + .text_3xl() + .child("blur(5px) content") } /// A content-blurred element with richer content (a row of colored chips), so the `filter: blur` /// effect — the element and its children blurred as one group — is clearly visible. fn content_blurred_rich() -> impl IntoElement { - div() - .absolute() - .left(px(120.)) - .top(px(580.)) - .w(px(280.)) - .h(px(120.)) - .blur(px(6.)) - .bg(rgb(0x1e293b)) - .rounded_lg() - .flex() - .items_center() - .justify_center() - .gap_3() - .children( - [0xef4444, 0x22c55e, 0x3b82f6] - .into_iter() - .map(|hex| div().w(px(48.)).h(px(48.)).rounded_md().bg(rgb(hex))), - ) + div() + .absolute() + .left(px(120.)) + .top(px(580.)) + .w(px(280.)) + .h(px(120.)) + .blur(px(6.)) + .bg(rgb(0x1e293b)) + .rounded_lg() + .flex() + .items_center() + .justify_center() + .gap_3() + .children( + [0xef4444, 0x22c55e, 0x3b82f6] + .into_iter() + .map(|hex| div().w(px(48.)).h(px(48.)).rounded_md().bg(rgb(hex))), + ) } /// Nested content blur: a `blur()` element inside another `blur()` element. The inner block is @@ -150,37 +150,37 @@ fn content_blurred_rich() -> impl IntoElement { /// per-nesting-level isolated group textures (up to `MAX_FILTER_DEPTH`). The inner content should /// read as markedly softer than the outer block's own text. fn nested_content_blurred() -> impl IntoElement { - div() - .absolute() - .left(px(740.)) - .top(px(80.)) - .w(px(290.)) - .h(px(220.)) - .blur(px(3.)) - .bg(rgb(0x1e293b)) - .rounded_xl() - .flex() - .flex_col() - .items_center() - .justify_center() - .gap_4() - .text_color(rgb(0xe2e8f0)) - .text_xl() - .child("outer blur(3px)") - .child( - div() - .w(px(190.)) - .h(px(100.)) - .blur(px(8.)) - .bg(rgb(0xf59e0b)) - .rounded_lg() - .flex() - .items_center() - .justify_center() - .text_color(rgb(0x111111)) - .text_2xl() - .child("inner blur(8px)"), - ) + div() + .absolute() + .left(px(740.)) + .top(px(80.)) + .w(px(290.)) + .h(px(220.)) + .blur(px(3.)) + .bg(rgb(0x1e293b)) + .rounded_xl() + .flex() + .flex_col() + .items_center() + .justify_center() + .gap_4() + .text_color(rgb(0xe2e8f0)) + .text_xl() + .child("outer blur(3px)") + .child( + div() + .w(px(190.)) + .h(px(100.)) + .blur(px(8.)) + .bg(rgb(0xf59e0b)) + .rounded_lg() + .flex() + .items_center() + .justify_center() + .text_color(rgb(0x111111)) + .text_2xl() + .child("inner blur(8px)"), + ) } const SEAM_COLORS: [u32; 4] = [0xef4444, 0x22c55e, 0x3b82f6, 0xeab308]; @@ -191,16 +191,16 @@ const SEAM_COLORS: [u32; 4] = [0xef4444, 0x22c55e, 0x3b82f6, 0xeab308]; /// parent would not clip the blurred children to its radius and the busy background would leak /// through the corner triangles — a separate concern from the seam blending under test here. fn seam_block(i: usize, hex: u32, blur_each: bool) -> impl IntoElement { - let block = div().flex_1().h_full(); - let block = if blur_each { block.blur(px(5.)) } else { block }; - block - .bg(rgb(hex)) - .flex() - .items_center() - .justify_center() - .text_color(rgb(0xffffff)) - .text_2xl() - .child(format!("{}", i + 1)) + let block = div().flex_1().h_full(); + let block = if blur_each { block.blur(px(5.)) } else { block }; + block + .bg(rgb(hex)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(0xffffff)) + .text_2xl() + .child(format!("{}", i + 1)) } /// Adjacent blocks, each its OWN content-filter group (`blur` on every block), no gap, dark parent. @@ -208,20 +208,20 @@ fn seam_block(i: usize, hex: u32, blur_each: bool) -> impl IntoElement { /// composites independently, so the dark parent shows through each seam by roughly `α_left · α_right` /// (peaking at ~25% right on the seam). This matches the web — the clean alternative is the next panel. fn adjacent_per_block_blur() -> impl IntoElement { - div() - .absolute() - .left(px(740.)) - .top(px(350.)) - .w(px(290.)) - .h(px(110.)) - .bg(rgb(0x050505)) - .flex() - .children( - SEAM_COLORS - .into_iter() - .enumerate() - .map(|(i, hex)| seam_block(i, hex, true)), - ) + div() + .absolute() + .left(px(740.)) + .top(px(350.)) + .w(px(290.)) + .h(px(110.)) + .bg(rgb(0x050505)) + .flex() + .children( + SEAM_COLORS + .into_iter() + .enumerate() + .map(|(i, hex)| seam_block(i, hex, true)), + ) } /// The same adjacent blocks, but `blur` is on the PARENT — one content-filter group covering all @@ -229,87 +229,87 @@ fn adjacent_per_block_blur() -> impl IntoElement { /// is continuous across the seams and only the group's outer edge fades. This is the CSS "blur the /// wrapper, not each child" idiom, and the seams come out clean. fn adjacent_group_blur() -> impl IntoElement { - div() - .absolute() - .left(px(740.)) - .top(px(510.)) - .w(px(290.)) - .h(px(110.)) - .bg(rgb(0x050505)) - .blur(px(5.)) - .flex() - .children( - SEAM_COLORS - .into_iter() - .enumerate() - .map(|(i, hex)| seam_block(i, hex, false)), - ) + div() + .absolute() + .left(px(740.)) + .top(px(510.)) + .w(px(290.)) + .h(px(110.)) + .bg(rgb(0x050505)) + .blur(px(5.)) + .flex() + .children( + SEAM_COLORS + .into_iter() + .enumerate() + .map(|(i, hex)| seam_block(i, hex, false)), + ) } /// A small dark pill label so the two new test sections are identifiable over the busy background. fn caption(text: &'static str, left: f32, top: f32) -> impl IntoElement { - div() - .absolute() - .left(px(left)) - .top(px(top)) - .px_2() - .py_1() - .rounded_md() - .bg(rgba(0x000000cc)) - .text_color(rgb(0xffffff)) - .text_sm() - .child(text) + div() + .absolute() + .left(px(left)) + .top(px(top)) + .px_2() + .py_1() + .rounded_md() + .bg(rgba(0x000000cc)) + .text_color(rgb(0xffffff)) + .text_sm() + .child(text) } impl Render for BlurExample { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .relative() - .size_full() - .bg(rgb(0x000000)) - .child(busy_background()) - .child(frosted_panel()) - .child(content_blurred()) - .child(content_blurred_rich()) - .child(nested_content_blurred()) - .child(adjacent_per_block_blur()) - .child(adjacent_group_blur()) - .child(caption("nested content blur", 740., 50.)) - .child(caption( - "adjacent — blur each block (seams, = CSS)", - 740., - 322., - )) - .child(caption( - "adjacent — blur the parent (one group, clean)", - 740., - 482., - )) - .child(deferred_popover()) - } + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .relative() + .size_full() + .bg(rgb(0x000000)) + .child(busy_background()) + .child(frosted_panel()) + .child(content_blurred()) + .child(content_blurred_rich()) + .child(nested_content_blurred()) + .child(adjacent_per_block_blur()) + .child(adjacent_group_blur()) + .child(caption("nested content blur", 740., 50.)) + .child(caption( + "adjacent — blur each block (seams, = CSS)", + 740., + 322., + )) + .child(caption( + "adjacent — blur the parent (one group, clean)", + 740., + 482., + )) + .child(deferred_popover()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); + gpui_platform::application().run(|cx: &mut App| { + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); - let bounds = Bounds { - origin: point(px(100.), px(100.)), - size: size(px(1060.), px(760.)), - }; - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| BlurExample), - ) - .expect("failed to open window"); - }); + let bounds = Bounds { + origin: point(px(100.), px(100.)), + size: size(px(1060.), px(760.)), + }; + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| BlurExample), + ) + .expect("failed to open window"); + }); } diff --git a/crates/gpui/examples/learn/creating_components.rs b/crates/gpui/examples/learn/creating_components.rs index 3fa08c6261..486ceee76f 100644 --- a/crates/gpui/examples/learn/creating_components.rs +++ b/crates/gpui/examples/learn/creating_components.rs @@ -13,8 +13,8 @@ mod example_prelude; use example_prelude::init_example; use gpui::colors::Colors; use gpui::{ - App, Bounds, Context, Entity, IntoElement, Render, RenderOnce, Window, WindowBounds, - WindowOptions, div, prelude::*, px, rgb, size, + App, Bounds, Context, Entity, IntoElement, Render, RenderOnce, Window, WindowBounds, + WindowOptions, div, prelude::*, px, rgb, size, }; // ============================================================================ @@ -35,84 +35,84 @@ use gpui::{ // - State is tied to call site location struct UseStateCounter { - count: i32, + count: i32, } fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl IntoElement { - let state: Entity = - window.use_state(cx, |_window, _cx| UseStateCounter { count: 0 }); + let state: Entity = + window.use_state(cx, |_window, _cx| UseStateCounter { count: 0 }); - let count = state.read(cx).count; + let count = state.read(cx).count; - let error = rgb(0xd32f2f); - let error_hover = rgb(0xe04545); - let success = rgb(0x388e3c); - let success_hover = rgb(0x43a047); + let error = rgb(0xd32f2f); + let error_hover = rgb(0xe04545); + let success = rgb(0x388e3c); + let success_hover = rgb(0x43a047); - div() - .id("use-state-counter") - .flex() - .flex_col() - .gap_2() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("use_state Counter"), - ) - .child( - div() - .text_2xl() - .text_color(colors.text) - .child(format!("{}", count)), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("use-state-decrement") - .px_3() - .py_1() - .rounded_md() - .bg(error) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(error_hover)) - .child("−") - .on_click({ - let state = state.clone(); - move |_, _, cx| { - state.update(cx, |state, cx| { - state.count -= 1; - cx.notify(); - }); - } - }), - ) - .child( - div() - .id("use-state-increment") - .px_3() - .py_1() - .rounded_md() - .bg(success) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(success_hover)) - .child("+") - .on_click(move |_, _, cx| { - state.update(cx, |state, cx| { - state.count += 1; - cx.notify(); - }); - }), - ), - ) + div() + .id("use-state-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("use_state Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("use-state-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .on_click({ + let state = state.clone(); + move |_, _, cx| { + state.update(cx, |state, cx| { + state.count -= 1; + cx.notify(); + }); + } + }), + ) + .child( + div() + .id("use-state-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .on_click(move |_, _, cx| { + state.update(cx, |state, cx| { + state.count += 1; + cx.notify(); + }); + }), + ), + ) } // ============================================================================ @@ -135,97 +135,97 @@ fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl #[derive(IntoElement)] struct RenderOnceCounter { - colors: Colors, - count: i32, - on_increment: Option>, - on_decrement: Option>, + colors: Colors, + count: i32, + on_increment: Option>, + on_decrement: Option>, } impl RenderOnceCounter { - fn new(colors: Colors, count: i32) -> Self { - Self { - colors, - count, - on_increment: None, - on_decrement: None, - } - } + fn new(colors: Colors, count: i32) -> Self { + Self { + colors, + count, + on_increment: None, + on_decrement: None, + } + } - fn on_increment(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.on_increment = Some(Box::new(callback)); - self - } + fn on_increment(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_increment = Some(Box::new(callback)); + self + } - fn on_decrement(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.on_decrement = Some(Box::new(callback)); - self - } + fn on_decrement(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_decrement = Some(Box::new(callback)); + self + } } impl RenderOnce for RenderOnceCounter { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let colors = self.colors; - let error = rgb(0xd32f2f); - let error_hover = rgb(0xe04545); - let success = rgb(0x388e3c); - let success_hover = rgb(0x43a047); + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let colors = self.colors; + let error = rgb(0xd32f2f); + let error_hover = rgb(0xe04545); + let success = rgb(0x388e3c); + let success_hover = rgb(0x43a047); - div() - .id("render-once-counter") - .flex() - .flex_col() - .gap_2() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("RenderOnce Counter"), - ) - .child( - div() - .text_2xl() - .text_color(colors.text) - .child(format!("{}", self.count)), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("render-once-decrement") - .px_3() - .py_1() - .rounded_md() - .bg(error) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(error_hover)) - .child("−") - .when_some(self.on_decrement, |element, callback| { - element.on_click(move |_, window, cx| callback(window, cx)) - }), - ) - .child( - div() - .id("render-once-increment") - .px_3() - .py_1() - .rounded_md() - .bg(success) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(success_hover)) - .child("+") - .when_some(self.on_increment, |element, callback| { - element.on_click(move |_, window, cx| callback(window, cx)) - }), - ), - ) - } + div() + .id("render-once-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("RenderOnce Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", self.count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("render-once-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .when_some(self.on_decrement, |element, callback| { + element.on_click(move |_, window, cx| callback(window, cx)) + }), + ) + .child( + div() + .id("render-once-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .when_some(self.on_increment, |element, callback| { + element.on_click(move |_, window, cx| callback(window, cx)) + }), + ), + ) + } } // ============================================================================ @@ -248,89 +248,89 @@ impl RenderOnce for RenderOnceCounter { // - More complex lifecycle struct RenderCounter { - count: i32, + count: i32, } impl RenderCounter { - fn new() -> Self { - Self { count: 0 } - } + fn new() -> Self { + Self { count: 0 } + } - fn increment(&mut self, _window: &mut Window, cx: &mut Context) { - self.count += 1; - cx.notify(); - } + fn increment(&mut self, _window: &mut Window, cx: &mut Context) { + self.count += 1; + cx.notify(); + } - fn decrement(&mut self, _window: &mut Window, cx: &mut Context) { - self.count -= 1; - cx.notify(); - } + fn decrement(&mut self, _window: &mut Window, cx: &mut Context) { + self.count -= 1; + cx.notify(); + } } impl Render for RenderCounter { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let error = rgb(0xd32f2f); - let error_hover = rgb(0xe04545); - let success = rgb(0x388e3c); - let success_hover = rgb(0x43a047); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let error = rgb(0xd32f2f); + let error_hover = rgb(0xe04545); + let success = rgb(0x388e3c); + let success_hover = rgb(0x43a047); - div() - .id("render-counter") - .flex() - .flex_col() - .gap_2() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Render Counter"), - ) - .child( - div() - .text_2xl() - .text_color(colors.text) - .child(format!("{}", self.count)), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("render-decrement") - .px_3() - .py_1() - .rounded_md() - .bg(error) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(error_hover)) - .child("−") - .on_click(cx.listener(|this, _, window, cx| { - this.decrement(window, cx); - })), - ) - .child( - div() - .id("render-increment") - .px_3() - .py_1() - .rounded_md() - .bg(success) - .text_color(colors.selected_text) - .cursor_pointer() - .hover(move |style| style.bg(success_hover)) - .child("+") - .on_click(cx.listener(|this, _, window, cx| { - this.increment(window, cx); - })), - ), - ) - } + div() + .id("render-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Render Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", self.count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("render-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .on_click(cx.listener(|this, _, window, cx| { + this.decrement(window, cx); + })), + ) + .child( + div() + .id("render-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .on_click(cx.listener(|this, _, window, cx| { + this.increment(window, cx); + })), + ), + ) + } } // ============================================================================ @@ -338,98 +338,98 @@ impl Render for RenderCounter { // ============================================================================ struct CreatingComponentsExample { - render_counter: Entity, - render_once_count: i32, + render_counter: Entity, + render_once_count: i32, } impl CreatingComponentsExample { - fn new(cx: &mut Context) -> Self { - Self { - render_counter: cx.new(|_| RenderCounter::new()), - render_once_count: 0, - } - } + fn new(cx: &mut Context) -> Self { + Self { + render_counter: cx.new(|_| RenderCounter::new()), + render_once_count: 0, + } + } } impl Render for CreatingComponentsExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let render_once_count = self.render_once_count; - let handle = cx.entity().downgrade(); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let render_once_count = self.render_once_count; + let handle = cx.entity().downgrade(); - div() - .id("main") - .size_full() - .flex() - .flex_col() - .gap_6() - .p_8() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_2xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Creating Components"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Three approaches to stateful components in GPUI"), - ), - ) - .child( - div() - .flex() - .flex_row() - .gap_4() - .child(use_state_counter(&colors, window, cx)) - .child( - RenderOnceCounter::new(colors.clone(), render_once_count) - .on_increment({ - let handle = handle.clone(); - move |_window, cx| { - handle - .update(cx, |this, cx| { - this.render_once_count += 1; - cx.notify(); - }) - .ok(); - } - }) - .on_decrement(move |_window, cx| { - handle - .update(cx, |this, cx| { - this.render_once_count -= 1; - cx.notify(); - }) - .ok(); - }), - ) - .child(self.render_counter.clone()), - ) - } + div() + .id("main") + .size_full() + .flex() + .flex_col() + .gap_6() + .p_8() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Creating Components"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Three approaches to stateful components in GPUI"), + ), + ) + .child( + div() + .flex() + .flex_row() + .gap_4() + .child(use_state_counter(&colors, window, cx)) + .child( + RenderOnceCounter::new(colors.clone(), render_once_count) + .on_increment({ + let handle = handle.clone(); + move |_window, cx| { + handle + .update(cx, |this, cx| { + this.render_once_count += 1; + cx.notify(); + }) + .ok(); + } + }) + .on_decrement(move |_window, cx| { + handle + .update(cx, |this, cx| { + this.render_once_count -= 1; + cx.notify(); + }) + .ok(); + }), + ) + .child(self.render_counter.clone()), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|cx| CreatingComponentsExample::new(cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| CreatingComponentsExample::new(cx)), + ) + .expect("Failed to open window"); - init_example(cx, "Creating Components"); - }); + init_example(cx, "Creating Components"); + }); } diff --git a/crates/gpui/examples/learn/custom_drawing.rs b/crates/gpui/examples/learn/custom_drawing.rs index 08d013e7cd..7751f1d06c 100644 --- a/crates/gpui/examples/learn/custom_drawing.rs +++ b/crates/gpui/examples/learn/custom_drawing.rs @@ -9,9 +9,9 @@ use gpui::colors::Colors; use gpui::{ - App, Bounds, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, - PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds, WindowOptions, canvas, div, - fill, point, prelude::*, px, rgb, size, + App, Bounds, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, + PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds, WindowOptions, canvas, div, + fill, point, prelude::*, px, rgb, size, }; #[path = "../shared/prelude.rs"] @@ -24,42 +24,42 @@ mod example_prelude; // - paint: Called during paint to actually draw fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement { - let error = rgb(0xd32f2f); - let success = rgb(0x388e3c); - let accent = colors.selected; + let error = rgb(0xd32f2f); + let success = rgb(0x388e3c); + let accent = colors.selected; - canvas( - move |_bounds, _window, _cx| {}, - move |bounds, _prepaint_state, window, _cx| { - // Draw a filled rectangle - window.paint_quad(fill( - Bounds { - origin: point(bounds.origin.x + px(10.), bounds.origin.y + px(10.)), - size: size(px(60.), px(40.)), - }, - error, - )); + canvas( + move |_bounds, _window, _cx| {}, + move |bounds, _prepaint_state, window, _cx| { + // Draw a filled rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(10.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + error, + )); - // Draw another rectangle - window.paint_quad(fill( - Bounds { - origin: point(bounds.origin.x + px(80.), bounds.origin.y + px(10.)), - size: size(px(60.), px(40.)), - }, - success, - )); + // Draw another rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(80.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + success, + )); - // Draw a third rectangle - window.paint_quad(fill( - Bounds { - origin: point(bounds.origin.x + px(150.), bounds.origin.y + px(10.)), - size: size(px(60.), px(40.)), - }, - accent, - )); - }, - ) - .size_full() + // Draw a third rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(150.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + accent, + )); + }, + ) + .size_full() } // Example 2: Custom Paths with PathBuilder @@ -71,80 +71,80 @@ fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement { // - close: Close the current subpath fn create_star(center: Point, outer_radius: f32, inner_radius: f32) -> Path { - let mut builder = PathBuilder::fill(); - let points = 5; + let mut builder = PathBuilder::fill(); + let points = 5; - for i in 0..points * 2 { - let angle = - std::f32::consts::PI / 2.0 - (i as f32) * std::f32::consts::PI / (points as f32); - let radius = if i % 2 == 0 { - outer_radius - } else { - inner_radius - }; + for i in 0..points * 2 { + let angle = + std::f32::consts::PI / 2.0 - (i as f32) * std::f32::consts::PI / (points as f32); + let radius = if i % 2 == 0 { + outer_radius + } else { + inner_radius + }; - let x = center.x + px(angle.cos() * radius); - let y = center.y - px(angle.sin() * radius); + let x = center.x + px(angle.cos() * radius); + let y = center.y - px(angle.sin() * radius); - if i == 0 { - builder.move_to(point(x, y)); - } else { - builder.line_to(point(x, y)); - } - } + if i == 0 { + builder.move_to(point(x, y)); + } else { + builder.line_to(point(x, y)); + } + } - builder.close(); - builder.build().unwrap() + builder.close(); + builder.build().unwrap() } fn create_triangle(p1: Point, p2: Point, p3: Point) -> Path { - let mut builder = PathBuilder::fill(); - builder.move_to(p1); - builder.line_to(p2); - builder.line_to(p3); - builder.close(); - builder.build().unwrap() + let mut builder = PathBuilder::fill(); + builder.move_to(p1); + builder.line_to(p2); + builder.line_to(p3); + builder.close(); + builder.build().unwrap() } fn custom_paths_canvas(colors: &Colors) -> impl IntoElement { - let warning = rgb(0xf9a825); - let accent = colors.selected; + let warning = rgb(0xf9a825); + let accent = colors.selected; - canvas( - move |_bounds, _window, _cx| {}, - move |bounds, _, window, _cx| { - let center_y = bounds.origin.y + bounds.size.height / 2.0; + canvas( + move |_bounds, _window, _cx| {}, + move |bounds, _, window, _cx| { + let center_y = bounds.origin.y + bounds.size.height / 2.0; - // Draw a star - let star_center = point(bounds.origin.x + px(50.), center_y); - let star = create_star(star_center, 30., 15.); - window.paint_path(star, warning); + // Draw a star + let star_center = point(bounds.origin.x + px(50.), center_y); + let star = create_star(star_center, 30., 15.); + window.paint_path(star, warning); - // Draw a triangle - let tri_base_x = bounds.origin.x + px(120.); - let triangle = create_triangle( - point(tri_base_x + px(30.), center_y - px(25.)), - point(tri_base_x, center_y + px(25.)), - point(tri_base_x + px(60.), center_y + px(25.)), - ); - window.paint_path(triangle, rgb(0x8b5cf6)); // Purple + // Draw a triangle + let tri_base_x = bounds.origin.x + px(120.); + let triangle = create_triangle( + point(tri_base_x + px(30.), center_y - px(25.)), + point(tri_base_x, center_y + px(25.)), + point(tri_base_x + px(60.), center_y + px(25.)), + ); + window.paint_path(triangle, rgb(0x8b5cf6)); // Purple - // Draw a custom shape (arrow) - let arrow_x = bounds.origin.x + px(200.); - let mut arrow_builder = PathBuilder::fill(); - arrow_builder.move_to(point(arrow_x, center_y)); - arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(20.))); - arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(10.))); - arrow_builder.line_to(point(arrow_x + px(50.), center_y - px(10.))); - arrow_builder.line_to(point(arrow_x + px(50.), center_y + px(10.))); - arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(10.))); - arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(20.))); - arrow_builder.close(); - let arrow = arrow_builder.build().unwrap(); - window.paint_path(arrow, accent); - }, - ) - .size_full() + // Draw a custom shape (arrow) + let arrow_x = bounds.origin.x + px(200.); + let mut arrow_builder = PathBuilder::fill(); + arrow_builder.move_to(point(arrow_x, center_y)); + arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(20.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(10.))); + arrow_builder.line_to(point(arrow_x + px(50.), center_y - px(10.))); + arrow_builder.line_to(point(arrow_x + px(50.), center_y + px(10.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(10.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(20.))); + arrow_builder.close(); + let arrow = arrow_builder.build().unwrap(); + window.paint_path(arrow, accent); + }, + ) + .size_full() } // Example 3: Interactive Drawing @@ -152,329 +152,329 @@ fn custom_paths_canvas(colors: &Colors) -> impl IntoElement { // Combine canvas with mouse events for interactive drawing struct DrawingCanvas { - lines: Vec>>, - current_line: Vec>, - is_drawing: bool, - color_index: usize, + lines: Vec>>, + current_line: Vec>, + is_drawing: bool, + color_index: usize, } impl DrawingCanvas { - fn new() -> Self { - Self { - lines: Vec::new(), - current_line: Vec::new(), - is_drawing: false, - color_index: 0, - } - } + fn new() -> Self { + Self { + lines: Vec::new(), + current_line: Vec::new(), + is_drawing: false, + color_index: 0, + } + } - fn get_colors(colors: &Colors) -> Vec { - vec![ - rgb(0xd32f2f), - rgb(0x388e3c), - colors.selected, - rgb(0xf9a825), - rgb(0x8b5cf6), // Purple - rgb(0x06b6d4), // Cyan - ] - } + fn get_colors(colors: &Colors) -> Vec { + vec![ + rgb(0xd32f2f), + rgb(0x388e3c), + colors.selected, + rgb(0xf9a825), + rgb(0x8b5cf6), // Purple + rgb(0x06b6d4), // Cyan + ] + } - fn current_color(&self, colors: &Colors) -> Rgba { - let palette = Self::get_colors(colors); - palette[self.color_index % palette.len()] - } + fn current_color(&self, colors: &Colors) -> Rgba { + let palette = Self::get_colors(colors); + palette[self.color_index % palette.len()] + } - fn next_color(&mut self, colors: &Colors) { - let palette = Self::get_colors(colors); - self.color_index = (self.color_index + 1) % palette.len(); - } + fn next_color(&mut self, colors: &Colors) { + let palette = Self::get_colors(colors); + self.color_index = (self.color_index + 1) % palette.len(); + } - fn on_mouse_down( - &mut self, - event: &MouseDownEvent, - _window: &mut Window, - cx: &mut Context, - ) { - if event.button == MouseButton::Left { - self.is_drawing = true; - self.current_line = vec![event.position]; - cx.notify(); - } - } + fn on_mouse_down( + &mut self, + event: &MouseDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if event.button == MouseButton::Left { + self.is_drawing = true; + self.current_line = vec![event.position]; + cx.notify(); + } + } - fn on_mouse_move( - &mut self, - event: &MouseMoveEvent, - _window: &mut Window, - cx: &mut Context, - ) { - if self.is_drawing { - self.current_line.push(event.position); - cx.notify(); - } - } + fn on_mouse_move( + &mut self, + event: &MouseMoveEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if self.is_drawing { + self.current_line.push(event.position); + cx.notify(); + } + } - fn on_mouse_up(&mut self, _event: &MouseUpEvent, window: &mut Window, cx: &mut Context) { - if self.is_drawing && self.current_line.len() > 1 { - self.lines.push(std::mem::take(&mut self.current_line)); - let colors = Colors::for_appearance(window); - self.next_color(&colors); - } - self.is_drawing = false; - self.current_line.clear(); - cx.notify(); - } + fn on_mouse_up(&mut self, _event: &MouseUpEvent, window: &mut Window, cx: &mut Context) { + if self.is_drawing && self.current_line.len() > 1 { + self.lines.push(std::mem::take(&mut self.current_line)); + let colors = Colors::for_appearance(window); + self.next_color(&colors); + } + self.is_drawing = false; + self.current_line.clear(); + cx.notify(); + } - fn clear(&mut self, cx: &mut Context) { - self.lines.clear(); - self.current_line.clear(); - self.color_index = 0; - cx.notify(); - } + fn clear(&mut self, cx: &mut Context) { + self.lines.clear(); + self.current_line.clear(); + self.color_index = 0; + cx.notify(); + } - fn draw_line(window: &mut Window, points: &[Point], color: Rgba) { - if points.len() < 2 { - return; - } + fn draw_line(window: &mut Window, points: &[Point], color: Rgba) { + if points.len() < 2 { + return; + } - for pair in points.windows(2) { - let start = pair[0]; - let end = pair[1]; + for pair in points.windows(2) { + let start = pair[0]; + let end = pair[1]; - let dx = end.x - start.x; - let dy = end.y - start.y; + let dx = end.x - start.x; + let dy = end.y - start.y; - let dx_f = f32::from(dx); - let dy_f = f32::from(dy); - let len = (dx_f * dx_f + dy_f * dy_f).sqrt(); + let dx_f = f32::from(dx); + let dy_f = f32::from(dy); + let len = (dx_f * dx_f + dy_f * dy_f).sqrt(); - if len < 0.1 { - continue; - } + if len < 0.1 { + continue; + } - let thickness = 3.0_f32; - let px_offset = px(-dy_f / len * thickness / 2.0); - let py_offset = px(dx_f / len * thickness / 2.0); + let thickness = 3.0_f32; + let px_offset = px(-dy_f / len * thickness / 2.0); + let py_offset = px(dx_f / len * thickness / 2.0); - let mut builder = PathBuilder::fill(); - builder.move_to(point(start.x + px_offset, start.y + py_offset)); - builder.line_to(point(end.x + px_offset, end.y + py_offset)); - builder.line_to(point(end.x - px_offset, end.y - py_offset)); - builder.line_to(point(start.x - px_offset, start.y - py_offset)); - builder.close(); + let mut builder = PathBuilder::fill(); + builder.move_to(point(start.x + px_offset, start.y + py_offset)); + builder.line_to(point(end.x + px_offset, end.y + py_offset)); + builder.line_to(point(end.x - px_offset, end.y - py_offset)); + builder.line_to(point(start.x - px_offset, start.y - py_offset)); + builder.close(); - if let Ok(path) = builder.build() { - window.paint_path(path, color); - } - } - } + if let Ok(path) = builder.build() { + window.paint_path(path, color); + } + } + } } impl Render for DrawingCanvas { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let lines = self.lines.clone(); - let current_line = self.current_line.clone(); - let current_color = self.current_color(&colors); - let palette = Self::get_colors(&colors); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let lines = self.lines.clone(); + let current_line = self.current_line.clone(); + let current_color = self.current_color(&colors); + let palette = Self::get_colors(&colors); - let surface = colors.container; - let border = colors.border; - let error = rgb(0xd32f2f); - let error_hover = rgb(0xe04545); - let text = colors.selected_text; - let text_muted = colors.disabled; + let surface = colors.container; + let border = colors.border; + let error = rgb(0xd32f2f); + let error_hover = rgb(0xe04545); + let text = colors.selected_text; + let text_muted = colors.disabled; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .id("drawing-area") - .h_48() - .rounded_lg() - .bg(surface) - .border_1() - .border_color(border) - .cursor_crosshair() - .overflow_hidden() - .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) - .on_mouse_move(cx.listener(Self::on_mouse_move)) - .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) - .child( - canvas( - move |_, _, _| {}, - move |_bounds, _, window, _cx| { - for (i, line) in lines.iter().enumerate() { - let color = palette[i % palette.len()]; - DrawingCanvas::draw_line(window, line, color); - } + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .id("drawing-area") + .h_48() + .rounded_lg() + .bg(surface) + .border_1() + .border_color(border) + .cursor_crosshair() + .overflow_hidden() + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .on_mouse_move(cx.listener(Self::on_mouse_move)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .child( + canvas( + move |_, _, _| {}, + move |_bounds, _, window, _cx| { + for (i, line) in lines.iter().enumerate() { + let color = palette[i % palette.len()]; + DrawingCanvas::draw_line(window, line, color); + } - if !current_line.is_empty() { - DrawingCanvas::draw_line(window, ¤t_line, current_color); - } - }, - ) - .size_full(), - ), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("clear-btn") - .px_3() - .py_1() - .rounded_md() - .bg(error) - .text_sm() - .text_color(text) - .cursor_pointer() - .hover(move |s| s.bg(error_hover)) - .child("Clear") - .on_click(cx.listener(|this, _, _, cx| { - this.clear(cx); - })), - ) - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Click and drag to draw"), - ), - ) - } + if !current_line.is_empty() { + DrawingCanvas::draw_line(window, ¤t_line, current_color); + } + }, + ) + .size_full(), + ), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("clear-btn") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_sm() + .text_color(text) + .cursor_pointer() + .hover(move |s| s.bg(error_hover)) + .child("Clear") + .on_click(cx.listener(|this, _, _, cx| { + this.clear(cx); + })), + ) + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Click and drag to draw"), + ), + ) + } } // Main Application View struct CustomDrawingExample { - drawing_canvas: gpui::Entity, + drawing_canvas: gpui::Entity, } impl CustomDrawingExample { - fn new(cx: &mut Context) -> Self { - Self { - drawing_canvas: cx.new(|_| DrawingCanvas::new()), - } - } + fn new(cx: &mut Context) -> Self { + Self { + drawing_canvas: cx.new(|_| DrawingCanvas::new()), + } + } } impl Render for CustomDrawingExample { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .id("main") - .size_full() - .p_6() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_6() - .max_w(px(500.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Custom Drawing"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Canvas element, paths, and interactive painting"), - ), - ) - .child(section( - &colors, - "1. Basic Shapes (paint_quad)", - "Use window.paint_quad() to draw filled rectangles", - basic_shapes_canvas(&colors), - px(70.), - )) - .child(section( - &colors, - "2. Custom Paths (PathBuilder)", - "Create complex shapes with PathBuilder and paint_path()", - custom_paths_canvas(&colors), - px(80.), - )) - .child(section( - &colors, - "3. Interactive Drawing", - "Combine canvas with mouse events for drawing", - self.drawing_canvas.clone(), - px(240.), - )), - ) - } + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Custom Drawing"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Canvas element, paths, and interactive painting"), + ), + ) + .child(section( + &colors, + "1. Basic Shapes (paint_quad)", + "Use window.paint_quad() to draw filled rectangles", + basic_shapes_canvas(&colors), + px(70.), + )) + .child(section( + &colors, + "2. Custom Paths (PathBuilder)", + "Create complex shapes with PathBuilder and paint_path()", + custom_paths_canvas(&colors), + px(80.), + )) + .child(section( + &colors, + "3. Interactive Drawing", + "Combine canvas with mouse events for drawing", + self.drawing_canvas.clone(), + px(240.), + )), + ) + } } fn section( - colors: &Colors, - title: &'static str, - description: &'static str, - content: impl IntoElement, - height: Pixels, + colors: &Colors, + title: &'static str, + description: &'static str, + content: impl IntoElement, + height: Pixels, ) -> impl IntoElement { - let surface: Hsla = colors.container.into(); + let surface: Hsla = colors.container.into(); - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(surface.opacity(0.5)) - .border_1() - .border_color(colors.border) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child(description), - ), - ) - .child(div().h(height).child(content)) + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(surface.opacity(0.5)) + .border_1() + .border_color(colors.border) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child(description), + ), + ) + .child(div().h(height).child(content)) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|cx| CustomDrawingExample::new(cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| CustomDrawingExample::new(cx)), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Custom Drawing"); - }); + example_prelude::init_example(cx, "Custom Drawing"); + }); } diff --git a/crates/gpui/examples/learn/dock_layout.rs b/crates/gpui/examples/learn/dock_layout.rs index 2f32aebd18..b828829dc8 100644 --- a/crates/gpui/examples/learn/dock_layout.rs +++ b/crates/gpui/examples/learn/dock_layout.rs @@ -17,12 +17,12 @@ mod example_prelude; use example_prelude::init_example; use gpui::dock::{ - DockArea, DockEvent, DockLayoutState, DockPanel, PanelEvent, PanelHandle, PanelId, - PanelRegistry, + DockArea, DockEvent, DockLayoutState, DockPanel, PanelEvent, PanelHandle, PanelId, + PanelRegistry, }; use gpui::{ - AnyElement, App, Bounds, Context, Entity, EventEmitter, Render, SharedString, Window, - WindowBounds, WindowOptions, div, prelude::*, px, size, + AnyElement, App, Bounds, Context, Entity, EventEmitter, Render, SharedString, Window, + WindowBounds, WindowOptions, div, prelude::*, px, size, }; use std::sync::Arc; @@ -40,41 +40,41 @@ const TIMELINE_ID: PanelId = PanelId::new(4); /// Shared shape of the demo placeholders: a labeled box. struct PlaceholderPanel { - id: PanelId, - title: &'static str, + id: PanelId, + title: &'static str, } impl PlaceholderPanel { - fn new(id: PanelId, title: &'static str) -> Self { - Self { id, title } - } + fn new(id: PanelId, title: &'static str) -> Self { + Self { id, title } + } } impl Render for PlaceholderPanel { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .items_center() - .justify_center() - .child(format!("{} (placeholder)", self.title)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .child(format!("{} (placeholder)", self.title)) + } } impl EventEmitter for PlaceholderPanel {} impl DockPanel for PlaceholderPanel { - fn panel_id(&self) -> PanelId { - self.id - } + fn panel_id(&self) -> PanelId { + self.id + } - fn title(&self, _cx: &App) -> SharedString { - self.title.into() - } + fn title(&self, _cx: &App) -> SharedString { + self.title.into() + } - fn tab_content(&self, _cx: &App) -> AnyElement { - div().child(self.title).into_any_element() - } + fn tab_content(&self, _cx: &App) -> AnyElement { + div().child(self.title).into_any_element() + } } // ============================================================================ @@ -84,29 +84,29 @@ impl DockPanel for PlaceholderPanel { struct DemoPanelRegistry; impl PanelRegistry for DemoPanelRegistry { - fn panel_key(&self, id: PanelId) -> Option { - match id { - PROJECT_BIN_ID => Some("project-bin".into()), - VIEWER_ID => Some("viewer".into()), - INSPECTOR_ID => Some("inspector".into()), - TIMELINE_ID => Some("timeline".into()), - _ => None, - } - } + fn panel_key(&self, id: PanelId) -> Option { + match id { + PROJECT_BIN_ID => Some("project-bin".into()), + VIEWER_ID => Some("viewer".into()), + INSPECTOR_ID => Some("inspector".into()), + TIMELINE_ID => Some("timeline".into()), + _ => None, + } + } - fn build_panel(&self, key: &str, _window: &mut Window, cx: &mut App) -> Option { - let (id, title) = match key { - "project-bin" => (PROJECT_BIN_ID, "Project Bin"), - "viewer" => (VIEWER_ID, "Viewer"), - "inspector" => (INSPECTOR_ID, "Inspector"), - "timeline" => (TIMELINE_ID, "Timeline"), - _ => return None, - }; - Some(PanelHandle::new( - cx.new(|_| PlaceholderPanel::new(id, title)), - cx, - )) - } + fn build_panel(&self, key: &str, _window: &mut Window, cx: &mut App) -> Option { + let (id, title) = match key { + "project-bin" => (PROJECT_BIN_ID, "Project Bin"), + "viewer" => (VIEWER_ID, "Viewer"), + "inspector" => (INSPECTOR_ID, "Inspector"), + "timeline" => (TIMELINE_ID, "Timeline"), + _ => return None, + }; + Some(PanelHandle::new( + cx.new(|_| PlaceholderPanel::new(id, title)), + cx, + )) + } } // ============================================================================ @@ -114,57 +114,60 @@ impl PanelRegistry for DemoPanelRegistry { // ============================================================================ struct DockLayoutExample { - dock: Entity, + dock: Entity, } impl DockLayoutExample { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let dock = cx.new(|cx| DockArea::new(cx).with_registry(Arc::new(DemoPanelRegistry))); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let dock = cx.new(|cx| DockArea::new(cx).with_registry(Arc::new(DemoPanelRegistry))); - // Seed a default workspace. Once the dock is implemented this will - // instead attempt `restore_state` from a persisted `DockLayoutState` - // first, falling back to this default when none exists. - let panels: Vec = ["project-bin", "viewer", "inspector", "timeline"] - .into_iter() - .filter_map(|key| DemoPanelRegistry.build_panel(key, window, cx)) - .collect(); - dock.update(cx, |dock, cx| { - for panel in panels { - dock.add_panel(panel, None, cx); - } - }); + // Seed a default workspace. Once the dock is implemented this will + // instead attempt `restore_state` from a persisted `DockLayoutState` + // first, falling back to this default when none exists. + let panels: Vec = ["project-bin", "viewer", "inspector", "timeline"] + .into_iter() + .filter_map(|key| DemoPanelRegistry.build_panel(key, window, cx)) + .collect(); + dock.update(cx, |dock, cx| { + for panel in panels { + dock.add_panel(panel, None, cx); + } + }); - // Autosave hook: persist on every layout change. - cx.subscribe(&dock, |_this, dock: Entity, event: &DockEvent, cx| { - if let DockEvent::LayoutChanged = event { - let _state: DockLayoutState = dock.read(cx).save_state(); - todo!("serialize `_state` with serde_json and write it to the app data dir"); - } - }) - .detach(); + // Autosave hook: persist on every layout change. + cx.subscribe( + &dock, + |_this, dock: Entity, event: &DockEvent, cx| { + if let DockEvent::LayoutChanged = event { + let _state: DockLayoutState = dock.read(cx).save_state(); + todo!("serialize `_state` with serde_json and write it to the app data dir"); + } + }, + ) + .detach(); - Self { dock } - } + Self { dock } + } } impl Render for DockLayoutExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.dock.clone()) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.dock.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(1200.), px(800.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| DockLayoutExample::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(1200.), px(800.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| DockLayoutExample::new(window, cx)), + ) + .expect("Failed to open window"); - init_example(cx, "Dock Layout"); - }); + init_example(cx, "Dock Layout"); + }); } diff --git a/crates/gpui/examples/learn/effect_stack.rs b/crates/gpui/examples/learn/effect_stack.rs index 83812ac3dd..6f78de184e 100644 --- a/crates/gpui/examples/learn/effect_stack.rs +++ b/crates/gpui/examples/learn/effect_stack.rs @@ -18,11 +18,11 @@ use std::sync::Arc; use example_prelude::init_example; use gpui::effect_stack::{ - EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, EffectStackView, + EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, EffectStackView, }; use gpui::{ - App, Bounds, Context, Entity, Render, SharedString, Window, WindowBounds, WindowOptions, div, - prelude::*, px, size, + App, Bounds, Context, Entity, Render, SharedString, Window, WindowBounds, WindowOptions, div, + prelude::*, px, size, }; // --------------------------------------------------------------------------- @@ -30,117 +30,117 @@ use gpui::{ // --------------------------------------------------------------------------- struct MockEffect { - id: EffectId, - kind: EffectCardKind, - title: String, - subtitle: Option, - enabled: bool, - expanded: bool, - badge: Option, + id: EffectId, + kind: EffectCardKind, + title: String, + subtitle: Option, + enabled: bool, + expanded: bool, + badge: Option, } impl EffectData for MockEffect { - fn id(&self) -> EffectId { - self.id - } - fn kind(&self) -> EffectCardKind { - self.kind - } - fn title(&self) -> SharedString { - self.title.clone().into() - } - fn subtitle(&self) -> Option { - self.subtitle.clone().map(Into::into) - } - fn is_enabled(&self) -> bool { - self.enabled - } - fn is_expanded(&self) -> bool { - self.expanded - } - fn badge_count(&self) -> Option { - self.badge - } + fn id(&self) -> EffectId { + self.id + } + fn kind(&self) -> EffectCardKind { + self.kind + } + fn title(&self) -> SharedString { + self.title.clone().into() + } + fn subtitle(&self) -> Option { + self.subtitle.clone().map(Into::into) + } + fn is_enabled(&self) -> bool { + self.enabled + } + fn is_expanded(&self) -> bool { + self.expanded + } + fn badge_count(&self) -> Option { + self.badge + } } /// The mock data source. In Oak this would be a view-model entity deriving /// the ordered card list from the node-graph path of the selected clip. struct MockStack { - clip_name: String, - effects: Vec, - /// Reserved for allocating ids to effects added at runtime. - #[allow(dead_code)] - next_id: u64, + clip_name: String, + effects: Vec, + /// Reserved for allocating ids to effects added at runtime. + #[allow(dead_code)] + next_id: u64, } impl MockStack { - fn demo() -> Self { - Self { - clip_name: "A001_C002_0103.mov".to_string(), - effects: vec![ - MockEffect { - id: EffectId(0), - kind: EffectCardKind::Source, - title: "Media".into(), - subtitle: Some("A001_C002_0103.mov".into()), - enabled: true, - expanded: false, - badge: None, - }, - MockEffect { - id: EffectId(1), - kind: EffectCardKind::Effect, - title: "Transform".into(), - subtitle: Some("scale 100%, rotate 0°".into()), - enabled: true, - expanded: true, - badge: Some(2), - }, - MockEffect { - id: EffectId(2), - kind: EffectCardKind::Effect, - title: "OCIO LUT".into(), - subtitle: Some("filmic_to_display.cube".into()), - enabled: true, - expanded: false, - badge: None, - }, - MockEffect { - id: EffectId(3), - kind: EffectCardKind::Output, - title: "Output".into(), - subtitle: None, - enabled: true, - expanded: false, - badge: None, - }, - ], - next_id: 4, - } - } + fn demo() -> Self { + Self { + clip_name: "A001_C002_0103.mov".to_string(), + effects: vec![ + MockEffect { + id: EffectId(0), + kind: EffectCardKind::Source, + title: "Media".into(), + subtitle: Some("A001_C002_0103.mov".into()), + enabled: true, + expanded: false, + badge: None, + }, + MockEffect { + id: EffectId(1), + kind: EffectCardKind::Effect, + title: "Transform".into(), + subtitle: Some("scale 100%, rotate 0°".into()), + enabled: true, + expanded: true, + badge: Some(2), + }, + MockEffect { + id: EffectId(2), + kind: EffectCardKind::Effect, + title: "OCIO LUT".into(), + subtitle: Some("filmic_to_display.cube".into()), + enabled: true, + expanded: false, + badge: None, + }, + MockEffect { + id: EffectId(3), + kind: EffectCardKind::Output, + title: "Output".into(), + subtitle: None, + enabled: true, + expanded: false, + badge: None, + }, + ], + next_id: 4, + } + } } impl EffectStackDataSource for MockStack { - fn effects(&self) -> Vec> { - self.effects - .iter() - .map(|effect| { - Arc::new(MockEffect { - id: effect.id, - kind: effect.kind, - title: effect.title.clone(), - subtitle: effect.subtitle.clone(), - enabled: effect.enabled, - expanded: effect.expanded, - badge: effect.badge, - }) as Arc - }) - .collect() - } + fn effects(&self) -> Vec> { + self.effects + .iter() + .map(|effect| { + Arc::new(MockEffect { + id: effect.id, + kind: effect.kind, + title: effect.title.clone(), + subtitle: effect.subtitle.clone(), + enabled: effect.enabled, + expanded: effect.expanded, + badge: effect.badge, + }) as Arc + }) + .collect() + } - fn target_label(&self) -> Option { - Some(self.clip_name.clone().into()) - } + fn target_label(&self) -> Option { + Some(self.clip_name.clone().into()) + } } // --------------------------------------------------------------------------- @@ -150,93 +150,93 @@ impl EffectStackDataSource for MockStack { /// Placeholder parameter view. A real app builds the effect's controls here /// and calls [`EffectStackView::notify_parameter_changed`] after edits. struct MockParams { - effect: EffectId, + effect: EffectId, } impl Render for MockParams { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(format!("parameters for {} (mock)", self.effect)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().child(format!("parameters for {} (mock)", self.effect)) + } } struct StackDemoRoot { - data: Entity, - stack: Entity>, + data: Entity, + stack: Entity>, } impl StackDemoRoot { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let data = cx.new(|_cx| MockStack::demo()); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let data = cx.new(|_cx| MockStack::demo()); - let stack = cx.new(|cx| { - EffectStackView::new(data.clone(), cx).params_renderer(|id, _window, cx| { - // Real apps build the effect's parameter controls here. The - // mock just shows a placeholder label. - cx.new(|_cx| MockParams { effect: *id }).into() - }) - }); + let stack = cx.new(|cx| { + EffectStackView::new(data.clone(), cx).params_renderer(|id, _window, cx| { + // Real apps build the effect's parameter controls here. The + // mock just shows a placeholder label. + cx.new(|_cx| MockParams { effect: *id }).into() + }) + }); - // The "edits are requests" loop: log each request, apply it to the - // model (Oak: engine command + undo), then notify. - cx.subscribe_in(&stack, window, { - let data = data.clone(); - move |_root, _stack, event: &EffectStackEvent, _window, cx| { - println!("[effect_stack] request: {event:?}"); - data.update(cx, |data, cx| { - match event { - EffectStackEvent::EnableToggled { effect, enabled } => { - if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) { - e.enabled = *enabled; - } - } - EffectStackEvent::ExpansionToggled { effect, expanded } => { - if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) { - e.expanded = *expanded; - } - } - EffectStackEvent::ReorderRequested { .. } - | EffectStackEvent::RemoveRequested(_) - | EffectStackEvent::AddRequested { .. } - | EffectStackEvent::ContextMenuRequested { .. } - | EffectStackEvent::ParameterChanged { .. } => { - todo!("apply {event:?} to the mock model (or engine, in a real app)") - } - } - cx.notify(); - }); - } - }) - .detach(); + // The "edits are requests" loop: log each request, apply it to the + // model (Oak: engine command + undo), then notify. + cx.subscribe_in(&stack, window, { + let data = data.clone(); + move |_root, _stack, event: &EffectStackEvent, _window, cx| { + println!("[effect_stack] request: {event:?}"); + data.update(cx, |data, cx| { + match event { + EffectStackEvent::EnableToggled { effect, enabled } => { + if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) { + e.enabled = *enabled; + } + } + EffectStackEvent::ExpansionToggled { effect, expanded } => { + if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) { + e.expanded = *expanded; + } + } + EffectStackEvent::ReorderRequested { .. } + | EffectStackEvent::RemoveRequested(_) + | EffectStackEvent::AddRequested { .. } + | EffectStackEvent::ContextMenuRequested { .. } + | EffectStackEvent::ParameterChanged { .. } => { + todo!("apply {event:?} to the mock model (or engine, in a real app)") + } + } + cx.notify(); + }); + } + }) + .detach(); - Self { data, stack } - } + Self { data, stack } + } } impl Render for StackDemoRoot { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let _ = &self.data; - let _ = cx; - div() - .size_full() - .flex() - .items_center() - .justify_center() - .child(self.stack.clone()) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let _ = &self.data; + let _ = cx; + div() + .size_full() + .flex() + .items_center() + .justify_center() + .child(self.stack.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - init_example(cx, "Effect Stack"); + gpui_platform::application().run(|cx: &mut App| { + init_example(cx, "Effect Stack"); - let bounds = Bounds::centered(None, size(px(420.0), px(640.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| StackDemoRoot::new(window, cx)), - ) - .unwrap(); - }); + let bounds = Bounds::centered(None, size(px(420.0), px(640.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| StackDemoRoot::new(window, cx)), + ) + .unwrap(); + }); } diff --git a/crates/gpui/examples/learn/haptic_feedback.rs b/crates/gpui/examples/learn/haptic_feedback.rs index a29a656b4b..e6a0db8dcb 100644 --- a/crates/gpui/examples/learn/haptic_feedback.rs +++ b/crates/gpui/examples/learn/haptic_feedback.rs @@ -14,10 +14,10 @@ mod example_prelude; use example_prelude::init_example; use gpui::{ - App, Bounds, Context, DragMoveEvent, FontWeight, HapticFeedbackStyle, Hsla, InteractiveElement, - IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, - StatefulInteractiveElement, Styled, Window, WindowBounds, WindowOptions, colors::Colors, div, - prelude::*, px, relative, rgb, size, + App, Bounds, Context, DragMoveEvent, FontWeight, HapticFeedbackStyle, Hsla, InteractiveElement, + IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, + StatefulInteractiveElement, Styled, Window, WindowBounds, WindowOptions, colors::Colors, div, + prelude::*, px, relative, rgb, size, }; const SLIDER_MIN: f32 = 0.0; @@ -29,304 +29,304 @@ const SLIDER_STEP_COUNT: usize = ((SLIDER_MAX - SLIDER_MIN) / SLIDER_STEP) as us struct SliderDrag; impl Render for SliderDrag { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - gpui::Empty - } + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + gpui::Empty + } } struct HapticFeedbackExample { - supported: bool, - slider_value: f32, - slider_prev_step: i32, - slider_bounds: Option>, + supported: bool, + slider_value: f32, + slider_prev_step: i32, + slider_bounds: Option>, } impl HapticFeedbackExample { - fn new(cx: &mut App) -> Self { - Self { - supported: cx.supports_haptic_feedback(), - slider_value: 0.0, - slider_prev_step: 0, - slider_bounds: None, - } - } + fn new(cx: &mut App) -> Self { + Self { + supported: cx.supports_haptic_feedback(), + slider_value: 0.0, + slider_prev_step: 0, + slider_bounds: None, + } + } - fn haptic_button( - &self, - id: &'static str, - label: &'static str, - style: HapticFeedbackStyle, - color: Hsla, - colors: &Colors, - cx: &mut Context, - ) -> impl IntoElement { - let _ = cx; + fn haptic_button( + &self, + id: &'static str, + label: &'static str, + style: HapticFeedbackStyle, + color: Hsla, + colors: &Colors, + cx: &mut Context, + ) -> impl IntoElement { + let _ = cx; - div() - .id(id) - .flex() - .flex_col() - .w_48() - .items_center() - .justify_center() - .px_4() - .py_3() - .rounded_md() - .bg(color) - .cursor_pointer() - .hover(move |s| s.bg(color.opacity(0.8))) - .active(move |s| s.bg(color.opacity(0.6))) - .child( - div() - .text_base() - .font_weight(FontWeight::MEDIUM) - .text_color(Hsla::from(colors.selected_text)) - .child(label), - ) - .on_hover(move |hovered, _, cx| { - if *hovered { - cx.play_haptic_feedback(style); - } - }) - } + div() + .id(id) + .flex() + .flex_col() + .w_48() + .items_center() + .justify_center() + .px_4() + .py_3() + .rounded_md() + .bg(color) + .cursor_pointer() + .hover(move |s| s.bg(color.opacity(0.8))) + .active(move |s| s.bg(color.opacity(0.6))) + .child( + div() + .text_base() + .font_weight(FontWeight::MEDIUM) + .text_color(Hsla::from(colors.selected_text)) + .child(label), + ) + .on_hover(move |hovered, _, cx| { + if *hovered { + cx.play_haptic_feedback(style); + } + }) + } - fn value_to_step(value: f32) -> i32 { - ((value - SLIDER_MIN) / SLIDER_STEP).round() as i32 - } + fn value_to_step(value: f32) -> i32 { + ((value - SLIDER_MIN) / SLIDER_STEP).round() as i32 + } - fn snap_to_step(raw: f32) -> f32 { - let snapped = (raw / SLIDER_STEP).round() * SLIDER_STEP; - snapped.clamp(SLIDER_MIN, SLIDER_MAX) - } + fn snap_to_step(raw: f32) -> f32 { + let snapped = (raw / SLIDER_STEP).round() * SLIDER_STEP; + snapped.clamp(SLIDER_MIN, SLIDER_MAX) + } - fn value_to_percentage(value: f32) -> f32 { - ((value - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN)).clamp(0.0, 1.0) - } + fn value_to_percentage(value: f32) -> f32 { + ((value - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN)).clamp(0.0, 1.0) + } - fn update_slider_from_position( - &mut self, - position_x: Pixels, - bounds: Bounds, - cx: &mut Context, - ) { - let inner_x = (position_x - bounds.left()).clamp(px(0.), bounds.size.width); - let percentage = inner_x / bounds.size.width; - let raw = SLIDER_MIN + (SLIDER_MAX - SLIDER_MIN) * percentage; - let new_value = Self::snap_to_step(raw); - let new_step = Self::value_to_step(new_value); + fn update_slider_from_position( + &mut self, + position_x: Pixels, + bounds: Bounds, + cx: &mut Context, + ) { + let inner_x = (position_x - bounds.left()).clamp(px(0.), bounds.size.width); + let percentage = inner_x / bounds.size.width; + let raw = SLIDER_MIN + (SLIDER_MAX - SLIDER_MIN) * percentage; + let new_value = Self::snap_to_step(raw); + let new_step = Self::value_to_step(new_value); - self.slider_value = new_value; - self.slider_bounds = Some(bounds); + self.slider_value = new_value; + self.slider_bounds = Some(bounds); - if new_step != self.slider_prev_step { - cx.play_haptic_feedback(HapticFeedbackStyle::LevelChange); - self.slider_prev_step = new_step; - } + if new_step != self.slider_prev_step { + cx.play_haptic_feedback(HapticFeedbackStyle::LevelChange); + self.slider_prev_step = new_step; + } - cx.notify(); - } + cx.notify(); + } } impl Render for HapticFeedbackExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let slider_percentage = Self::value_to_percentage(self.slider_value); - let slider_color = Hsla::from(rgb(0x3b82f6)); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let slider_percentage = Self::value_to_percentage(self.slider_value); + let slider_color = Hsla::from(rgb(0x3b82f6)); - div() - .flex() - .flex_col() - .size_full() - .bg(colors.background) - .p_8() - .gap_6() - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_2xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Haptic Feedback"), - ) - .child( - div().text_sm().text_color(colors.disabled).child( - "macOS Force Touch trackpad haptics via NSHapticFeedbackManager", - ), - ), - ) - .child( - div() - .flex() - .items_center() - .gap_2() - .px_4() - .py_2() - .rounded_md() - .bg(if self.supported { - Hsla::from(rgb(0x22c55e)).opacity(0.1) - } else { - Hsla::from(rgb(0xf59e0b)).opacity(0.1) - }) - .child({ - let dot = if self.supported { - rgb(0x22c55e) - } else { - rgb(0xf59e0b) - }; - div().size_2().rounded_full().bg(dot) - }) - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .text_color(colors.text) - .when_else( - self.supported, - |this| this.child("Haptics is supported on this machine"), - |this| this.child("Haptics is not supported on this machine"), - ), - ), - ) - .child( - div() - .child("Hover over each button to trigger haptic feedback") - .text_sm() - .text_color(colors.text), - ) - .child( - div() - .flex() - .gap_3() - .child(self.haptic_button( - "btn-generic", - "Generic", - HapticFeedbackStyle::Generic, - Hsla::from(rgb(0x3b82f6)), - &colors, - cx, - )) - .child(self.haptic_button( - "btn-alignment", - "Alignment", - HapticFeedbackStyle::Alignment, - Hsla::from(rgb(0x10b981)), - &colors, - cx, - )) - .child(self.haptic_button( - "btn-levelchange", - "LevelChange", - HapticFeedbackStyle::LevelChange, - Hsla::from(rgb(0x8b5cf6)), - &colors, - cx, - )), - ) - .child( - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_lg() - .font_weight(FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Slider with LevelChange haptic"), - ) - .child( - div().flex().items_center().gap_2().child( - div() - .font_weight(FontWeight::MEDIUM) - .text_color(colors.selected_text) - .child(format!("Value: {:.0}", self.slider_value)), - ), - ) - .child( - div() - .id("slider-container") - .w_full() - .h_4() - .flex() - .items_center() - .px_2() - .cursor_pointer() - .on_drag(SliderDrag, |_, _, _, cx| cx.new(|_| SliderDrag)) - .on_drag_move(cx.listener( - |this, e: &DragMoveEvent, _, cx| { - this.update_slider_from_position( - e.event.position.x, - e.bounds, - cx, - ); - }, - )) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, e: &MouseDownEvent, _, cx| { - if let Some(bounds) = this.slider_bounds { - this.update_slider_from_position(e.position.x, bounds, cx); - } - }), - ) - .child( - div() - .w_full() - .h_1p5() - .rounded_full() - .bg(Hsla::from(colors.text).opacity(0.12)) - .flex() - .items_center() - .justify_between() - .children((0..(SLIDER_STEP_COUNT + 1)).map(|_| { - div() - .size_1p5() - .rounded_full() - .bg(colors.disabled) - .into_any() - })) - .child( - div() - .absolute() - .h_full() - .left(px(0.)) - .right(relative(1.0 - slider_percentage)) - .bg(slider_color.opacity(0.7)) - .rounded_full(), - ) - .child( - div() - .absolute() - .top(px(-6.)) - .left(relative(slider_percentage)) - .ml(px(-8.)) - .size_4() - .rounded_full() - .bg(slider_color) - .shadow_md(), - ), - ), - ), - ) - } + div() + .flex() + .flex_col() + .size_full() + .bg(colors.background) + .p_8() + .gap_6() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_2xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Haptic Feedback"), + ) + .child( + div().text_sm().text_color(colors.disabled).child( + "macOS Force Touch trackpad haptics via NSHapticFeedbackManager", + ), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .px_4() + .py_2() + .rounded_md() + .bg(if self.supported { + Hsla::from(rgb(0x22c55e)).opacity(0.1) + } else { + Hsla::from(rgb(0xf59e0b)).opacity(0.1) + }) + .child({ + let dot = if self.supported { + rgb(0x22c55e) + } else { + rgb(0xf59e0b) + }; + div().size_2().rounded_full().bg(dot) + }) + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(colors.text) + .when_else( + self.supported, + |this| this.child("Haptics is supported on this machine"), + |this| this.child("Haptics is not supported on this machine"), + ), + ), + ) + .child( + div() + .child("Hover over each button to trigger haptic feedback") + .text_sm() + .text_color(colors.text), + ) + .child( + div() + .flex() + .gap_3() + .child(self.haptic_button( + "btn-generic", + "Generic", + HapticFeedbackStyle::Generic, + Hsla::from(rgb(0x3b82f6)), + &colors, + cx, + )) + .child(self.haptic_button( + "btn-alignment", + "Alignment", + HapticFeedbackStyle::Alignment, + Hsla::from(rgb(0x10b981)), + &colors, + cx, + )) + .child(self.haptic_button( + "btn-levelchange", + "LevelChange", + HapticFeedbackStyle::LevelChange, + Hsla::from(rgb(0x8b5cf6)), + &colors, + cx, + )), + ) + .child( + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Slider with LevelChange haptic"), + ) + .child( + div().flex().items_center().gap_2().child( + div() + .font_weight(FontWeight::MEDIUM) + .text_color(colors.selected_text) + .child(format!("Value: {:.0}", self.slider_value)), + ), + ) + .child( + div() + .id("slider-container") + .w_full() + .h_4() + .flex() + .items_center() + .px_2() + .cursor_pointer() + .on_drag(SliderDrag, |_, _, _, cx| cx.new(|_| SliderDrag)) + .on_drag_move(cx.listener( + |this, e: &DragMoveEvent, _, cx| { + this.update_slider_from_position( + e.event.position.x, + e.bounds, + cx, + ); + }, + )) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, e: &MouseDownEvent, _, cx| { + if let Some(bounds) = this.slider_bounds { + this.update_slider_from_position(e.position.x, bounds, cx); + } + }), + ) + .child( + div() + .w_full() + .h_1p5() + .rounded_full() + .bg(Hsla::from(colors.text).opacity(0.12)) + .flex() + .items_center() + .justify_between() + .children((0..(SLIDER_STEP_COUNT + 1)).map(|_| { + div() + .size_1p5() + .rounded_full() + .bg(colors.disabled) + .into_any() + })) + .child( + div() + .absolute() + .h_full() + .left(px(0.)) + .right(relative(1.0 - slider_percentage)) + .bg(slider_color.opacity(0.7)) + .rounded_full(), + ) + .child( + div() + .absolute() + .top(px(-6.)) + .left(relative(slider_percentage)) + .ml(px(-8.)) + .size_4() + .rounded_full() + .bg(slider_color) + .shadow_md(), + ), + ), + ), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(520.), px(520.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|cx| HapticFeedbackExample::new(cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(520.), px(520.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| HapticFeedbackExample::new(cx)), + ) + .expect("Failed to open window"); - init_example(cx, "Haptic Feedback"); - }); + init_example(cx, "Haptic Feedback"); + }); } diff --git a/crates/gpui/examples/learn/interactive_elements.rs b/crates/gpui/examples/learn/interactive_elements.rs index 92b112e50a..810a5ff4a2 100644 --- a/crates/gpui/examples/learn/interactive_elements.rs +++ b/crates/gpui/examples/learn/interactive_elements.rs @@ -13,8 +13,8 @@ mod example_prelude; use example_prelude::init_example; use gpui::colors::Colors; use gpui::{ - App, Bounds, ClickEvent, Context, Entity, Half, Hsla, IntoElement, MouseButton, MouseMoveEvent, - Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, + App, Bounds, ClickEvent, Context, Entity, Half, Hsla, IntoElement, MouseButton, MouseMoveEvent, + Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, }; // ============================================================================ @@ -27,88 +27,88 @@ use gpui::{ // - Click count tracking struct ClickDemo { - click_count: usize, - last_click_type: String, + click_count: usize, + last_click_type: String, } impl ClickDemo { - fn new() -> Self { - Self { - click_count: 0, - last_click_type: "None".to_string(), - } - } + fn new() -> Self { + Self { + click_count: 0, + last_click_type: "None".to_string(), + } + } } impl Render for ClickDemo { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Click Events"), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("Single click, double click, or triple click the button"), - ) - .child( - div() - .id("click-target") - .px_4() - .py_2() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .text_sm() - .cursor_pointer() - .hover(|style| style.bg(colors.selected)) - .active(|style| style.bg(colors.selected)) - .child("Click Me!") - // on_click receives a ClickEvent with click_count() method - .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { - this.click_count += 1; - this.last_click_type = match event.click_count() { - 1 => "Single Click".to_string(), - 2 => "Double Click".to_string(), - 3 => "Triple Click".to_string(), - n => format!("{n}x Click"), - }; - cx.notify(); - })), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .mt_2() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child(format!("Total clicks: {}", self.click_count)), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child(format!("Last: {}", self.last_click_type)), - ), - ) - } + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Click Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("Single click, double click, or triple click the button"), + ) + .child( + div() + .id("click-target") + .px_4() + .py_2() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .text_sm() + .cursor_pointer() + .hover(|style| style.bg(colors.selected)) + .active(|style| style.bg(colors.selected)) + .child("Click Me!") + // on_click receives a ClickEvent with click_count() method + .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { + this.click_count += 1; + this.last_click_type = match event.click_count() { + 1 => "Single Click".to_string(), + 2 => "Double Click".to_string(), + 3 => "Triple Click".to_string(), + n => format!("{n}x Click"), + }; + cx.notify(); + })), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .mt_2() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child(format!("Total clicks: {}", self.click_count)), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child(format!("Last: {}", self.last_click_type)), + ), + ) + } } // ============================================================================ @@ -120,90 +120,90 @@ impl Render for ClickDemo { // - on_hover() callback for programmatic hover detection struct HoverDemo { - is_hovered: bool, - hover_count: usize, + is_hovered: bool, + hover_count: usize, } impl HoverDemo { - fn new() -> Self { - Self { - is_hovered: false, - hover_count: 0, - } - } + fn new() -> Self { + Self { + is_hovered: false, + hover_count: 0, + } + } } impl Render for HoverDemo { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let is_hovered = self.is_hovered; + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let is_hovered = self.is_hovered; - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Hover Events"), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("Move your mouse in and out of the target"), - ) - .child( - div() - .id("hover-target") - .px_4() - .py_3() - .rounded_md() - .border_2() - .border_color(if is_hovered { - colors.selected - } else { - colors.border - }) - .bg(if is_hovered { - colors.selected - } else { - colors.selected - }) - .text_color(if is_hovered { - colors.selected_text - } else { - colors.text - }) - .text_sm() - .cursor_pointer() - .child(if is_hovered { - "Mouse Inside!" - } else { - "Hover Over Me" - }) - // on_hover callback receives a bool: true when mouse enters, false when it leaves - .on_hover(cx.listener(|this, &hovered, _window, cx| { - this.is_hovered = hovered; - if hovered { - this.hover_count += 1; - } - cx.notify(); - })), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .mt_2() - .child(format!("Times hovered: {}", self.hover_count)), - ) - } + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Hover Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("Move your mouse in and out of the target"), + ) + .child( + div() + .id("hover-target") + .px_4() + .py_3() + .rounded_md() + .border_2() + .border_color(if is_hovered { + colors.selected + } else { + colors.border + }) + .bg(if is_hovered { + colors.selected + } else { + colors.selected + }) + .text_color(if is_hovered { + colors.selected_text + } else { + colors.text + }) + .text_sm() + .cursor_pointer() + .child(if is_hovered { + "Mouse Inside!" + } else { + "Hover Over Me" + }) + // on_hover callback receives a bool: true when mouse enters, false when it leaves + .on_hover(cx.listener(|this, &hovered, _window, cx| { + this.is_hovered = hovered; + if hovered { + this.hover_count += 1; + } + cx.notify(); + })), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .mt_2() + .child(format!("Times hovered: {}", self.hover_count)), + ) + } } // ============================================================================ @@ -216,115 +216,115 @@ impl Render for HoverDemo { // - on_mouse_move - fires when mouse moves over element struct MouseEventsDemo { - mouse_position: Option>, - is_pressed: bool, - event_log: Vec, + mouse_position: Option>, + is_pressed: bool, + event_log: Vec, } impl MouseEventsDemo { - fn new() -> Self { - Self { - mouse_position: None, - is_pressed: false, - event_log: Vec::new(), - } - } + fn new() -> Self { + Self { + mouse_position: None, + is_pressed: false, + event_log: Vec::new(), + } + } - fn log_event(&mut self, event: &str) { - self.event_log.push(event.to_string()); - if self.event_log.len() > 5 { - self.event_log.remove(0); - } - } + fn log_event(&mut self, event: &str) { + self.event_log.push(event.to_string()); + if self.event_log.len() > 5 { + self.event_log.remove(0); + } + } } impl Render for MouseEventsDemo { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let is_pressed = self.is_pressed; - let position_text = self - .mouse_position - .map(|p| format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y))) - .unwrap_or_else(|| "—".to_string()); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let is_pressed = self.is_pressed; + let position_text = self + .mouse_position + .map(|p| format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y))) + .unwrap_or_else(|| "—".to_string()); - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Mouse Events"), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("Move and click within the target area"), - ) - .child( - div() - .id("mouse-events-target") - .h_20() - .rounded_md() - .border_2() - .border_color(if is_pressed { - colors.selected - } else { - colors.border - }) - .bg(if is_pressed { - colors.selected - } else { - colors.selected - }) - .flex() - .items_center() - .justify_center() - .text_sm() - .text_color(colors.text) - .child(format!("Position: {}", position_text)) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _event, _window, cx| { - this.is_pressed = true; - this.log_event("Mouse Down"); - cx.notify(); - }), - ) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _event, _window, cx| { - this.is_pressed = false; - this.log_event("Mouse Up"); - cx.notify(); - }), - ) - .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { - this.mouse_position = Some(event.position); - cx.notify(); - })), - ) - .child( - div() - .flex() - .flex_col() - .gap_0p5() - .mt_2() - .text_xs() - .text_color(colors.disabled) - .children( - self.event_log - .iter() - .map(|e| div().child(format!("• {}", e))), - ), - ) - } + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Mouse Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("Move and click within the target area"), + ) + .child( + div() + .id("mouse-events-target") + .h_20() + .rounded_md() + .border_2() + .border_color(if is_pressed { + colors.selected + } else { + colors.border + }) + .bg(if is_pressed { + colors.selected + } else { + colors.selected + }) + .flex() + .items_center() + .justify_center() + .text_sm() + .text_color(colors.text) + .child(format!("Position: {}", position_text)) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + this.is_pressed = true; + this.log_event("Mouse Down"); + cx.notify(); + }), + ) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + this.is_pressed = false; + this.log_event("Mouse Up"); + cx.notify(); + }), + ) + .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { + this.mouse_position = Some(event.position); + cx.notify(); + })), + ) + .child( + div() + .flex() + .flex_col() + .gap_0p5() + .mt_2() + .text_xs() + .text_color(colors.disabled) + .children( + self.event_log + .iter() + .map(|e| div().child(format!("• {}", e))), + ), + ) + } } // ============================================================================ @@ -337,146 +337,146 @@ impl Render for MouseEventsDemo { #[derive(Clone, Copy)] struct DragData { - index: usize, - color: Hsla, - position: Point, + index: usize, + color: Hsla, + position: Point, } impl DragData { - fn new(index: usize, color: Hsla) -> Self { - Self { - index, - color, - position: Point::default(), - } - } + fn new(index: usize, color: Hsla) -> Self { + Self { + index, + color, + position: Point::default(), + } + } - fn with_position(mut self, position: Point) -> Self { - self.position = position; - self - } + fn with_position(mut self, position: Point) -> Self { + self.position = position; + self + } } // Render trait for DragData allows it to be rendered as drag feedback impl Render for DragData { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let size = gpui::size(px(80.), px(40.)); + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let size = gpui::size(px(80.), px(40.)); - // Position the drag preview at the cursor - 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.8)) - .text_color(gpui::white()) - .text_xs() - .rounded_md() - .shadow_lg() - .child(format!("Item {}", self.index + 1)), - ) - } + // Position the drag preview at the cursor + 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.8)) + .text_color(gpui::white()) + .text_xs() + .rounded_md() + .shadow_lg() + .child(format!("Item {}", self.index + 1)), + ) + } } struct DragDropDemo { - dropped_item: Option, + dropped_item: Option, } impl DragDropDemo { - fn new() -> Self { - Self { dropped_item: None } - } + fn new() -> Self { + Self { dropped_item: None } + } } impl Render for DragDropDemo { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); - let item_colors = [rgb(0xd32f2f), rgb(0x388e3c), rgb(0xf9a825)]; + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let item_colors = [rgb(0xd32f2f), rgb(0x388e3c), rgb(0xf9a825)]; - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .rounded_lg() - .bg(colors.container) - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child("Drag and Drop"), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("Drag items to the drop zone below"), - ) - .child( - div() - .flex() - .gap_2() - .children(item_colors.into_iter().enumerate().map(|(index, color)| { - let drag_data = DragData::new(index, color.into()); + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.container) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Drag and Drop"), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("Drag items to the drop zone below"), + ) + .child( + div() + .flex() + .gap_2() + .children(item_colors.into_iter().enumerate().map(|(index, color)| { + let drag_data = DragData::new(index, color.into()); - div() - .id(("drag-item", index)) - .px_3() - .py_2() - .rounded_md() - .border_2() - .border_color(color) - .text_color(color) - .text_xs() - .cursor_grab() - .hover(move |style| { - let c: Hsla = color.into(); - style.bg(c.opacity(0.1)) - }) - .child(format!("Item {}", index + 1)) - // on_drag takes: drag data, and a closure that creates the drag preview - .on_drag(drag_data, |data: &DragData, position, _, cx| { - cx.new(|_| data.with_position(position)) - }) - })), - ) - .child( - div() - .id("drop-target") - .mt_2() - .h_16() - .rounded_md() - .border_2() - .border_dashed() - .border_color( - self.dropped_item - .map(|d| d.color) - .unwrap_or_else(|| colors.border.into()), - ) - .when_some(self.dropped_item, |el, data| el.bg(data.color.opacity(0.2))) - .flex() - .items_center() - .justify_center() - .text_xs() - .text_color(colors.disabled) - // on_drop receives the drag data when an item is dropped - .on_drop(cx.listener(|this, data: &DragData, _window, cx| { - this.dropped_item = Some(*data); - cx.notify(); - })) - .child( - self.dropped_item - .map(|d| format!("Dropped: Item {}", d.index + 1)) - .unwrap_or_else(|| "Drop Zone".to_string()), - ), - ) - } + div() + .id(("drag-item", index)) + .px_3() + .py_2() + .rounded_md() + .border_2() + .border_color(color) + .text_color(color) + .text_xs() + .cursor_grab() + .hover(move |style| { + let c: Hsla = color.into(); + style.bg(c.opacity(0.1)) + }) + .child(format!("Item {}", index + 1)) + // on_drag takes: drag data, and a closure that creates the drag preview + .on_drag(drag_data, |data: &DragData, position, _, cx| { + cx.new(|_| data.with_position(position)) + }) + })), + ) + .child( + div() + .id("drop-target") + .mt_2() + .h_16() + .rounded_md() + .border_2() + .border_dashed() + .border_color( + self.dropped_item + .map(|d| d.color) + .unwrap_or_else(|| colors.border.into()), + ) + .when_some(self.dropped_item, |el, data| el.bg(data.color.opacity(0.2))) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(colors.disabled) + // on_drop receives the drag data when an item is dropped + .on_drop(cx.listener(|this, data: &DragData, _window, cx| { + this.dropped_item = Some(*data); + cx.notify(); + })) + .child( + self.dropped_item + .map(|d| format!("Dropped: Item {}", d.index + 1)) + .unwrap_or_else(|| "Drop Zone".to_string()), + ), + ) + } } // ============================================================================ @@ -484,84 +484,84 @@ impl Render for DragDropDemo { // ============================================================================ struct InteractiveElementsExample { - click_demo: Entity, - hover_demo: Entity, - mouse_events_demo: Entity, - drag_drop_demo: Entity, + click_demo: Entity, + hover_demo: Entity, + mouse_events_demo: Entity, + drag_drop_demo: Entity, } impl InteractiveElementsExample { - fn new(cx: &mut Context) -> Self { - Self { - click_demo: cx.new(|_| ClickDemo::new()), - hover_demo: cx.new(|_| HoverDemo::new()), - mouse_events_demo: cx.new(|_| MouseEventsDemo::new()), - drag_drop_demo: cx.new(|_| DragDropDemo::new()), - } - } + fn new(cx: &mut Context) -> Self { + Self { + click_demo: cx.new(|_| ClickDemo::new()), + hover_demo: cx.new(|_| HoverDemo::new()), + mouse_events_demo: cx.new(|_| MouseEventsDemo::new()), + drag_drop_demo: cx.new(|_| DragDropDemo::new()), + } + } } impl Render for InteractiveElementsExample { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .id("main") - .size_full() - .p_6() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_6() - .max_w(px(800.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Interactive Elements"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Click, hover, mouse events, and drag-and-drop in GPUI"), - ), - ) - .child( - div() - .grid() - .grid_cols(2) - .gap_4() - .child(self.click_demo.clone()) - .child(self.hover_demo.clone()) - .child(self.mouse_events_demo.clone()) - .child(self.drag_drop_demo.clone()), - ), - ) - } + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(800.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Interactive Elements"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Click, hover, mouse events, and drag-and-drop in GPUI"), + ), + ) + .child( + div() + .grid() + .grid_cols(2) + .gap_4() + .child(self.click_demo.clone()) + .child(self.hover_demo.clone()) + .child(self.mouse_events_demo.clone()) + .child(self.drag_drop_demo.clone()), + ), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|cx| InteractiveElementsExample::new(cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| InteractiveElementsExample::new(cx)), + ) + .expect("Failed to open window"); - init_example(cx, "Interactive Elements"); - }); + init_example(cx, "Interactive Elements"); + }); } diff --git a/crates/gpui/examples/learn/keyring.rs b/crates/gpui/examples/learn/keyring.rs index b2121a41b4..fa91c84cf9 100644 --- a/crates/gpui/examples/learn/keyring.rs +++ b/crates/gpui/examples/learn/keyring.rs @@ -15,7 +15,7 @@ mod example_prelude; use gpui::colors::Colors; use gpui::{ - App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, size, + App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, size, }; /// The URL the credentials are keyed by. The keyring label (Linux/FreeBSD) is a @@ -23,154 +23,154 @@ use gpui::{ const CREDENTIAL_URL: &str = "https://example.com/keyring-demo"; struct KeyringExample { - status: String, + status: String, } impl KeyringExample { - fn new() -> Self { - Self { - status: "Use the buttons to store, load, and delete credentials.".into(), - } - } + fn new() -> Self { + Self { + status: "Use the buttons to store, load, and delete credentials.".into(), + } + } - fn set_status(&mut self, status: impl Into, cx: &mut Context) { - self.status = status.into(); - cx.notify(); - } + fn set_status(&mut self, status: impl Into, cx: &mut Context) { + self.status = status.into(); + cx.notify(); + } - fn save(&mut self, cx: &mut Context) { - self.set_status("Saving...", cx); - cx.spawn(async move |this, cx| { - let task = - cx.update(|cx| cx.write_credentials(CREDENTIAL_URL, "ada@example.com", b"hunter2")); - let result = task.await; - this.update(cx, |this, cx| match result { - Ok(()) => this.set_status("Saved credentials for ada@example.com.", cx), - Err(err) => this.set_status(format!("Failed to save: {err}"), cx), - }) - }) - .detach(); - } + fn save(&mut self, cx: &mut Context) { + self.set_status("Saving...", cx); + cx.spawn(async move |this, cx| { + let task = + cx.update(|cx| cx.write_credentials(CREDENTIAL_URL, "ada@example.com", b"hunter2")); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(()) => this.set_status("Saved credentials for ada@example.com.", cx), + Err(err) => this.set_status(format!("Failed to save: {err}"), cx), + }) + }) + .detach(); + } - fn load(&mut self, cx: &mut Context) { - self.set_status("Loading...", cx); - cx.spawn(async move |this, cx| { - let task = cx.update(|cx| cx.read_credentials(CREDENTIAL_URL)); - let result = task.await; - this.update(cx, |this, cx| match result { - Ok(Some((username, password))) => this.set_status( - format!("Loaded {username} (password is {} bytes).", password.len()), - cx, - ), - Ok(None) => this.set_status("No credentials stored yet.", cx), - Err(err) => this.set_status(format!("Failed to load: {err}"), cx), - }) - }) - .detach(); - } + fn load(&mut self, cx: &mut Context) { + self.set_status("Loading...", cx); + cx.spawn(async move |this, cx| { + let task = cx.update(|cx| cx.read_credentials(CREDENTIAL_URL)); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(Some((username, password))) => this.set_status( + format!("Loaded {username} (password is {} bytes).", password.len()), + cx, + ), + Ok(None) => this.set_status("No credentials stored yet.", cx), + Err(err) => this.set_status(format!("Failed to load: {err}"), cx), + }) + }) + .detach(); + } - fn delete(&mut self, cx: &mut Context) { - self.set_status("Deleting...", cx); - cx.spawn(async move |this, cx| { - let task = cx.update(|cx| cx.delete_credentials(CREDENTIAL_URL)); - let result = task.await; - this.update(cx, |this, cx| match result { - Ok(()) => this.set_status("Deleted stored credentials.", cx), - Err(err) => this.set_status(format!("Failed to delete: {err}"), cx), - }) - }) - .detach(); - } + fn delete(&mut self, cx: &mut Context) { + self.set_status("Deleting...", cx); + cx.spawn(async move |this, cx| { + let task = cx.update(|cx| cx.delete_credentials(CREDENTIAL_URL)); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(()) => this.set_status("Deleted stored credentials.", cx), + Err(err) => this.set_status(format!("Failed to delete: {err}"), cx), + }) + }) + .detach(); + } } impl Render for KeyringExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div().size_full().p_6().bg(colors.background).child( - div() - .flex() - .flex_col() - .gap_4() - .max_w(px(460.)) - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Keyring Credentials"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child(format!("Stored under: {CREDENTIAL_URL}")), - ) - .child( - div() - .p_4() - .rounded_lg() - .bg(colors.container) - .border_1() - .border_color(colors.border) - .text_sm() - .text_color(colors.text) - .child(self.status.clone()), - ) - .child( - div() - .flex() - .gap_2() - .child( - button(&colors, "save", "Save") - .on_click(cx.listener(|this, _, _, cx| this.save(cx))), - ) - .child( - button(&colors, "load", "Load") - .on_click(cx.listener(|this, _, _, cx| this.load(cx))), - ) - .child( - button(&colors, "delete", "Delete") - .on_click(cx.listener(|this, _, _, cx| this.delete(cx))), - ), - ), - ) - } + div().size_full().p_6().bg(colors.background).child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(460.)) + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Keyring Credentials"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child(format!("Stored under: {CREDENTIAL_URL}")), + ) + .child( + div() + .p_4() + .rounded_lg() + .bg(colors.container) + .border_1() + .border_color(colors.border) + .text_sm() + .text_color(colors.text) + .child(self.status.clone()), + ) + .child( + div() + .flex() + .gap_2() + .child( + button(&colors, "save", "Save") + .on_click(cx.listener(|this, _, _, cx| this.save(cx))), + ) + .child( + button(&colors, "load", "Load") + .on_click(cx.listener(|this, _, _, cx| this.load(cx))), + ) + .child( + button(&colors, "delete", "Delete") + .on_click(cx.listener(|this, _, _, cx| this.delete(cx))), + ), + ), + ) + } } fn button( - colors: &Colors, - id: impl Into, - label: &'static str, + colors: &Colors, + id: impl Into, + label: &'static str, ) -> gpui::Stateful { - let bg_hover = colors.border; - div() - .id(id) - .px_3() - .py_1p5() - .rounded_md() - .text_sm() - .text_color(colors.selected_text) - .bg(colors.selected) - .cursor_pointer() - .hover(move |style| style.bg(bg_hover)) - .child(label) + let bg_hover = colors.border; + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(colors.selected_text) + .bg(colors.selected) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .child(label) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.set_keyring_label("gpui-ce-keyring-example"); + gpui_platform::application().run(|cx: &mut App| { + cx.set_keyring_label("gpui-ce-keyring-example"); - let bounds = Bounds::centered(None, size(px(500.), px(360.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| KeyringExample::new()), - ) - .expect("Failed to open window"); + let bounds = Bounds::centered(None, size(px(500.), px(360.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| KeyringExample::new()), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Keyring"); - }); + example_prelude::init_example(cx, "Keyring"); + }); } diff --git a/crates/gpui/examples/learn/layout.rs b/crates/gpui/examples/learn/layout.rs index abadb9b67d..da1526d63e 100644 --- a/crates/gpui/examples/learn/layout.rs +++ b/crates/gpui/examples/learn/layout.rs @@ -12,379 +12,379 @@ mod example_prelude; use example_prelude::init_example; use gpui::colors::Colors; use gpui::{ - Bounds, Context, Div, Hsla, Render, Rgba, Window, WindowBounds, WindowOptions, div, prelude::*, - px, size, + Bounds, Context, Div, Hsla, Render, Rgba, Window, WindowBounds, WindowOptions, div, prelude::*, + px, size, }; // Helper: Colored block for visualization fn block(label: &'static str, color: Hsla, text_color: Rgba) -> Div { - div() - .flex() - .items_center() - .justify_center() - .bg(color) - .border_1() - .border_color(gpui::white().opacity(0.3)) - .rounded_md() - .text_xs() - .text_color(text_color) - .child(label) + div() + .flex() + .items_center() + .justify_center() + .bg(color) + .border_1() + .border_color(gpui::white().opacity(0.3)) + .rounded_md() + .text_xs() + .text_color(text_color) + .child(label) } // Flexbox Examples fn flexbox_row_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; + let text_muted = colors.disabled; + let text = colors.selected_text; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("flex().flex_row().gap_2()"), - ) - .child( - div() - .flex() - .flex_row() - .gap_2() - .child(block("A", gpui::red(), text).size_8()) - .child(block("B", gpui::green(), text).size_8()) - .child(block("C", gpui::blue(), text).size_8()), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex().flex_row().gap_2()"), + ) + .child( + div() + .flex() + .flex_row() + .gap_2() + .child(block("A", gpui::red(), text).size_8()) + .child(block("B", gpui::green(), text).size_8()) + .child(block("C", gpui::blue(), text).size_8()), + ) } fn flexbox_column_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; + let text_muted = colors.disabled; + let text = colors.selected_text; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("flex().flex_col().gap_2()"), - ) - .child( - div() - .h_24() - .flex() - .flex_col() - .gap_2() - .child(block("A", gpui::red(), text).h_6()) - .child(block("B", gpui::green(), text).h_6()) - .child(block("C", gpui::blue(), text).h_6()), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex().flex_col().gap_2()"), + ) + .child( + div() + .h_24() + .flex() + .flex_col() + .gap_2() + .child(block("A", gpui::red(), text).h_6()) + .child(block("B", gpui::green(), text).h_6()) + .child(block("C", gpui::blue(), text).h_6()), + ) } fn flexbox_justify_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; - let surface = colors.container; + let text_muted = colors.disabled; + let text = colors.selected_text; + let surface = colors.container; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("justify_between / justify_center / justify_end"), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .flex() - .justify_between() - .p_1() - .bg(surface) - .rounded_sm() - .child(block("Start", gpui::red(), text).px_2().py_1()) - .child(block("End", gpui::blue(), text).px_2().py_1()), - ) - .child( - div() - .flex() - .justify_center() - .p_1() - .bg(surface) - .rounded_sm() - .child(block("Center", gpui::green(), text).px_2().py_1()), - ) - .child( - div() - .flex() - .justify_end() - .p_1() - .bg(surface) - .rounded_sm() - .child(block("End", gpui::yellow(), text).px_2().py_1()), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("justify_between / justify_center / justify_end"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .flex() + .justify_between() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("Start", gpui::red(), text).px_2().py_1()) + .child(block("End", gpui::blue(), text).px_2().py_1()), + ) + .child( + div() + .flex() + .justify_center() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("Center", gpui::green(), text).px_2().py_1()), + ) + .child( + div() + .flex() + .justify_end() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("End", gpui::yellow(), text).px_2().py_1()), + ), + ) } fn flexbox_grow_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; + let text_muted = colors.disabled; + let text = colors.selected_text; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("flex_1 (grow) vs flex_none (fixed)"), - ) - .child( - div() - .flex() - .gap_2() - .child(block("fixed", gpui::red(), text).flex_none().w_16().h_8()) - .child(block("flex_1 (grows)", gpui::green(), text).flex_1().h_8()) - .child(block("fixed", gpui::blue(), text).flex_none().w_16().h_8()), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex_1 (grow) vs flex_none (fixed)"), + ) + .child( + div() + .flex() + .gap_2() + .child(block("fixed", gpui::red(), text).flex_none().w_16().h_8()) + .child(block("flex_1 (grows)", gpui::green(), text).flex_1().h_8()) + .child(block("fixed", gpui::blue(), text).flex_none().w_16().h_8()), + ) } // Grid Examples fn grid_basic_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; + let text_muted = colors.disabled; + let text = colors.selected_text; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("grid().grid_cols(3).gap_1()"), - ) - .child( - div() - .grid() - .grid_cols(3) - .gap_1() - .child(block("1", gpui::red(), text).h_8()) - .child(block("2", gpui::green(), text).h_8()) - .child(block("3", gpui::blue(), text).h_8()) - .child(block("4", gpui::yellow(), text).h_8()) - .child(block("5", gpui::red(), text).h_8()) - .child(block("6", gpui::green(), text).h_8()), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("grid().grid_cols(3).gap_1()"), + ) + .child( + div() + .grid() + .grid_cols(3) + .gap_1() + .child(block("1", gpui::red(), text).h_8()) + .child(block("2", gpui::green(), text).h_8()) + .child(block("3", gpui::blue(), text).h_8()) + .child(block("4", gpui::yellow(), text).h_8()) + .child(block("5", gpui::red(), text).h_8()) + .child(block("6", gpui::green(), text).h_8()), + ) } fn grid_span_example(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; + let text_muted = colors.disabled; + let text = colors.selected_text; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("col_span / row_span"), - ) - .child( - div() - .grid() - .grid_cols(4) - .grid_rows(3) - .gap_1() - .child( - block("Header (col_span_full)", gpui::red(), text) - .col_span_full() - .h_6(), - ) - .child( - block("Side", gpui::green(), text) - .col_span(1) - .row_span(2) - .h_full(), - ) - .child( - block("Content (col_span 3)", gpui::blue(), text) - .col_span(3) - .row_span(2) - .h_full(), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("col_span / row_span"), + ) + .child( + div() + .grid() + .grid_cols(4) + .grid_rows(3) + .gap_1() + .child( + block("Header (col_span_full)", gpui::red(), text) + .col_span_full() + .h_6(), + ) + .child( + block("Side", gpui::green(), text) + .col_span(1) + .row_span(2) + .h_full(), + ) + .child( + block("Content (col_span 3)", gpui::blue(), text) + .col_span(3) + .row_span(2) + .h_full(), + ), + ) } // Common Layout Patterns fn app_shell_pattern(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.text; - let surface = colors.container; - let surface_hover = colors.selected; - let background = colors.background; - let border = colors.border; + let text_muted = colors.disabled; + let text = colors.text; + let surface = colors.container; + let surface_hover = colors.selected; + let background = colors.background; + let border = colors.border; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("App Shell: Header + Sidebar + Content"), - ) - .child( - div() - .h_32() - .flex() - .flex_col() - .border_1() - .border_color(border) - .rounded_md() - .overflow_hidden() - .child( - div() - .h_6() - .flex() - .items_center() - .px_2() - .bg(surface_hover) - .text_xs() - .text_color(text) - .child("Header"), - ) - .child( - div() - .flex_1() - .flex() - .child( - div() - .w_16() - .bg(surface) - .flex() - .items_center() - .justify_center() - .text_xs() - .text_color(text_muted) - .child("Side"), - ) - .child( - div() - .flex_1() - .bg(background) - .flex() - .items_center() - .justify_center() - .text_xs() - .text_color(text_muted) - .child("Content"), - ), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("App Shell: Header + Sidebar + Content"), + ) + .child( + div() + .h_32() + .flex() + .flex_col() + .border_1() + .border_color(border) + .rounded_md() + .overflow_hidden() + .child( + div() + .h_6() + .flex() + .items_center() + .px_2() + .bg(surface_hover) + .text_xs() + .text_color(text) + .child("Header"), + ) + .child( + div() + .flex_1() + .flex() + .child( + div() + .w_16() + .bg(surface) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(text_muted) + .child("Side"), + ) + .child( + div() + .flex_1() + .bg(background) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(text_muted) + .child("Content"), + ), + ), + ) } fn centered_pattern(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let text = colors.selected_text; - let surface = colors.container; - let accent = colors.selected; + let text_muted = colors.disabled; + let text = colors.selected_text; + let surface = colors.container; + let accent = colors.selected; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Centering: items_center + justify_center"), - ) - .child( - div() - .h_20() - .flex() - .items_center() - .justify_center() - .bg(surface) - .rounded_md() - .child( - div() - .px_4() - .py_2() - .bg(accent) - .rounded_md() - .text_xs() - .text_color(text) - .child("Perfectly Centered"), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Centering: items_center + justify_center"), + ) + .child( + div() + .h_20() + .flex() + .items_center() + .justify_center() + .bg(surface) + .rounded_md() + .child( + div() + .px_4() + .py_2() + .bg(accent) + .rounded_md() + .text_xs() + .text_color(text) + .child("Perfectly Centered"), + ), + ) } fn stack_pattern(colors: &Colors) -> impl IntoElement { - let text_muted = colors.disabled; - let surface = colors.container; + let text_muted = colors.disabled; + let surface = colors.container; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Stack: Overlapping with absolute positioning"), - ) - .child( - div() - .h_20() - .relative() - .bg(surface) - .rounded_md() - .child( - div() - .absolute() - .top_2() - .left_2() - .size_10() - .bg(gpui::red().opacity(0.7)) - .rounded_md(), - ) - .child( - div() - .absolute() - .top_4() - .left_4() - .size_10() - .bg(gpui::green().opacity(0.7)) - .rounded_md(), - ) - .child( - div() - .absolute() - .top_6() - .left_6() - .size_10() - .bg(gpui::blue().opacity(0.7)) - .rounded_md(), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Stack: Overlapping with absolute positioning"), + ) + .child( + div() + .h_20() + .relative() + .bg(surface) + .rounded_md() + .child( + div() + .absolute() + .top_2() + .left_2() + .size_10() + .bg(gpui::red().opacity(0.7)) + .rounded_md(), + ) + .child( + div() + .absolute() + .top_4() + .left_4() + .size_10() + .bg(gpui::green().opacity(0.7)) + .rounded_md(), + ) + .child( + div() + .absolute() + .top_6() + .left_6() + .size_10() + .bg(gpui::blue().opacity(0.7)) + .rounded_md(), + ), + ) } // Main Application View @@ -392,109 +392,109 @@ fn stack_pattern(colors: &Colors) -> impl IntoElement { struct LayoutExample; impl Render for LayoutExample { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .id("main") - .size_full() - .p_4() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_4() - .max_w(px(600.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Layout Patterns"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Flexbox, Grid, and common layout patterns in GPUI"), - ), - ) - .child(section( - &colors, - "Flexbox: Row", - flexbox_row_example(&colors), - )) - .child(section( - &colors, - "Flexbox: Column", - flexbox_column_example(&colors), - )) - .child(section( - &colors, - "Flexbox: Justify Content", - flexbox_justify_example(&colors), - )) - .child(section( - &colors, - "Flexbox: Grow/Shrink", - flexbox_grow_example(&colors), - )) - .child(section(&colors, "Grid: Basic", grid_basic_example(&colors))) - .child(section(&colors, "Grid: Spans", grid_span_example(&colors))) - .child(section( - &colors, - "Pattern: App Shell", - app_shell_pattern(&colors), - )) - .child(section( - &colors, - "Pattern: Centering", - centered_pattern(&colors), - )) - .child(section(&colors, "Pattern: Stack", stack_pattern(&colors))), - ) - } + div() + .id("main") + .size_full() + .p_4() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Layout Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Flexbox, Grid, and common layout patterns in GPUI"), + ), + ) + .child(section( + &colors, + "Flexbox: Row", + flexbox_row_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Column", + flexbox_column_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Justify Content", + flexbox_justify_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Grow/Shrink", + flexbox_grow_example(&colors), + )) + .child(section(&colors, "Grid: Basic", grid_basic_example(&colors))) + .child(section(&colors, "Grid: Spans", grid_span_example(&colors))) + .child(section( + &colors, + "Pattern: App Shell", + app_shell_pattern(&colors), + )) + .child(section( + &colors, + "Pattern: Centering", + centered_pattern(&colors), + )) + .child(section(&colors, "Pattern: Stack", stack_pattern(&colors))), + ) + } } fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { - let surface: Hsla = colors.container.into(); + let surface: Hsla = colors.container.into(); - div() - .flex() - .flex_col() - .gap_2() - .p_3() - .bg(surface.opacity(0.5)) - .rounded_lg() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child(content) + div() + .flex() + .flex_col() + .gap_2() + .p_3() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) } fn main() { - gpui_platform::application().run(|cx| { - let bounds = Bounds::centered(None, size(px(650.), px(700.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| LayoutExample), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx| { + let bounds = Bounds::centered(None, size(px(650.), px(700.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| LayoutExample), + ) + .expect("Failed to open window"); - init_example(cx, "Layout"); - }); + init_example(cx, "Layout"); + }); } diff --git a/crates/gpui/examples/learn/node_graph.rs b/crates/gpui/examples/learn/node_graph.rs index 412974b9af..8fd81fd7f7 100644 --- a/crates/gpui/examples/learn/node_graph.rs +++ b/crates/gpui/examples/learn/node_graph.rs @@ -13,13 +13,13 @@ // The modules under demo are skeletons whose bodies are `todo!()` by design. #![allow(clippy::todo)] -use gpui::{ - App, Bounds, Context, Entity, Hsla, Pixels, Point, Render, SharedString, Window, WindowBounds, - WindowOptions, div, point, prelude::*, px, size, -}; use gpui::node_graph::{ - EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeGraphView, NodeId, - PortData, PortDataType, PortId, PortKind, + EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeGraphView, NodeId, + PortData, PortDataType, PortId, PortKind, +}; +use gpui::{ + App, Bounds, Context, Entity, Hsla, Pixels, Point, Render, SharedString, Window, WindowBounds, + WindowOptions, div, point, prelude::*, px, size, }; // --------------------------------------------------------------------------- @@ -28,198 +28,198 @@ use gpui::node_graph::{ #[derive(Clone)] struct MockPort { - id: PortId, - kind: PortKind, - label: &'static str, - data_type: PortDataType, - connected: bool, + id: PortId, + kind: PortKind, + label: &'static str, + data_type: PortDataType, + connected: bool, } impl PortData for MockPort { - fn id(&self) -> PortId { - self.id - } - fn kind(&self) -> PortKind { - self.kind - } - fn label(&self) -> SharedString { - self.label.into() - } - fn data_type(&self) -> PortDataType { - self.data_type.clone() - } - fn is_connected(&self) -> bool { - self.connected - } + fn id(&self) -> PortId { + self.id + } + fn kind(&self) -> PortKind { + self.kind + } + fn label(&self) -> SharedString { + self.label.into() + } + fn data_type(&self) -> PortDataType { + self.data_type.clone() + } + fn is_connected(&self) -> bool { + self.connected + } } #[derive(Clone)] struct MockNode { - id: NodeId, - title: &'static str, - position: Point, - inputs: Vec, - outputs: Vec, - header_color: Option, + id: NodeId, + title: &'static str, + position: Point, + inputs: Vec, + outputs: Vec, + header_color: Option, } impl NodeData for MockNode { - type Port = MockPort; + type Port = MockPort; - fn id(&self) -> NodeId { - self.id - } - fn title(&self) -> SharedString { - self.title.into() - } - fn position(&self) -> Point { - self.position - } - fn inputs(&self) -> Vec { - self.inputs.clone() - } - fn outputs(&self) -> Vec { - self.outputs.clone() - } - fn header_color(&self) -> Option { - self.header_color - } - fn is_collapsed(&self) -> bool { - false - } - fn is_enabled(&self) -> bool { - true - } + fn id(&self) -> NodeId { + self.id + } + fn title(&self) -> SharedString { + self.title.into() + } + fn position(&self) -> Point { + self.position + } + fn inputs(&self) -> Vec { + self.inputs.clone() + } + fn outputs(&self) -> Vec { + self.outputs.clone() + } + fn header_color(&self) -> Option { + self.header_color + } + fn is_collapsed(&self) -> bool { + false + } + fn is_enabled(&self) -> bool { + true + } } #[derive(Clone)] struct MockEdge { - id: EdgeId, - from_node: NodeId, - from_port: PortId, - to_node: NodeId, - to_port: PortId, + id: EdgeId, + from_node: NodeId, + from_port: PortId, + to_node: NodeId, + to_port: PortId, } impl EdgeData for MockEdge { - fn id(&self) -> EdgeId { - self.id - } - fn from_node(&self) -> NodeId { - self.from_node - } - fn from_port(&self) -> PortId { - self.from_port - } - fn to_node(&self) -> NodeId { - self.to_node - } - fn to_port(&self) -> PortId { - self.to_port - } + fn id(&self) -> EdgeId { + self.id + } + fn from_node(&self) -> NodeId { + self.from_node + } + fn from_port(&self) -> PortId { + self.from_port + } + fn to_node(&self) -> NodeId { + self.to_node + } + fn to_port(&self) -> PortId { + self.to_port + } } /// The mock pipeline: media → transform → output. struct MockGraph { - nodes: Vec, - edges: Vec, + nodes: Vec, + edges: Vec, } impl MockGraph { - fn new() -> Self { - let video = PortDataType::new("video", Hsla::blue()); - // Port id packing: node id in the high bits, port index low. Inputs - // and outputs share one index space per node. - let port = |node: u64, index: u64| PortId((node << 32) | index); + fn new() -> Self { + let video = PortDataType::new("video", Hsla::blue()); + // Port id packing: node id in the high bits, port index low. Inputs + // and outputs share one index space per node. + let port = |node: u64, index: u64| PortId((node << 32) | index); - let media = MockNode { - id: NodeId(1), - title: "Media", - position: point(px(40.), px(80.)), - inputs: vec![], - outputs: vec![MockPort { - id: port(1, 0), - kind: PortKind::Output, - label: "video", - data_type: video.clone(), - connected: true, - }], - header_color: Some(Hsla::green()), - }; - let transform = MockNode { - id: NodeId(2), - title: "Transform", - position: point(px(320.), px(140.)), - inputs: vec![MockPort { - id: port(2, 0), - kind: PortKind::Input, - label: "in", - data_type: video.clone(), - connected: true, - }], - outputs: vec![MockPort { - id: port(2, 1), - kind: PortKind::Output, - label: "out", - data_type: video.clone(), - connected: true, - }], - header_color: None, - }; - let output = MockNode { - id: NodeId(3), - title: "Output", - position: point(px(600.), px(200.)), - inputs: vec![MockPort { - id: port(3, 0), - kind: PortKind::Input, - label: "in", - data_type: video, - connected: true, - }], - outputs: vec![], - header_color: Some(Hsla::red()), - }; + let media = MockNode { + id: NodeId(1), + title: "Media", + position: point(px(40.), px(80.)), + inputs: vec![], + outputs: vec![MockPort { + id: port(1, 0), + kind: PortKind::Output, + label: "video", + data_type: video.clone(), + connected: true, + }], + header_color: Some(Hsla::green()), + }; + let transform = MockNode { + id: NodeId(2), + title: "Transform", + position: point(px(320.), px(140.)), + inputs: vec![MockPort { + id: port(2, 0), + kind: PortKind::Input, + label: "in", + data_type: video.clone(), + connected: true, + }], + outputs: vec![MockPort { + id: port(2, 1), + kind: PortKind::Output, + label: "out", + data_type: video.clone(), + connected: true, + }], + header_color: None, + }; + let output = MockNode { + id: NodeId(3), + title: "Output", + position: point(px(600.), px(200.)), + inputs: vec![MockPort { + id: port(3, 0), + kind: PortKind::Input, + label: "in", + data_type: video, + connected: true, + }], + outputs: vec![], + header_color: Some(Hsla::red()), + }; - let edges = vec![ - MockEdge { - id: EdgeId(1), - from_node: NodeId(1), - from_port: port(1, 0), - to_node: NodeId(2), - to_port: port(2, 0), - }, - MockEdge { - id: EdgeId(2), - from_node: NodeId(2), - from_port: port(2, 1), - to_node: NodeId(3), - to_port: port(3, 0), - }, - ]; + let edges = vec![ + MockEdge { + id: EdgeId(1), + from_node: NodeId(1), + from_port: port(1, 0), + to_node: NodeId(2), + to_port: port(2, 0), + }, + MockEdge { + id: EdgeId(2), + from_node: NodeId(2), + from_port: port(2, 1), + to_node: NodeId(3), + to_port: port(3, 0), + }, + ]; - Self { - nodes: vec![media, transform, output], - edges, - } - } + Self { + nodes: vec![media, transform, output], + edges, + } + } } impl NodeGraphDataSource for MockGraph { - type Node = MockNode; - type Edge = MockEdge; + type Node = MockNode; + type Edge = MockEdge; - fn nodes(&self) -> Vec { - self.nodes.clone() - } - fn edges(&self) -> Vec { - self.edges.clone() - } - fn can_connect(&self, _from: PortId, _to: PortId) -> bool { - // A real app checks type compatibility, cycles and cardinality here. - // The mock allows everything between distinct ports. - true - } + fn nodes(&self) -> Vec { + self.nodes.clone() + } + fn edges(&self) -> Vec { + self.edges.clone() + } + fn can_connect(&self, _from: PortId, _to: PortId) -> bool { + // A real app checks type compatibility, cycles and cardinality here. + // The mock allows everything between distinct ports. + true + } } // --------------------------------------------------------------------------- @@ -227,47 +227,47 @@ impl NodeGraphDataSource for MockGraph { // --------------------------------------------------------------------------- struct NodeGraphExample { - graph: Entity, - view: Entity>, + graph: Entity, + view: Entity>, } impl NodeGraphExample { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let graph = cx.new(|_cx| MockGraph::new()); - let view = cx.new(|cx| NodeGraphView::new(graph.clone(), window, cx)); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let graph = cx.new(|_cx| MockGraph::new()); + let view = cx.new(|cx| NodeGraphView::new(graph.clone(), window, cx)); - // In Oak, each event becomes an engine operation wrapped in an undo - // command; here we just log the request. - cx.subscribe(&view, |_this, _view, event: &NodeGraphEvent, cx| { - println!("[node_graph] edit request: {event:?}"); - // After applying a request to the model, notify so the view - // re-reads it, e.g.: `graph.update(cx, |_, cx| cx.notify())`. - let _ = cx; - }) - .detach(); + // In Oak, each event becomes an engine operation wrapped in an undo + // command; here we just log the request. + cx.subscribe(&view, |_this, _view, event: &NodeGraphEvent, cx| { + println!("[node_graph] edit request: {event:?}"); + // After applying a request to the model, notify so the view + // re-reads it, e.g.: `graph.update(cx, |_, cx| cx.notify())`. + let _ = cx; + }) + .detach(); - Self { graph, view } - } + Self { graph, view } + } } impl Render for NodeGraphExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let _ = &self.graph; - div().size_full().child(self.view.clone()) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let _ = &self.graph; + div().size_full().child(self.view.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(900.), px(600.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| NodeGraphExample::new(window, cx)), - ) - .expect("failed to open window"); - cx.activate(true); - }); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(900.), px(600.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| NodeGraphExample::new(window, cx)), + ) + .expect("failed to open window"); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/learn/styling.rs b/crates/gpui/examples/learn/styling.rs index b08bd4f346..3034d4c3ab 100644 --- a/crates/gpui/examples/learn/styling.rs +++ b/crates/gpui/examples/learn/styling.rs @@ -8,8 +8,8 @@ use gpui::colors::Colors; use gpui::{ - App, Bounds, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, Render, Rgba, Window, - WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, + App, Bounds, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, Render, Rgba, Window, + WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, }; actions!(styling_example, [Quit, Tab, TabPrev]); @@ -17,497 +17,497 @@ actions!(styling_example, [Quit, Tab, TabPrev]); // Interactive States Example fn interactive_button( - id: impl Into, - label: &'static str, - colors: &Colors, + id: impl Into, + label: &'static str, + colors: &Colors, ) -> impl IntoElement { - let accent = colors.selected; - let accent_hover = colors.selected; - let accent_active = colors.selected; - let text = colors.selected_text; + let accent = colors.selected; + let accent_hover = colors.selected; + let accent_active = colors.selected; + let text = colors.selected_text; - div() - .id(id) - .px_4() - .py_2() - .rounded_md() - .cursor_pointer() - .bg(accent) - .text_color(text) - .text_sm() - .hover(move |style| style.bg(accent_hover)) - .active(move |style| style.bg(accent_active)) - .child(label) + div() + .id(id) + .px_4() + .py_2() + .rounded_md() + .cursor_pointer() + .bg(accent) + .text_color(text) + .text_sm() + .hover(move |style| style.bg(accent_hover)) + .active(move |style| style.bg(accent_active)) + .child(label) } fn focus_button( - id: impl Into, - label: &'static str, - focus_handle: &FocusHandle, - colors: &Colors, + id: impl Into, + label: &'static str, + focus_handle: &FocusHandle, + colors: &Colors, ) -> impl IntoElement { - let surface = colors.container; - let surface_hover = colors.selected; - let text = colors.text; - let accent = colors.selected; - let focus_ring: Rgba = rgb(0x60a5fa); + let surface = colors.container; + let surface_hover = colors.selected; + let text = colors.text; + let accent = colors.selected; + let focus_ring: Rgba = rgb(0x60a5fa); - div() - .id(id) - .track_focus(focus_handle) - .px_4() - .py_2() - .rounded_md() - .cursor_pointer() - .bg(surface) - .text_color(text) - .text_sm() - .border_2() - .border_color(gpui::transparent_black()) - .hover(move |style| style.bg(surface_hover)) - .focus(move |style| style.border_color(accent)) - .focus_visible(move |style| style.border_color(focus_ring).shadow_sm()) - .child(label) + div() + .id(id) + .track_focus(focus_handle) + .px_4() + .py_2() + .rounded_md() + .cursor_pointer() + .bg(surface) + .text_color(text) + .text_sm() + .border_2() + .border_color(gpui::transparent_black()) + .hover(move |style| style.bg(surface_hover)) + .focus(move |style| style.border_color(accent)) + .focus_visible(move |style| style.border_color(focus_ring).shadow_sm()) + .child(label) } fn interactive_states_section(colors: &Colors) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("hover() / active() - Mouse interaction states"), - ) - .child( - div() - .flex() - .gap_2() - .child(interactive_button("btn-1", "Hover me", colors)) - .child(interactive_button("btn-2", "Click me", colors)), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("hover() / active() - Mouse interaction states"), + ) + .child( + div() + .flex() + .gap_2() + .child(interactive_button("btn-1", "Hover me", colors)) + .child(interactive_button("btn-2", "Click me", colors)), + ) } // Conditional Styling Example fn status_badge(status: &'static str, variant: StatusVariant, colors: &Colors) -> impl IntoElement { - let (bg, text): (Rgba, Rgba) = match variant { - StatusVariant::Success => (rgb(0x388e3c), colors.selected_text), - StatusVariant::Warning => (rgb(0xf9a825), rgb(0x000000)), - StatusVariant::Error => (rgb(0xd32f2f), colors.selected_text), - StatusVariant::Neutral => (colors.container, colors.text), - }; + let (bg, text): (Rgba, Rgba) = match variant { + StatusVariant::Success => (rgb(0x388e3c), colors.selected_text), + StatusVariant::Warning => (rgb(0xf9a825), rgb(0x000000)), + StatusVariant::Error => (rgb(0xd32f2f), colors.selected_text), + StatusVariant::Neutral => (colors.container, colors.text), + }; - div() - .px_2() - .py_0p5() - .rounded_full() - .text_xs() - .bg(bg) - .text_color(text) - .child(status) + div() + .px_2() + .py_0p5() + .rounded_full() + .text_xs() + .bg(bg) + .text_color(text) + .child(status) } #[derive(Clone, Copy)] enum StatusVariant { - Success, - Warning, - Error, - Neutral, + Success, + Warning, + Error, + Neutral, } fn list_item( - id: impl Into, - label: &'static str, - is_selected: bool, - is_disabled: bool, - colors: &Colors, + id: impl Into, + label: &'static str, + is_selected: bool, + is_disabled: bool, + colors: &Colors, ) -> impl IntoElement { - let surface = colors.container; - let surface_hover = colors.selected; - let text = colors.text; - let text_muted = colors.disabled; - let accent = colors.selected; + let surface = colors.container; + let surface_hover = colors.selected; + let text = colors.text; + let text_muted = colors.disabled; + let accent = colors.selected; - div() - .id(id) - .px_3() - .py_2() - .rounded_md() - .text_sm() - .cursor_pointer() - .border_1() - .border_color(gpui::transparent_black()) - .when(is_disabled, |el| { - el.opacity(0.5) - .cursor_not_allowed() - .bg(surface) - .text_color(text_muted) - }) - .when(!is_disabled && is_selected, move |el| { - let accent_bg: Hsla = accent.into(); - el.bg(accent_bg.opacity(0.2)) - .border_color(accent) - .text_color(text) - }) - .when(!is_disabled && !is_selected, move |el| { - el.bg(surface) - .text_color(text) - .hover(move |style| style.bg(surface_hover)) - }) - .child(label) + div() + .id(id) + .px_3() + .py_2() + .rounded_md() + .text_sm() + .cursor_pointer() + .border_1() + .border_color(gpui::transparent_black()) + .when(is_disabled, |el| { + el.opacity(0.5) + .cursor_not_allowed() + .bg(surface) + .text_color(text_muted) + }) + .when(!is_disabled && is_selected, move |el| { + let accent_bg: Hsla = accent.into(); + el.bg(accent_bg.opacity(0.2)) + .border_color(accent) + .text_color(text) + }) + .when(!is_disabled && !is_selected, move |el| { + el.bg(surface) + .text_color(text) + .hover(move |style| style.bg(surface_hover)) + }) + .child(label) } fn conditional_section(colors: &Colors) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("when() - Apply styles conditionally"), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child(list_item("item-1", "Normal item", false, false, colors)) - .child(list_item("item-2", "Selected item", true, false, colors)) - .child(list_item("item-3", "Disabled item", false, true, colors)), - ) - .child( - div() - .text_xs() - .text_color(colors.disabled) - .mt_2() - .child("Status badges with variant-based styling"), - ) - .child( - div() - .flex() - .gap_2() - .child(status_badge("Success", StatusVariant::Success, colors)) - .child(status_badge("Warning", StatusVariant::Warning, colors)) - .child(status_badge("Error", StatusVariant::Error, colors)) - .child(status_badge("Neutral", StatusVariant::Neutral, colors)), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("when() - Apply styles conditionally"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child(list_item("item-1", "Normal item", false, false, colors)) + .child(list_item("item-2", "Selected item", true, false, colors)) + .child(list_item("item-3", "Disabled item", false, true, colors)), + ) + .child( + div() + .text_xs() + .text_color(colors.disabled) + .mt_2() + .child("Status badges with variant-based styling"), + ) + .child( + div() + .flex() + .gap_2() + .child(status_badge("Success", StatusVariant::Success, colors)) + .child(status_badge("Warning", StatusVariant::Warning, colors)) + .child(status_badge("Error", StatusVariant::Error, colors)) + .child(status_badge("Neutral", StatusVariant::Neutral, colors)), + ) } // Group Hover Example fn card_with_group_hover( - id: impl Into, - title: &'static str, - description: &'static str, - colors: &Colors, + id: impl Into, + title: &'static str, + description: &'static str, + colors: &Colors, ) -> impl IntoElement { - let surface = colors.container; - let border = colors.border; - let accent = colors.selected; - let text = colors.text; - let text_muted = colors.disabled; + let surface = colors.container; + let border = colors.border; + let accent = colors.selected; + let text = colors.text; + let text_muted = colors.disabled; - div() - .id(id) - .group("card") - .p_4() - .rounded_lg() - .bg(surface) - .border_1() - .border_color(border) - .cursor_pointer() - .hover(move |style| style.border_color(accent)) - .child( - div() - .flex() - .justify_between() - .items_center() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(text) - .child(title), - ) - .child( - div() - .text_xs() - .text_color(text_muted) - .opacity(0.) - .group_hover("card", |style| style.opacity(1.)) - .child("→"), - ), - ) - .child( - div() - .mt_1() - .text_xs() - .text_color(text_muted) - .child(description), - ) + div() + .id(id) + .group("card") + .p_4() + .rounded_lg() + .bg(surface) + .border_1() + .border_color(border) + .cursor_pointer() + .hover(move |style| style.border_color(accent)) + .child( + div() + .flex() + .justify_between() + .items_center() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(text_muted) + .opacity(0.) + .group_hover("card", |style| style.opacity(1.)) + .child("→"), + ), + ) + .child( + div() + .mt_1() + .text_xs() + .text_color(text_muted) + .child(description), + ) } fn group_hover_section(colors: &Colors) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("group() / group_hover() - Parent hover affects children"), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(card_with_group_hover( - "card-1", - "Documents", - "View and manage your documents", - colors, - )) - .child(card_with_group_hover( - "card-2", - "Settings", - "Configure application settings", - colors, - )), - ) + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("group() / group_hover() - Parent hover affects children"), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(card_with_group_hover( + "card-1", + "Documents", + "View and manage your documents", + colors, + )) + .child(card_with_group_hover( + "card-2", + "Settings", + "Configure application settings", + colors, + )), + ) } // Main Application View struct StylingExample { - focus_handle: FocusHandle, - buttons: Vec, + focus_handle: FocusHandle, + buttons: Vec, } impl StylingExample { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - window.focus(&focus_handle, cx); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle, cx); - let buttons = 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), - ]; + let buttons = 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), + ]; - Self { - focus_handle, - buttons, - } - } + Self { + focus_handle, + buttons, + } + } - fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - window.focus_next(cx); - } + fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + window.focus_next(cx); + } - fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { - window.focus_prev(cx); - } + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { + window.focus_prev(cx); + } } impl Render for StylingExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - 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() - .p_6() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_6() - .max_w(px(500.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(colors.text) - .child("Styling Patterns"), - ) - .child( - div() - .text_sm() - .text_color(colors.disabled) - .child("Interactive states, conditional styling, and theming"), - ), - ) - .child(section( - &colors, - "Interactive States", - interactive_states_section(&colors), - )) - .child(section( - &colors, - "Focus States (Tab to navigate)", - div() - .flex() - .flex_col() - .gap_3() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("focus() / focus_visible() - Keyboard navigation"), - ) - .child( - div() - .flex() - .gap_2() - .child(focus_button( - "focus-1", - "Button 1", - &self.buttons[0], - &colors, - )) - .child(focus_button( - "focus-2", - "Button 2", - &self.buttons[1], - &colors, - )) - .child(focus_button( - "focus-3", - "Button 3", - &self.buttons[2], - &colors, - )), - ), - )) - .child(section( - &colors, - "Conditional Styling", - conditional_section(&colors), - )) - .child(section( - &colors, - "Group Hover", - group_hover_section(&colors), - )) - .child(section( - &colors, - "Default Colors", - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(colors.disabled) - .child("Using Colors::for_appearance() for consistent theming"), - ) - .child( - div() - .flex() - .flex_wrap() - .gap_2() - .child(color_swatch(&colors, "background", colors.background)) - .child(color_swatch(&colors, "container", colors.container)) - .child(color_swatch(&colors, "selected", colors.selected)) - .child(color_swatch(&colors, "success", rgb(0x388e3c))) - .child(color_swatch(&colors, "warning", rgb(0xf9a825))) - .child(color_swatch(&colors, "error", rgb(0xd32f2f))) - .child(color_swatch(&colors, "border", colors.border)), - ), - )), - ) - } + 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() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Styling Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child("Interactive states, conditional styling, and theming"), + ), + ) + .child(section( + &colors, + "Interactive States", + interactive_states_section(&colors), + )) + .child(section( + &colors, + "Focus States (Tab to navigate)", + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("focus() / focus_visible() - Keyboard navigation"), + ) + .child( + div() + .flex() + .gap_2() + .child(focus_button( + "focus-1", + "Button 1", + &self.buttons[0], + &colors, + )) + .child(focus_button( + "focus-2", + "Button 2", + &self.buttons[1], + &colors, + )) + .child(focus_button( + "focus-3", + "Button 3", + &self.buttons[2], + &colors, + )), + ), + )) + .child(section( + &colors, + "Conditional Styling", + conditional_section(&colors), + )) + .child(section( + &colors, + "Group Hover", + group_hover_section(&colors), + )) + .child(section( + &colors, + "Default Colors", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(colors.disabled) + .child("Using Colors::for_appearance() for consistent theming"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_2() + .child(color_swatch(&colors, "background", colors.background)) + .child(color_swatch(&colors, "container", colors.container)) + .child(color_swatch(&colors, "selected", colors.selected)) + .child(color_swatch(&colors, "success", rgb(0x388e3c))) + .child(color_swatch(&colors, "warning", rgb(0xf9a825))) + .child(color_swatch(&colors, "error", rgb(0xd32f2f))) + .child(color_swatch(&colors, "border", colors.border)), + ), + )), + ) + } } fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { - let surface: Hsla = colors.container.into(); - let border: Hsla = colors.border.into(); + let surface: Hsla = colors.container.into(); + let border: Hsla = colors.border.into(); - div() - .flex() - .flex_col() - .gap_3() - .p_4() - .bg(surface.opacity(0.3)) - .rounded_lg() - .border_1() - .border_color(border.opacity(0.5)) - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child(content) + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .bg(surface.opacity(0.3)) + .rounded_lg() + .border_1() + .border_color(border.opacity(0.5)) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) } fn color_swatch(colors: &Colors, name: &'static str, color: Rgba) -> impl IntoElement { - let text_muted = colors.disabled; + let text_muted = colors.disabled; - div() - .flex() - .flex_col() - .items_center() - .gap_1() - .child( - div() - .size_8() - .rounded_md() - .bg(color) - .border_1() - .border_color(gpui::white().opacity(0.2)), - ) - .child(div().text_xs().text_color(text_muted).child(name)) + div() + .flex() + .flex_col() + .items_center() + .gap_1() + .child( + div() + .size_8() + .rounded_md() + .bg(color) + .border_1() + .border_color(gpui::white().opacity(0.2)), + ) + .child(div().text_xs().text_color(text_muted).child(name)) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([ - KeyBinding::new("cmd-q", Quit, None), - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - ]); - cx.set_menus(vec![Menu { - name: "Styling".into(), - items: vec![MenuItem::action("Quit", Quit)], - disabled: false, - }]); - cx.on_window_closed(|cx, _window_id| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); + gpui_platform::application().run(|cx: &mut App| { + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([ + KeyBinding::new("cmd-q", Quit, None), + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + ]); + cx.set_menus(vec![Menu { + name: "Styling".into(), + items: vec![MenuItem::action("Quit", Quit)], + disabled: false, + }]); + cx.on_window_closed(|cx, _window_id| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); - let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| StylingExample::new(window, cx)), - ) - .expect("Failed to open window"); - }); + let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| StylingExample::new(window, cx)), + ) + .expect("Failed to open window"); + }); } diff --git a/crates/gpui/examples/learn/text.rs b/crates/gpui/examples/learn/text.rs index 6b21a25f31..b849de4d22 100644 --- a/crates/gpui/examples/learn/text.rs +++ b/crates/gpui/examples/learn/text.rs @@ -14,457 +14,457 @@ mod example_prelude; use example_prelude::init_example; use gpui::{ - App, Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText, TextOverflow, Window, - WindowBounds, WindowOptions, colors::Colors, div, prelude::*, px, relative, rgb, size, + App, Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText, TextOverflow, Window, + WindowBounds, WindowOptions, colors::Colors, div, prelude::*, px, relative, rgb, size, }; // Text Styling Examples fn text_sizes_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; + let text = colors.text; + let text_muted = colors.disabled; - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Font sizes: text_xs, text_sm, text_base, text_lg, text_xl"), - ) - .child( - div() - .flex() - .flex_wrap() - .items_baseline() - .gap_3() - .child(div().text_xs().text_color(text).child("Extra Small")) - .child(div().text_sm().text_color(text).child("Small")) - .child(div().text_base().text_color(text).child("Base")) - .child(div().text_lg().text_color(text).child("Large")) - .child(div().text_xl().text_color(text).child("Extra Large")), - ) + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Font sizes: text_xs, text_sm, text_base, text_lg, text_xl"), + ) + .child( + div() + .flex() + .flex_wrap() + .items_baseline() + .gap_3() + .child(div().text_xs().text_color(text).child("Extra Small")) + .child(div().text_sm().text_color(text).child("Small")) + .child(div().text_base().text_color(text).child("Base")) + .child(div().text_lg().text_color(text).child("Large")) + .child(div().text_xl().text_color(text).child("Extra Large")), + ) } fn text_weights_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; + let text = colors.text; + let text_muted = colors.disabled; - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Font weights: THIN through BLACK"), - ) - .child( - div() - .flex() - .flex_wrap() - .gap_3() - .child( - div() - .text_color(text) - .font_weight(FontWeight::THIN) - .child("Thin"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::LIGHT) - .child("Light"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::NORMAL) - .child("Normal"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::MEDIUM) - .child("Medium"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::SEMIBOLD) - .child("Semibold"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::BOLD) - .child("Bold"), - ) - .child( - div() - .text_color(text) - .font_weight(FontWeight::BLACK) - .child("Black"), - ), - ) + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Font weights: THIN through BLACK"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_3() + .child( + div() + .text_color(text) + .font_weight(FontWeight::THIN) + .child("Thin"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::LIGHT) + .child("Light"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::NORMAL) + .child("Normal"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::MEDIUM) + .child("Medium"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::SEMIBOLD) + .child("Semibold"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::BOLD) + .child("Bold"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::BLACK) + .child("Black"), + ), + ) } // Text Alignment Examples fn text_alignment_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; - let surface = colors.container; + let text = colors.text; + let text_muted = colors.disabled; + let surface = colors.container; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Alignment: default (left), text_center, text_right"), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .p_2() - .bg(surface) - .rounded_sm() - .text_color(text) - .child("Left aligned (default)"), - ) - .child( - div() - .p_2() - .bg(surface) - .rounded_sm() - .text_center() - .text_color(text) - .child("Center aligned"), - ) - .child( - div() - .p_2() - .bg(surface) - .rounded_sm() - .text_right() - .text_color(text) - .child("Right aligned"), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Alignment: default (left), text_center, text_right"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .child("Left aligned (default)"), + ) + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_center() + .text_color(text) + .child("Center aligned"), + ) + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_right() + .text_color(text) + .child("Right aligned"), + ), + ) } // Text Decoration Examples fn text_decoration_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; - let accent = colors.selected; - let error = rgb(0xd32f2f); + let text = colors.text; + let text_muted = colors.disabled; + let accent = colors.selected; + let error = rgb(0xd32f2f); - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Decorations: underline, strikethrough, italic"), - ) - .child( - div() - .flex() - .flex_wrap() - .gap_4() - .child( - div() - .text_color(text) - .text_decoration_1() - .text_decoration_color(accent) - .child("Underlined text"), - ) - .child( - div() - .text_color(text) - .line_through() - .text_decoration_color(error) - .child("Strikethrough text"), - ) - .child(div().text_color(text).italic().child("Italic text")), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Decorations: underline, strikethrough, italic"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_4() + .child( + div() + .text_color(text) + .text_decoration_1() + .text_decoration_color(accent) + .child("Underlined text"), + ) + .child( + div() + .text_color(text) + .line_through() + .text_decoration_color(error) + .child("Strikethrough text"), + ) + .child(div().text_color(text).italic().child("Italic text")), + ) } // Text Overflow Examples fn text_overflow_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; - let surface = colors.container; - let border = colors.border; + let text = colors.text; + let text_muted = colors.disabled; + let surface = colors.container; + let border = colors.border; - let long_text = "The quick brown fox jumps over the lazy dog. This is a long sentence that will overflow its container."; + let long_text = "The quick brown fox jumps over the lazy dog. This is a long sentence that will overflow its container."; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Overflow handling: ellipsis, truncate, line_clamp"), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("text_ellipsis (single line):"), - ) - .child( - div() - .p_2() - .bg(surface) - .border_1() - .border_color(border) - .rounded_sm() - .text_color(text) - .overflow_hidden() - .text_ellipsis() - .child(long_text), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("line_clamp(2):"), - ) - .child( - div() - .p_2() - .bg(surface) - .border_1() - .border_color(border) - .rounded_sm() - .text_color(text) - .overflow_hidden() - .text_ellipsis() - .line_clamp(2) - .child(long_text), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("truncate (hard cut):"), - ) - .child( - div() - .p_2() - .bg(surface) - .border_1() - .border_color(border) - .rounded_sm() - .text_color(text) - .overflow_hidden() - .text_overflow(TextOverflow::Truncate("".into())) - .child(long_text), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("whitespace_nowrap:"), - ) - .child( - div() - .p_2() - .bg(surface) - .border_1() - .border_color(border) - .rounded_sm() - .text_color(text) - .overflow_hidden() - .whitespace_nowrap() - .child(long_text), - ), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Overflow handling: ellipsis, truncate, line_clamp"), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("text_ellipsis (single line):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_ellipsis() + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("line_clamp(2):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_ellipsis() + .line_clamp(2) + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("truncate (hard cut):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_overflow(TextOverflow::Truncate("".into())) + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("whitespace_nowrap:"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .whitespace_nowrap() + .child(long_text), + ), + ), + ) } // Styled Text Examples fn styled_text_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; + let text = colors.text; + let text_muted = colors.disabled; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("StyledText with inline highlights"), - ) - .child(div().text_lg().text_color(text).child( - StyledText::new("Bold Italic Normal Semibold").with_highlights([ - (0..4, FontWeight::BOLD.into()), - (5..11, FontStyle::Italic.into()), - (19..27, FontWeight::SEMIBOLD.into()), - ]), - )) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("StyledText with inline highlights"), + ) + .child(div().text_lg().text_color(text).child( + StyledText::new("Bold Italic Normal Semibold").with_highlights([ + (0..4, FontWeight::BOLD.into()), + (5..11, FontStyle::Italic.into()), + (19..27, FontWeight::SEMIBOLD.into()), + ]), + )) } // Character Grid Example fn character_grid_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; - let surface = colors.container; - let border = colors.border; + let text = colors.text; + let text_muted = colors.disabled; + let surface = colors.container; + let border = colors.border; - let characters = [ - // Latin - "A", "B", "C", "D", "E", "a", "b", "c", "d", "e", // Numbers - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // Greek - "α", "β", "γ", "δ", "ε", "θ", "λ", "π", "σ", "ω", // Cyrillic - "Д", "Ж", "И", "Л", "Ф", "Ц", "Ш", "Щ", "Ы", "Я", // CJK - "你", "好", "世", "界", "日", "本", "語", "中", "文", "字", // Symbols - "→", "←", "↑", "↓", "•", "★", "♠", "♥", "♦", "♣", // Emoji - "😀", "🎉", "🚀", "💡", "🔥", "✨", "🎨", "📚", "🎵", "❤️", - ]; + let characters = [ + // Latin + "A", "B", "C", "D", "E", "a", "b", "c", "d", "e", // Numbers + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // Greek + "α", "β", "γ", "δ", "ε", "θ", "λ", "π", "σ", "ω", // Cyrillic + "Д", "Ж", "И", "Л", "Ф", "Ц", "Ш", "Щ", "Ы", "Я", // CJK + "你", "好", "世", "界", "日", "本", "語", "中", "文", "字", // Symbols + "→", "←", "↑", "↓", "•", "★", "♠", "♥", "♦", "♣", // Emoji + "😀", "🎉", "🚀", "💡", "🔥", "✨", "🎨", "📚", "🎵", "❤️", + ]; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Unicode and emoji support"), - ) - .child( - div() - .p_2() - .bg(surface) - .border_1() - .border_color(border) - .rounded_md() - .child( - div() - .grid() - .grid_cols(10) - .gap_1() - .children(characters.iter().map(|c| { - div() - .flex() - .items_center() - .justify_center() - .size_8() - .text_lg() - .text_color(text) - .line_height(relative(1.0)) - .child(*c) - })), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Unicode and emoji support"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_md() + .child( + div() + .grid() + .grid_cols(10) + .gap_1() + .children(characters.iter().map(|c| { + div() + .flex() + .items_center() + .justify_center() + .size_8() + .text_lg() + .text_color(text) + .line_height(relative(1.0)) + .child(*c) + })), + ), + ) } // Line Height Example fn line_height_example(colors: &Colors) -> impl IntoElement { - let text = colors.text; - let text_muted = colors.disabled; - let surface = colors.container; + let text = colors.text; + let text_muted = colors.disabled; + let surface = colors.container; - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_xs() - .text_color(text_muted) - .child("Line height: relative(1.0), relative(1.5), relative(2.0)"), - ) - .child( - div() - .flex() - .gap_3() - .child( - div() - .flex_1() - .p_2() - .bg(surface) - .rounded_sm() - .text_color(text) - .text_sm() - .line_height(relative(1.0)) - .child("Tight\nline\nheight"), - ) - .child( - div() - .flex_1() - .p_2() - .bg(surface) - .rounded_sm() - .text_color(text) - .text_sm() - .line_height(relative(1.5)) - .child("Normal\nline\nheight"), - ) - .child( - div() - .flex_1() - .p_2() - .bg(surface) - .rounded_sm() - .text_color(text) - .text_sm() - .line_height(relative(2.0)) - .child("Loose\nline\nheight"), - ), - ) + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Line height: relative(1.0), relative(1.5), relative(2.0)"), + ) + .child( + div() + .flex() + .gap_3() + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(1.0)) + .child("Tight\nline\nheight"), + ) + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(1.5)) + .child("Normal\nline\nheight"), + ) + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(2.0)) + .child("Loose\nline\nheight"), + ), + ) } // Main Application View @@ -472,111 +472,111 @@ fn line_height_example(colors: &Colors) -> impl IntoElement { struct TextExample; impl Render for TextExample { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let colors = Colors::for_appearance(window); + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); - div() - .id("main") - .size_full() - .p_4() - .bg(colors.background) - .overflow_scroll() - .child( - div() - .flex() - .flex_col() - .gap_4() - .max_w(px(600.)) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(FontWeight::BOLD) - .text_color(colors.text) - .child("Text & Typography"), - ) - .child( - div().text_sm().text_color(colors.disabled).child( - "Font styling, alignment, overflow, and unicode support", - ), - ), - ) - .child(section(&colors, "Font Sizes", text_sizes_example(&colors))) - .child(section( - &colors, - "Font Weights", - text_weights_example(&colors), - )) - .child(section( - &colors, - "Text Alignment", - text_alignment_example(&colors), - )) - .child(section( - &colors, - "Text Decoration", - text_decoration_example(&colors), - )) - .child(section( - &colors, - "Line Height", - line_height_example(&colors), - )) - .child(section( - &colors, - "Styled Text", - styled_text_example(&colors), - )) - .child(section( - &colors, - "Text Overflow", - text_overflow_example(&colors), - )) - .child(section( - &colors, - "Character Grid", - character_grid_example(&colors), - )), - ) - } + div() + .id("main") + .size_full() + .p_4() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .text_color(colors.text) + .child("Text & Typography"), + ) + .child( + div().text_sm().text_color(colors.disabled).child( + "Font styling, alignment, overflow, and unicode support", + ), + ), + ) + .child(section(&colors, "Font Sizes", text_sizes_example(&colors))) + .child(section( + &colors, + "Font Weights", + text_weights_example(&colors), + )) + .child(section( + &colors, + "Text Alignment", + text_alignment_example(&colors), + )) + .child(section( + &colors, + "Text Decoration", + text_decoration_example(&colors), + )) + .child(section( + &colors, + "Line Height", + line_height_example(&colors), + )) + .child(section( + &colors, + "Styled Text", + styled_text_example(&colors), + )) + .child(section( + &colors, + "Text Overflow", + text_overflow_example(&colors), + )) + .child(section( + &colors, + "Character Grid", + character_grid_example(&colors), + )), + ) + } } fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { - let surface: Hsla = colors.container.into(); + let surface: Hsla = colors.container.into(); - div() - .flex() - .flex_col() - .gap_2() - .p_3() - .bg(surface.opacity(0.5)) - .rounded_lg() - .child( - div() - .text_sm() - .font_weight(FontWeight::SEMIBOLD) - .text_color(colors.text) - .child(title), - ) - .child(content) + div() + .flex() + .flex_col() + .gap_2() + .p_3() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| TextExample), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TextExample), + ) + .expect("Failed to open window"); - init_example(cx, "Text"); - }); + init_example(cx, "Text"); + }); } diff --git a/crates/gpui/examples/learn/timeline.rs b/crates/gpui/examples/learn/timeline.rs index ba7ae6643e..c6dde800d4 100644 --- a/crates/gpui/examples/learn/timeline.rs +++ b/crates/gpui/examples/learn/timeline.rs @@ -15,13 +15,13 @@ // The modules under demo are skeletons whose bodies are `todo!()` by design. #![allow(clippy::todo)] -use gpui::{ - App, Bounds, Context, Entity, Pixels, Render, SharedString, Window, WindowBounds, - WindowOptions, div, prelude::*, px, size, -}; use gpui::timeline::{ - ClipData, ClipId, Frame, FrameRange, FrameRate, TimelineDataSource, TimelineEvent, - TimelineView, TrackData, TrackKind, + ClipData, ClipId, Frame, FrameRange, FrameRate, TimelineDataSource, TimelineEvent, + TimelineView, TrackData, TrackKind, +}; +use gpui::{ + App, Bounds, Context, Entity, Pixels, Render, SharedString, Window, WindowBounds, + WindowOptions, div, prelude::*, px, size, }; #[path = "../shared/prelude.rs"] @@ -30,178 +30,178 @@ mod example_prelude; // --- mock model ------------------------------------------------------------ struct MockClip { - id: ClipId, - range: FrameRange, - media_in: Frame, - label: SharedString, + id: ClipId, + range: FrameRange, + media_in: Frame, + label: SharedString, } impl ClipData for MockClip { - fn id(&self) -> ClipId { - self.id - } + fn id(&self) -> ClipId { + self.id + } - fn range(&self) -> FrameRange { - self.range - } + fn range(&self) -> FrameRange { + self.range + } - fn media_in(&self) -> Frame { - self.media_in - } + fn media_in(&self) -> Frame { + self.media_in + } - fn label(&self) -> SharedString { - self.label.clone() - } + fn label(&self) -> SharedString { + self.label.clone() + } } struct MockTrack { - kind: TrackKind, - name: SharedString, - height: Pixels, - clips: Vec, + kind: TrackKind, + name: SharedString, + height: Pixels, + clips: Vec, } impl TrackData for MockTrack { - type Clip = MockClip; + type Clip = MockClip; - fn kind(&self) -> TrackKind { - self.kind - } + fn kind(&self) -> TrackKind { + self.kind + } - fn name(&self) -> SharedString { - self.name.clone() - } + fn name(&self) -> SharedString { + self.name.clone() + } - fn height(&self) -> Pixels { - self.height - } + fn height(&self) -> Pixels { + self.height + } - fn clips(&self) -> &[Self::Clip] { - &self.clips - } + fn clips(&self) -> &[Self::Clip] { + &self.clips + } } struct MockSequence { - tracks: Vec, + tracks: Vec, } impl MockSequence { - fn demo() -> Self { - let clip = |id: u64, start: i64, end: i64, label: &str| MockClip { - id: ClipId(id), - range: FrameRange::new(Frame(start), Frame(end)), - media_in: Frame::ZERO, - label: label.into(), - }; - MockSequence { - tracks: vec![ - MockTrack { - kind: TrackKind::Video, - name: "V1".into(), - height: px(64.), - clips: vec![clip(1, 0, 240, "opening.mov"), clip(2, 240, 600, "b-roll.mp4")], - }, - MockTrack { - kind: TrackKind::Video, - name: "V2".into(), - height: px(64.), - clips: vec![clip(3, 120, 300, "title.mov")], - }, - MockTrack { - kind: TrackKind::Audio, - name: "A1".into(), - height: px(48.), - clips: vec![clip(4, 0, 600, "dialog.wav")], - }, - MockTrack { - kind: TrackKind::Audio, - name: "A2".into(), - height: px(48.), - clips: vec![clip(5, 0, 480, "score.flac")], - }, - ], - } - } + fn demo() -> Self { + let clip = |id: u64, start: i64, end: i64, label: &str| MockClip { + id: ClipId(id), + range: FrameRange::new(Frame(start), Frame(end)), + media_in: Frame::ZERO, + label: label.into(), + }; + MockSequence { + tracks: vec![ + MockTrack { + kind: TrackKind::Video, + name: "V1".into(), + height: px(64.), + clips: vec![ + clip(1, 0, 240, "opening.mov"), + clip(2, 240, 600, "b-roll.mp4"), + ], + }, + MockTrack { + kind: TrackKind::Video, + name: "V2".into(), + height: px(64.), + clips: vec![clip(3, 120, 300, "title.mov")], + }, + MockTrack { + kind: TrackKind::Audio, + name: "A1".into(), + height: px(48.), + clips: vec![clip(4, 0, 600, "dialog.wav")], + }, + MockTrack { + kind: TrackKind::Audio, + name: "A2".into(), + height: px(48.), + clips: vec![clip(5, 0, 480, "score.flac")], + }, + ], + } + } } impl TimelineDataSource for MockSequence { - type Track = MockTrack; + type Track = MockTrack; - fn frame_rate(&self) -> FrameRate { - FrameRate::NTSC_2997 - } + fn frame_rate(&self) -> FrameRate { + FrameRate::NTSC_2997 + } - fn sequence_length(&self) -> Frame { - Frame(600) - } + fn sequence_length(&self) -> Frame { + Frame(600) + } - fn track_count(&self) -> usize { - self.tracks.len() - } + fn track_count(&self) -> usize { + self.tracks.len() + } - fn track(&self, index: usize) -> Option { - // A real host returns a lightweight snapshot; the mock simply - // reports the track's existence. Returning an owned value here is - // what the trait requires, so the mock clones its clips. - self.tracks.get(index).map(|t| MockTrack { - kind: t.kind, - name: t.name.clone(), - height: t.height, - clips: t - .clips - .iter() - .map(|c| MockClip { - id: c.id, - range: c.range, - media_in: c.media_in, - label: c.label.clone(), - }) - .collect(), - }) - } + fn track(&self, index: usize) -> Option { + // A real host returns a lightweight snapshot; the mock simply + // reports the track's existence. Returning an owned value here is + // what the trait requires, so the mock clones its clips. + self.tracks.get(index).map(|t| MockTrack { + kind: t.kind, + name: t.name.clone(), + height: t.height, + clips: t + .clips + .iter() + .map(|c| MockClip { + id: c.id, + range: c.range, + media_in: c.media_in, + label: c.label.clone(), + }) + .collect(), + }) + } } // --- the example view ------------------------------------------------------ struct TimelineExample { - timeline: Entity>, + timeline: Entity>, } impl TimelineExample { - fn new(model: Entity, window: &mut Window, cx: &mut Context) -> Self { - let timeline = cx.new(|cx| TimelineView::new(model, window, cx).zoom(2.0)); - cx.subscribe( - &timeline, - |_this, _timeline, event: &TimelineEvent, _cx| { - // In Oak, each request becomes an undoable engine command - // here, followed by `model.update(cx, |_, cx| cx.notify())`. - println!("timeline edit request: {event:?}"); - }, - ) - .detach(); - TimelineExample { timeline } - } + fn new(model: Entity, window: &mut Window, cx: &mut Context) -> Self { + let timeline = cx.new(|cx| TimelineView::new(model, window, cx).zoom(2.0)); + cx.subscribe(&timeline, |_this, _timeline, event: &TimelineEvent, _cx| { + // In Oak, each request becomes an undoable engine command + // here, followed by `model.update(cx, |_, cx| cx.notify())`. + println!("timeline edit request: {event:?}"); + }) + .detach(); + TimelineExample { timeline } + } } impl Render for TimelineExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.timeline.clone()) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.timeline.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(1000.), px(480.)), cx); - let model = cx.new(|_cx| MockSequence::demo()); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| TimelineExample::new(model, window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(1000.), px(480.)), cx); + let model = cx.new(|_cx| MockSequence::demo()); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| TimelineExample::new(model, window, cx)), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Timeline"); - }); + example_prelude::init_example(cx, "Timeline"); + }); } diff --git a/crates/gpui/examples/learn/transition.rs b/crates/gpui/examples/learn/transition.rs index d5c0509536..d0cb7dea78 100644 --- a/crates/gpui/examples/learn/transition.rs +++ b/crates/gpui/examples/learn/transition.rs @@ -8,8 +8,8 @@ mod example_prelude; use std::time::Duration; use gpui::{ - AnyElement, App, AppContext, Bounds, Context, ElementId, Lerp, Rgba, Window, WindowBounds, - WindowOptions, actions, div, ease_in_out, prelude::*, px, rgb, size, + AnyElement, App, AppContext, Bounds, Context, ElementId, Lerp, Rgba, Window, WindowBounds, + WindowOptions, actions, div, ease_in_out, prelude::*, px, rgb, size, }; use smallvec::SmallVec; @@ -17,93 +17,93 @@ actions!(app, [Quit]); #[derive(IntoElement)] struct Button { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, + id: ElementId, + children: SmallVec<[AnyElement; 2]>, } impl Button { - fn new(id: impl Into) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - } - } + fn new(id: impl Into) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + } + } } impl ParentElement for Button { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } } impl RenderOnce for Button { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - const HOVER_STRENGTH: f32 = 0.3; - let base_color: Rgba = rgb(0x663399); + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + const HOVER_STRENGTH: f32 = 0.3; + let base_color: Rgba = rgb(0x663399); - let hover_transition = window - .use_keyed_transition( - (self.id.clone(), "hover"), - cx, - Duration::from_millis(300), - |_window, _cx| 0., - ) - .with_easing(ease_in_out); + let hover_transition = window + .use_keyed_transition( + (self.id.clone(), "hover"), + cx, + Duration::from_millis(300), + |_window, _cx| 0., + ) + .with_easing(ease_in_out); - let bg_color = base_color.lerp( - &rgb(0x000), - *hover_transition.evaluate(window, cx) * HOVER_STRENGTH, - ); + let bg_color = base_color.lerp( + &rgb(0x000), + *hover_transition.evaluate(window, cx) * HOVER_STRENGTH, + ); - div() - .id(self.id) - .cursor_pointer() - .rounded(px(100.)) - .pl(px(14.)) - .pr(px(14.)) - .pt(px(10.)) - .pb(px(10.)) - .bg(bg_color) - .text_color(rgb(0x110F15)) - .children(self.children) - .on_hover(move |hover, _window, cx| { - hover_transition.update(cx, |this, cx| { - *this = *hover as u8 as f32; - cx.notify(); - }); - }) - } + div() + .id(self.id) + .cursor_pointer() + .rounded(px(100.)) + .pl(px(14.)) + .pr(px(14.)) + .pt(px(10.)) + .pb(px(10.)) + .bg(bg_color) + .text_color(rgb(0x110F15)) + .children(self.children) + .on_hover(move |hover, _window, cx| { + hover_transition.update(cx, |this, cx| { + *this = *hover as u8 as f32; + cx.notify(); + }); + }) + } } struct TransitionExample; impl Render for TransitionExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .justify_center() - .items_center() - .absolute() - .bg(rgb(0x110F15)) - .gap(px(20.)) - .p(px(100.)) - .child(Button::new("btn").child("Click me!")) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .justify_center() + .items_center() + .absolute() + .bg(rgb(0x110F15)) + .gap(px(20.)) + .p(px(100.)) + .child(Button::new("btn").child("Click me!")) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(500.), px(650.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| TransitionExample), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(500.), px(650.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TransitionExample), + ) + .expect("Failed to open window"); - example_prelude::init_example(cx, "Transition"); - }); + example_prelude::init_example(cx, "Transition"); + }); } diff --git a/crates/gpui/examples/legacy/focus_visible.rs b/crates/gpui/examples/legacy/focus_visible.rs index 21387dae2d..c0acb7e35e 100644 --- a/crates/gpui/examples/legacy/focus_visible.rs +++ b/crates/gpui/examples/legacy/focus_visible.rs @@ -1,214 +1,214 @@ use gpui::{ - App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, - WindowBounds, WindowOptions, actions, div, prelude::*, px, size, + App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, + WindowBounds, WindowOptions, actions, div, prelude::*, px, size, }; actions!(example, [Tab, TabPrev, Quit]); struct Example { - focus_handle: FocusHandle, - items: Vec<(FocusHandle, &'static str)>, - message: SharedString, + 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()", - ), - ]; + 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); + 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.", - ), - } - } + 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(&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_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(); - } + 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) - } + 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(); - })), - ), - ), - ) - } + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - cx.bind_keys([ - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - KeyBinding::new("cmd-q", Quit, None), - ]); + gpui_platform::application().run(|cx: &mut App| { + 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(); + 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); - }); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/legacy/gif_viewer.rs b/crates/gpui/examples/legacy/gif_viewer.rs index ca00688ce6..7124556787 100644 --- a/crates/gpui/examples/legacy/gif_viewer.rs +++ b/crates/gpui/examples/legacy/gif_viewer.rs @@ -2,40 +2,40 @@ use gpui::{App, Context, Render, Window, WindowOptions, div, img, prelude::*}; use std::path::PathBuf; struct GifViewer { - gif_path: PathBuf, + gif_path: PathBuf, } impl GifViewer { - fn new(gif_path: PathBuf) -> Self { - Self { gif_path } - } + 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 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 main() { - env_logger::init(); - gpui_platform::application().run(|cx: &mut App| { - let gif_path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif"); + env_logger::init(); + gpui_platform::application().run(|cx: &mut App| { + 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); - }); + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |_, cx| cx.new(|_| GifViewer::new(gif_path)), + ) + .unwrap(); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/legacy/gradient.rs b/crates/gpui/examples/legacy/gradient.rs index c01ef84f82..591d4c16ba 100644 --- a/crates/gpui/examples/legacy/gradient.rs +++ b/crates/gpui/examples/legacy/gradient.rs @@ -1,257 +1,257 @@ use gpui::{ - App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div, - linear_color_stop, linear_gradient, point, prelude::*, px, size, + App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div, + linear_color_stop, linear_gradient, point, prelude::*, px, size, }; struct GradientViewer { - color_space: ColorSpace, + color_space: ColorSpace, } impl GradientViewer { - fn new() -> Self { - Self { - color_space: ColorSpace::default(), - } - } + 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; + 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), - ); + 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), - ); - }, - ))) - } + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - cx.open_window( - WindowOptions { - focus: true, - ..Default::default() - }, - |_, cx| cx.new(|_| GradientViewer::new()), - ) - .unwrap(); - cx.activate(true); - }); + gpui_platform::application().run(|cx: &mut App| { + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |_, cx| cx.new(|_| GradientViewer::new()), + ) + .unwrap(); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/legacy/hello_world.rs b/crates/gpui/examples/legacy/hello_world.rs index b217c559dd..554205f118 100644 --- a/crates/gpui/examples/legacy/hello_world.rs +++ b/crates/gpui/examples/legacy/hello_world.rs @@ -1,106 +1,106 @@ use gpui::{ - App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px, - rgb, size, + App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px, + rgb, size, }; struct HelloWorld { - text: SharedString, + 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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); - }); + gpui_platform::application().run(|cx: &mut App| { + 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); + }); } diff --git a/crates/gpui/examples/legacy/image_loading.rs b/crates/gpui/examples/legacy/image_loading.rs index 8096354601..6071fbf9c6 100644 --- a/crates/gpui/examples/legacy/image_loading.rs +++ b/crates/gpui/examples/legacy/image_loading.rs @@ -1,213 +1,213 @@ 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, + 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, }; 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 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::>()) - } + 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, + timeout: Duration, + fail: bool, } struct LoadImageWithParameters {} impl Asset for LoadImageWithParameters { - type Source = LoadImageParameters; + type Source = LoadImageParameters; - type Output = Result, ImageCacheError>; + 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 - } - } - } + 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 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); + 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("?"), - ) - } + 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, - }; + 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, - }; + // 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, - }; + 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()); - }) - }), - ), - ) - } + // 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 main() { - env_logger::init(); - gpui_platform::application() - .with_assets(Assets {}) - .run(|cx: &mut App| { - 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(); - }); + env_logger::init(); + gpui_platform::application() + .with_assets(Assets {}) + .run(|cx: &mut App| { + 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(); + }); } diff --git a/crates/gpui/examples/legacy/layer_shell.rs b/crates/gpui/examples/legacy/layer_shell.rs index 49dd6b5348..0dd6e244a8 100644 --- a/crates/gpui/examples/legacy/layer_shell.rs +++ b/crates/gpui/examples/legacy/layer_shell.rs @@ -1,86 +1,86 @@ fn main() { - #[cfg(all(target_os = "linux", feature = "wayland"))] - example::main(); + #[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(all(target_os = "linux", feature = "wayland")))] + panic!("This example requires the `wayland` feature and a linux system."); } #[cfg(all(target_os = "linux", feature = "wayland"))] mod example { - use std::time::{Duration, SystemTime, UNIX_EPOCH}; + 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::{ + App, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds, + WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems, rgba, white, + }; - struct LayerShellExample; + 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(); + 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 - } - } + 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(); + 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; + 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)) - } - } + 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() { - gpui_platform::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(); - }); - } + pub fn main() { + gpui_platform::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/examples/legacy/on_window_close_quit.rs b/crates/gpui/examples/legacy/on_window_close_quit.rs index f8afd18d1d..d4b903dbd7 100644 --- a/crates/gpui/examples/legacy/on_window_close_quit.rs +++ b/crates/gpui/examples/legacy/on_window_close_quit.rs @@ -1,82 +1,82 @@ use gpui::{ - App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions, - div, prelude::*, px, rgb, size, + App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions, + div, prelude::*, px, rgb, size, }; actions!(example, [CloseWindow]); struct ExampleWindow { - focus_handle: FocusHandle, + 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + gpui_platform::application().run(|cx: &mut App| { + 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, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); + cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]); + cx.on_window_closed(|cx, _| { + 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(); + 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; + 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(); - }); + 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(); + }); } diff --git a/crates/gpui/examples/legacy/opacity.rs b/crates/gpui/examples/legacy/opacity.rs index 6381255a00..0168939a38 100644 --- a/crates/gpui/examples/legacy/opacity.rs +++ b/crates/gpui/examples/legacy/opacity.rs @@ -2,175 +2,175 @@ use std::{fs, path::PathBuf}; use anyhow::Result; use gpui::{ - App, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task, Window, - WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg, + App, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task, Window, + WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg, }; struct Assets { - base: PathBuf, + 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 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()) - } + 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, + _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 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(); - } + 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(); - } - } + 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 { - color: hsla(0.0, 0.0, 0.0, 0.5), - blur_radius: px(1.0), - spread_radius: px(5.0), - offset: point(px(10.0), px(10.0)), - inset: false, - }]) - .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()), - ), - ) - } + 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 { + color: hsla(0.0, 0.0, 0.0, 0.5), + blur_radius: px(1.0), + spread_radius: px(5.0), + offset: point(px(10.0), px(10.0)), + inset: false, + }]) + .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 main() { - gpui_platform::application() - .with_assets(Assets { - base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), - }) - .run(|cx: &mut App| { - 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); - }); + gpui_platform::application() + .with_assets(Assets { + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), + }) + .run(|cx: &mut App| { + 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); + }); } diff --git a/crates/gpui/examples/legacy/scrollable.rs b/crates/gpui/examples/legacy/scrollable.rs index ecc0643475..50f5e78652 100644 --- a/crates/gpui/examples/legacy/scrollable.rs +++ b/crates/gpui/examples/legacy/scrollable.rs @@ -3,55 +3,55 @@ use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelu 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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); - }); + gpui_platform::application().run(|cx: &mut App| { + 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); + }); } diff --git a/crates/gpui/examples/legacy/svg/svg.rs b/crates/gpui/examples/legacy/svg/svg.rs index da59af89f7..7ad8d4cceb 100644 --- a/crates/gpui/examples/legacy/svg/svg.rs +++ b/crates/gpui/examples/legacy/svg/svg.rs @@ -3,85 +3,85 @@ use std::path::PathBuf; use anyhow::Result; use gpui::{ - App, AssetSource, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, - prelude::*, px, rgb, size, svg, + App, AssetSource, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, + prelude::*, px, rgb, size, svg, }; struct Assets { - base: PathBuf, + 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 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()) - } + 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 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 main() { - gpui_platform::application() - .with_assets(Assets { - base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), - }) - .run(|cx: &mut App| { - 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); - }); + gpui_platform::application() + .with_assets(Assets { + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), + }) + .run(|cx: &mut App| { + 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); + }); } diff --git a/crates/gpui/examples/legacy/tab_stop.rs b/crates/gpui/examples/legacy/tab_stop.rs index 9c113ce7eb..77a89fcf58 100644 --- a/crates/gpui/examples/legacy/tab_stop.rs +++ b/crates/gpui/examples/legacy/tab_stop.rs @@ -1,155 +1,155 @@ use gpui::{ - App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, - WindowBounds, WindowOptions, actions, div, prelude::*, px, size, + App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, + WindowBounds, WindowOptions, actions, div, prelude::*, px, size, }; actions!(example, [Tab, TabPrev]); struct Example { - focus_handle: FocusHandle, - items: Vec, - message: SharedString, + 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), - ]; + 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); + 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."), - } - } + 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(&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`."); - } + 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 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() - } + 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(); - })), - ), - ) - } + 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(); + })), + ), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.bind_keys([ - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - ]); + gpui_platform::application().run(|cx: &mut App| { + 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(); + 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); - }); + cx.activate(true); + }); } diff --git a/crates/gpui/examples/legacy/tree.rs b/crates/gpui/examples/legacy/tree.rs index 6ab4d4a991..d4d0b7dcb5 100644 --- a/crates/gpui/examples/legacy/tree.rs +++ b/crates/gpui/examples/legacy/tree.rs @@ -7,37 +7,37 @@ use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelu struct Tree {} static DEPTH: LazyLock = LazyLock::new(|| { - std::env::var("GPUI_TREE_DEPTH") - .ok() - .and_then(|depth| depth.parse().ok()) - .unwrap_or_else(|| 50) + 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 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); - }); + gpui_platform::application().run(|cx: &mut App| { + 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(); + }); } diff --git a/crates/gpui/examples/legacy/uniform_list.rs b/crates/gpui/examples/legacy/uniform_list.rs index 2b30b0cb28..8fa141cc8e 100644 --- a/crates/gpui/examples/legacy/uniform_list.rs +++ b/crates/gpui/examples/legacy/uniform_list.rs @@ -1,50 +1,50 @@ use gpui::{ - App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, - uniform_list, + App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, + uniform_list, }; 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; + 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(), - ) - } + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); - }); + gpui_platform::application().run(|cx: &mut App| { + 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(); + }); } diff --git a/crates/gpui/examples/legacy/window.rs b/crates/gpui/examples/legacy/window.rs index 94d2200e7b..ae99131a4e 100644 --- a/crates/gpui/examples/legacy/window.rs +++ b/crates/gpui/examples/legacy/window.rs @@ -1,269 +1,269 @@ use gpui::{ - App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind, - WindowOptions, actions, div, prelude::*, px, rgb, size, + App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind, + WindowOptions, actions, div, prelude::*, px, rgb, size, }; struct SubWindow { - custom_titlebar: bool, + custom_titlebar: 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)) + 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, _: &mut Context) -> impl IntoElement { - 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() - .gap_2() - .child("SubWindow") - .child(button("Close", |window, _| { - window.remove_window(); - })), - ) - } + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { + 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() + .gap_2() + .child("SubWindow") + .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)); + 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, - }) - }, - ) - .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, - }) - }, - ) - .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, - }) - }, - ) - .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, - }) - }, - ) - .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, - }) - }, - ) - .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, - }) - }, - ) - .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, - }) - }, - ) - .unwrap(); - })) - .child(button("Hide Application", |window, cx| { - cx.hide(); + 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, + }) + }, + ) + .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, + }) + }, + ) + .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, + }) + }, + ) + .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, + }) + }, + ) + .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, + }) + }, + ) + .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, + }) + }, + ) + .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, + }) + }, + ) + .unwrap(); + })) + .child(button("Hide Application", |window, cx| { + cx.hide(); - // Restore the application after 3 seconds - window - .spawn(cx, async move |cx| { - smol::Timer::after(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, - ); + // Restore the application after 3 seconds + window + .spawn(cx, async move |cx| { + smol::Timer::after(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(); + })) + .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(); - })) - } + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); + gpui_platform::application().run(|cx: &mut App| { + 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(); + 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(); + WindowDemo {} + }) + }, + ) + .unwrap(); - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - }); + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + }); } diff --git a/crates/gpui/examples/legacy/window_positioning.rs b/crates/gpui/examples/legacy/window_positioning.rs index a383bb1148..ffa5ca8a5b 100644 --- a/crates/gpui/examples/legacy/window_positioning.rs +++ b/crates/gpui/examples/legacy/window_positioning.rs @@ -1,219 +1,219 @@ use gpui::{ - App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window, - WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*, - px, rgb, + App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window, + WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*, + px, rgb, }; struct WindowContent { - text: SharedString, - bounds: Bounds, - bg: Hsla, + 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(); + 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 - )), - ) - } + 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() - } + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - // 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.); + gpui_platform::application().run(|cx: &mut App| { + // 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); + 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, - }; + 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(); - } - }); + 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(); + } + }); } diff --git a/crates/gpui/examples/legacy/window_shadow.rs b/crates/gpui/examples/legacy/window_shadow.rs index 2fc1d618ee..f3af53c39c 100644 --- a/crates/gpui/examples/legacy/window_shadow.rs +++ b/crates/gpui/examples/legacy/window_shadow.rs @@ -1,8 +1,8 @@ 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, + 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, }; struct WindowShadow {} @@ -14,216 +14,216 @@ struct WindowShadow {} // 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); + 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; + 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 { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 0.4, - }, - blur_radius: shadow_size / 2., - spread_radius: px(0.), - offset: point(px(0.0), px(0.0)), - inset: false, - }]) - }), - }) - .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 { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 1.0, - }, - blur_radius: px(20.0), - spread_radius: px(0.0), - offset: point(px(0.0), px(0.0)), - inset: false, - }]) - .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"), - }), - ), - ), - ), - ) - } + 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 { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + blur_radius: shadow_size / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + inset: false, + }]) + }), + }) + .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 { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 1.0, + }, + blur_radius: px(20.0), + spread_radius: px(0.0), + offset: point(px(0.0), px(0.0)), + inset: false, + }]) + .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) + 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 main() { - gpui_platform::application().run(|cx: &mut App| { - 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(); - }); + gpui_platform::application().run(|cx: &mut App| { + 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(); + }); } diff --git a/crates/gpui/examples/shared/prelude.rs b/crates/gpui/examples/shared/prelude.rs index 9135aa423e..4c7e7e5966 100644 --- a/crates/gpui/examples/shared/prelude.rs +++ b/crates/gpui/examples/shared/prelude.rs @@ -17,26 +17,26 @@ actions!(example, [Quit, CloseWindow]); /// - Sets up an app menu with the example name and a Quit action (cmd-q) /// - Configures the app to quit when all windows are closed pub fn init_example(cx: &mut App, name: impl Into) { - // Bring the example window to the front - cx.activate(true); + // Bring the example window to the front + cx.activate(true); - // Define the quit action... - cx.on_action(|_: &Quit, cx| cx.quit()); - // ...then bind it to cmd+q - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + // Define the quit action... + cx.on_action(|_: &Quit, cx| cx.quit()); + // ...then bind it to cmd+q + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - // Set up an app menu with the example name and a Quit action (cmd-q) - cx.set_menus(vec![Menu { - name: name.into(), - items: vec![MenuItem::action("Quit", Quit)], - disabled: false, - }]); + // Set up an app menu with the example name and a Quit action (cmd-q) + cx.set_menus(vec![Menu { + name: name.into(), + items: vec![MenuItem::action("Quit", Quit)], + disabled: false, + }]); - // Quit the app when all windows are closed - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); + // Quit the app when all windows are closed + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); } diff --git a/crates/gpui/examples/text_transform_preview.rs b/crates/gpui/examples/text_transform_preview.rs index 64481fbe9d..adf43017f1 100644 --- a/crates/gpui/examples/text_transform_preview.rs +++ b/crates/gpui/examples/text_transform_preview.rs @@ -1,84 +1,84 @@ use gpui::{ - App, Bounds, Context, FontWeight, Render, TextTransform, Window, WindowBounds, WindowOptions, - div, prelude::*, px, rgb, size, + App, Bounds, Context, FontWeight, Render, TextTransform, Window, WindowBounds, WindowOptions, + div, prelude::*, px, rgb, size, }; struct TextTransformPreview; impl Render for TextTransformPreview { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_4() - .bg(rgb(0x10141c)) - .size(px(720.)) - .p_8() - .text_color(rgb(0xe5e7eb)) - .child( - div() - .text_xl() - .font_weight(FontWeight::BOLD) - .child("Text spacing and transforms"), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .p_4() - .bg(rgb(0x1f2937)) - .rounded_md() - .child(div().text_sm().text_color(rgb(0x9ca3af)).child("Uppercase")) - .child( - div() - .text_2xl() - .font_weight(FontWeight::SEMIBOLD) - .letter_spacing(px(3.)) - .text_transform(TextTransform::Uppercase) - .text_color(rgb(0x93c5fd)) - .child("letter spacing works"), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .p_4() - .bg(rgb(0x1f2937)) - .rounded_md() - .child( - div() - .text_sm() - .text_color(rgb(0x9ca3af)) - .child("Capitalize"), - ) - .child( - div() - .text_2xl() - .letter_spacing(px(1.5)) - .text_transform(TextTransform::Capitalize) - .text_color(rgb(0xfcd34d)) - .child("each word keeps its byte offsets"), - ), - ) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_4() + .bg(rgb(0x10141c)) + .size(px(720.)) + .p_8() + .text_color(rgb(0xe5e7eb)) + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .child("Text spacing and transforms"), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .p_4() + .bg(rgb(0x1f2937)) + .rounded_md() + .child(div().text_sm().text_color(rgb(0x9ca3af)).child("Uppercase")) + .child( + div() + .text_2xl() + .font_weight(FontWeight::SEMIBOLD) + .letter_spacing(px(3.)) + .text_transform(TextTransform::Uppercase) + .text_color(rgb(0x93c5fd)) + .child("letter spacing works"), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .p_4() + .bg(rgb(0x1f2937)) + .rounded_md() + .child( + div() + .text_sm() + .text_color(rgb(0x9ca3af)) + .child("Capitalize"), + ) + .child( + div() + .text_2xl() + .letter_spacing(px(1.5)) + .text_transform(TextTransform::Capitalize) + .text_color(rgb(0xfcd34d)) + .child("each word keeps its byte offsets"), + ), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.activate(true); - let bounds = Bounds::centered(None, size(px(720.), px(480.)), cx); - if let Err(error) = cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| TextTransformPreview), - ) { - eprintln!("failed to open preview window: {error}"); - return; - } - }); + gpui_platform::application().run(|cx: &mut App| { + cx.activate(true); + let bounds = Bounds::centered(None, size(px(720.), px(480.)), cx); + if let Err(error) = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TextTransformPreview), + ) { + eprintln!("failed to open preview window: {error}"); + return; + } + }); } diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index a47ebe69f0..ea4fc06618 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -4,8 +4,8 @@ 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, + any::{Any, TypeId}, + fmt::Display, }; /// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`. @@ -115,150 +115,150 @@ macro_rules! actions { /// register_action!(Paste); /// ``` pub trait Action: Any + Send { - /// Clone the action into a new box - fn boxed_clone(&self) -> Box; + /// 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; + /// 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, 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; + /// 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; + /// 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 - } + /// 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, - { - &[] - } + /// 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 - } + /// 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 - } + /// 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() - } + 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 - } + /// 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, - }, + /// 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(), - } - } + 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}") - } - } - } + 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: HashMap, - 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 + by_name: HashMap<&'static str, ActionData>, + names_by_type_id: HashMap, + 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(), - }; + 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.load_actions(); - this - } + this + } } struct ActionData { - pub build: ActionBuilder, - pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, + 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. @@ -270,141 +270,141 @@ pub struct MacroActionBuilder(pub fn() -> MacroActionData); /// 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>, + 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); - } - } + /// 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 \ + 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. \ + ); + } + 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); - } - } + ); + } + 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:?}"))?; + /// 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)?) - } + Ok(self.build_action(name, None)?) + } - /// 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, - } - }) - } + /// 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 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_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 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 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 deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { + &self.deprecation_messages + } - pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { - &self.documentation - } + pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { + &self.documentation + } } /// Generate a list of all the registered actions. @@ -412,42 +412,42 @@ impl ActionRegistry { /// 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()) + inventory::iter:: + .into_iter() + .map(|builder| builder.0()) } mod no_action { - use crate as gpui; - use schemars::JsonSchema; - use serde::Deserialize; + 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 - ] - ); + 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); + /// 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 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::() - } + /// 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/src/app.rs b/crates/gpui/src/app.rs index 594d68bf01..5f980ecb47 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,22 +1,22 @@ use scheduler::Instant; use std::{ - any::{TypeId, type_name}, - cell::{BorrowMutError, Cell, Ref, RefCell, RefMut}, - marker::PhantomData, - mem, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::{Arc, atomic::Ordering::SeqCst}, - time::Duration, + any::{TypeId, type_name}, + cell::{BorrowMutError, Cell, Ref, RefCell, RefMut}, + 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}, + Future, FutureExt, + channel::oneshot, + future::{LocalBoxFuture, Shared}, }; use itertools::Itertools; use parking_lot::RwLock; @@ -41,18 +41,18 @@ 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, CursorStyle, - DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, - HapticFeedbackStyle, 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, Task, TextRenderingMode, TextSystem, - ThermalState, Window, WindowAppearance, WindowButtonLayout, WindowHandle, WindowId, - WindowInvalidator, - colors::{Colors, GlobalColors}, - hash, init_app_menus, + Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Arena, + ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, + DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, + HapticFeedbackStyle, 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, Task, TextRenderingMode, TextSystem, + ThermalState, Window, WindowAppearance, WindowButtonLayout, WindowHandle, WindowId, + WindowInvalidator, + colors::{Colors, GlobalColors}, + hash, init_app_menus, }; mod async_context; @@ -74,39 +74,39 @@ pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100); /// Strongly consider removing after stabilization. #[doc(hidden)] pub struct AppCell { - app: RefCell, + 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(&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 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)] + #[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)] @@ -114,12 +114,12 @@ impl AppCell { 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:?}"); - } - } + 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)] @@ -127,12 +127,12 @@ impl Drop for AppRef<'_> { 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:?}"); - } - } + 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. @@ -142,119 +142,119 @@ pub struct Application(Rc); /// 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 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 - } + /// 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 - } + /// 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 + } - /// 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 - } + /// 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 - } + /// 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. 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); + })); + } - /// 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 - } + /// 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 - } + /// 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 [`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 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 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) - } + /// 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>; + Box bool + 'static>; type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; type WindowClosedHandler = Box; type ReleaseListener = Box; @@ -263,13 +263,13 @@ type NewEntityListener = Box, &mut /// 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, + /// 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. @@ -278,2563 +278,2563 @@ pub enum QuitMode { /// 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, + /// 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, + 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(), - } - } + /// 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>, + 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(), - } - } + /// 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()); - } + /// 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 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)); + /// 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(); + 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) - }) - } + 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)); + /// 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 - }; + 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) - }) - } + 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)) - } + /// 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); - } - } + /// 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) - } + /// 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); - } + /// 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 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 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); + /// 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; - } + 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; - } - } - } - } + 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; - }; + /// 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 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; - } - } + 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); - } - } + 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; + /// 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() - }); + 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 - } + 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::(); + /// 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]); - } - } + 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; - }; + /// 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(); + 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); - } + 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); - } + 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; - }; + /// 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 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(); - }); - } + 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; - }; + /// 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 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(); - }); - } + 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, + #[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, - } - } + #[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, - } - } + #[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, + } + } } /// 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) this: Weak, + pub(crate) platform: Rc, + text_system: Arc, - pub(crate) actions: Rc, - pub(crate) active_drag: Option, - pub(crate) background_executor: BackgroundExecutor, - pub(crate) foreground_executor: ForegroundExecutor, - 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: - FxHashMap>>, - pending_effects: VecDeque, + pub(crate) actions: Rc, + pub(crate) active_drag: Option, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + 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: + FxHashMap>>, + 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) 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>, + 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) 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, + /// 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: FxHashMap>, + // 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: FxHashMap>, - // assets - pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, - asset_source: Arc, - pub(crate) svg_renderer: SvgRenderer, - http_client: Arc, + // 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: FxHashSet, - pub(crate) restart_path: Option, - 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>, + // below is plain data, the drop order is insignificant here + pub(crate) pending_notifications: FxHashSet, + pub(crate) pending_global_notifications: FxHashSet, + pub(crate) restart_path: Option, + 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, - /// 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, + pub(crate) window_update_stack: Vec, + pub(crate) mode: GpuiMode, + pub(crate) cursor_hide_mode: CursorHideMode, + /// 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>, + // 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" - ); - - 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, - background_executor, - foreground_executor, - svg_renderer: SvgRenderer::new(asset_source.clone()), - loading_assets: Default::default(), - asset_source, - http_client, - globals_by_type: FxHashMap::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: FxHashMap::default(), - pending_effects: VecDeque::new(), - pending_notifications: FxHashSet::default(), - pending_global_notifications: FxHashSet::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(), - global_observers: SubscriberSet::new(), - quit_observers: SubscriberSet::new(), - restart_observers: SubscriberSet::new(), - restart_path: None, - 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(), - 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_quit(Box::new({ - let cx = Rc::downgrade(&app); - move || { - if let Some(cx) = cx.upgrade() { - cx.borrow_mut().shutdown(); - } - } - })); - - 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. Handlers registered with [`Context::on_app_quit`] - /// will be given 100ms to complete before exiting. - 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); - if self - .foreground_executor - .block_with_timeout(SHUTDOWN_TIMEOUT, futures) - .is_err() - { - log::error!("timed out waiting on app_will_quit"); - } - - 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() - } - - /// 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; - } - - /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. - /// A globally-callable equivalent to `Context::emit` without requiring an entity update. - pub fn emit(&mut self, entity: &Entity, event: EventType) - where - EntityType: EventEmitter, - EventType: 'static, - { - let event = self - .event_arena - .alloc(|| event) - .map(|it| it as &mut dyn Any); - self.pending_effects.push_back(Effect::Emit { - emitter: entity.entity_id(), - event_type: TypeId::of::(), - event, - }); - } - - /// 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() - } - - /// Register additional GPU device requirements (extra features and/or - /// limits) before opening any windows. The `Box` must contain a - /// `gpui_wgpu::WgpuDeviceRequirements`. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub fn set_gpu_requirements(&self, requirements: Box) { - self.platform.set_gpu_requirements(requirements); - } - - /// Sets the label applied to credentials stored in the system keyring. - /// Call before writing credentials. Only Linux/FreeBSD apply the label. - pub fn set_keyring_label(&self, label: impl Into) { - self.platform.set_keyring_label(label.into()); - } - - /// 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.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 - } - - /// Returns the appearance of the application's windows. - pub fn window_appearance(&self) -> WindowAppearance { - self.platform.window_appearance() - } - - /// Whether the current platform supports haptic feedback. - pub fn supports_haptic_feedback(&self) -> bool { - self.platform.supports_haptic_feedback() - } - - /// Play a haptic feedback of the given style. - /// - /// Must be called from the main thread. This is a no-op on platforms that - /// do not support haptic feedback. Styles correspond to - /// [`NSHapticFeedbackPattern`](https://developer.apple.com/documentation/appkit/nshapticfeedbackmanager/feedbackpattern) - /// values on macOS. - pub fn play_haptic_feedback(&self, style: HapticFeedbackStyle) { - self.platform.play_haptic_feedback(style) - } - - /// 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() - } - - /// 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) - } - - /// 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()) - } - - /// 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"))] - 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()) - .unwrap(); - } - - if self.pending_effects.is_empty() { - 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, _| { - if window.focus == Some(handle_id) { - window.blur(); - } - }) - .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.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 - } - - /// 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| { - if window.pending_input_keystrokes().is_some() { - window.clear_pending_keystrokes(); - window.pending_input_changed(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; - window.refresh(); - true - } else { - false - } - } - - /// 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); - } - - /// 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()))); - } + #[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" + ); + + 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, + background_executor, + foreground_executor, + svg_renderer: SvgRenderer::new(asset_source.clone()), + loading_assets: Default::default(), + asset_source, + http_client, + globals_by_type: FxHashMap::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: FxHashMap::default(), + pending_effects: VecDeque::new(), + pending_notifications: FxHashSet::default(), + pending_global_notifications: FxHashSet::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(), + global_observers: SubscriberSet::new(), + quit_observers: SubscriberSet::new(), + restart_observers: SubscriberSet::new(), + restart_path: None, + 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(), + 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_quit(Box::new({ + let cx = Rc::downgrade(&app); + move || { + if let Some(cx) = cx.upgrade() { + cx.borrow_mut().shutdown(); + } + } + })); + + 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. Handlers registered with [`Context::on_app_quit`] + /// will be given 100ms to complete before exiting. + 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); + if self + .foreground_executor + .block_with_timeout(SHUTDOWN_TIMEOUT, futures) + .is_err() + { + log::error!("timed out waiting on app_will_quit"); + } + + 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() + } + + /// 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; + } + + /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. + /// A globally-callable equivalent to `Context::emit` without requiring an entity update. + pub fn emit(&mut self, entity: &Entity, event: EventType) + where + EntityType: EventEmitter, + EventType: 'static, + { + let event = self + .event_arena + .alloc(|| event) + .map(|it| it as &mut dyn Any); + self.pending_effects.push_back(Effect::Emit { + emitter: entity.entity_id(), + event_type: TypeId::of::(), + event, + }); + } + + /// 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() + } + + /// Register additional GPU device requirements (extra features and/or + /// limits) before opening any windows. The `Box` must contain a + /// `gpui_wgpu::WgpuDeviceRequirements`. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn set_gpu_requirements(&self, requirements: Box) { + self.platform.set_gpu_requirements(requirements); + } + + /// Sets the label applied to credentials stored in the system keyring. + /// Call before writing credentials. Only Linux/FreeBSD apply the label. + pub fn set_keyring_label(&self, label: impl Into) { + self.platform.set_keyring_label(label.into()); + } + + /// 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.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 + } + + /// Returns the appearance of the application's windows. + pub fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } + + /// Whether the current platform supports haptic feedback. + pub fn supports_haptic_feedback(&self) -> bool { + self.platform.supports_haptic_feedback() + } + + /// Play a haptic feedback of the given style. + /// + /// Must be called from the main thread. This is a no-op on platforms that + /// do not support haptic feedback. Styles correspond to + /// [`NSHapticFeedbackPattern`](https://developer.apple.com/documentation/appkit/nshapticfeedbackmanager/feedbackpattern) + /// values on macOS. + pub fn play_haptic_feedback(&self, style: HapticFeedbackStyle) { + self.platform.play_haptic_feedback(style) + } + + /// 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() + } + + /// 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) + } + + /// 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()) + } + + /// 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"))] + 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()) + .unwrap(); + } + + if self.pending_effects.is_empty() { + 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, _| { + if window.focus == Some(handle_id) { + window.blur(); + } + }) + .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.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 + } + + /// 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| { + if window.pending_input_keystrokes().is_some() { + window.clear_pending_keystrokes(); + window.pending_input_changed(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; + window.refresh(); + true + } else { + false + } + } + + /// 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); + } + + /// 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())); + /// 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.push_effect(Effect::EntityCreated { + entity: handle.into_any(), + tid: TypeId::of::(), + window: cx.window_update_stack.last().cloned(), + }); - cx.entities.insert(slot, entity) - }) - } + cx.entities.insert(slot, entity) + }) + } - fn reserve_entity(&mut self) -> Reservation { - Reservation(self.entities.reserve()) - } + 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) - }) - } + 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 - }) - } + /// 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 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 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 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 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"); + 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"))?; + 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)) - } + Ok(read(view, self)) + } - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } + 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) - } + 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, - }, + 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), - } - } + 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, + global: Box, + global_type: PhantomData, } impl GlobalLease { - fn new(global: Box) -> Self { - GlobalLease { - global, - global_type: PhantomData, - } - } + fn new(global: Box) -> Self { + GlobalLease { + global, + global_type: PhantomData, + } + } } impl Deref for GlobalLease { - type Target = G; + type Target = G; - fn deref(&self) -> &Self::Target { - self.global.downcast_ref().unwrap() - } + 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() - } + 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 view used to render this drag + pub view: AnyView, - /// The value of the dragged item, to be dropped - pub value: Arc, + /// 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, + /// 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, + /// The cursor style to use while dragging + pub cursor_style: Option, } /// 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 view used to display the tooltip + pub view: AnyView, - /// The absolute position of the mouse when the tooltip was deployed. - pub mouse_position: Point, + /// 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>, + /// 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 keystroke that occurred + pub keystroke: Keystroke, - /// The action that was resolved for the keystroke, if any - pub action: Option>, + /// The action that was resolved for the keystroke, if any + pub action: Option>, - /// The context stack at the time - pub context_stack: Vec, + /// The context stack at the time + pub context_stack: Vec, } /// A mutable reference to an entity owned by GPUI pub struct GpuiBorrow<'a, T> { - inner: Option>, - app: &'a mut App, + 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, - } - } + 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() - } + 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() - } + 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; + type Target = T; - fn deref(&self) -> &Self::Target { - self.inner.as_ref().unwrap() - } + 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() - } + 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(); - } + 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::RefCell, rc::Rc}; + use std::{cell::RefCell, rc::Rc}; - use crate::{AppContext, TestAppContext}; + use crate::{AppContext, TestAppContext}; - #[test] - fn test_gpui_borrow() { - let cx = TestAppContext::single(); - let observation_count = Rc::new(RefCell::new(0)); + #[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(); + 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 - }); + 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| { + // 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); - }); + cx.update(|cx| { + state.write(cx, false); + }); - assert_eq!(*observation_count.borrow(), 2); - } + assert_eq!(*observation_count.borrow(), 2); + } } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 6c5246f33d..6788fdbca6 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -1,8 +1,8 @@ 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, + 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}; @@ -20,516 +20,516 @@ use super::{Context, WeakEntity}; /// 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, + 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") - } + 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 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 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 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 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 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 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 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 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) - } + 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) - } + #[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)) - } + 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(); - lock.refresh_windows(); - } + /// Schedules all windows in the application to be redrawn. + pub fn refresh(&self) { + let app = self.app(); + let mut lock = app.borrow_mut(); + lock.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 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 - } + /// 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) - } + /// 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) - } + /// 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) - } + /// 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()) - } + /// 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::() - } + /// 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. + /// + /// 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. + /// + /// 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) - } + /// 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)) - } + /// 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(); - }) - } + /// 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, + #[deref] + #[deref_mut] + app: AsyncApp, + window: AnyWindowHandle, } impl AsyncWindowContext { - pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self { - Self { app, window } - } + 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 - } + /// 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(&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 [`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 [`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::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)) - }) - } + /// 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()) - } + /// 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) - } + /// 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 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 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 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 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 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 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 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 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) - } + 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) - } + #[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) - } + 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; + type Result = Result; - fn window_handle(&self) -> AnyWindowHandle { - self.window - } + 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 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 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 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); - }) - } + 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/src/app/context.rs b/crates/gpui/src/app/context.rs index 30675a1733..169ce6f44c 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -1,875 +1,875 @@ use crate::{ - AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, EntityId, EventEmitter, - FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Priority, Reservation, - SubscriberSet, Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle, + AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, 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, + 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, + app: &'a mut App, + entity_state: WeakEntity, } impl<'a, T> ops::Deref for Context<'a, T> { - type Target = App; + type Target = App; - fn deref(&self) -> &Self::Target { - self.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 - } + 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 } - } + 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 - } + /// 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 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() - } + /// 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 - } - }) - } + /// 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)) - }) - } + /// 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 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)) - }) - } + /// 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 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 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 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(); - }) - } + /// 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() - }) - } + /// 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); - } + /// 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) - } + /// 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 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)) - } - } + /// 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(); - }) - } + /// 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); - } + /// 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))); - } + /// 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(); - }); - }); - } + /// 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 - }), - ) - } + /// 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 - }), - ), - ) - } + /// 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 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 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 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 - } + /// 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 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 - } + /// 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 - } + /// 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 - } - }), - ) - } + 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 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 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 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 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 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 - } + /// 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. + /// 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) - } + /// 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 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(); - }); - } + /// 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) - }) - } + /// 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, - { - self.app.emit(&self.entity(), event); - } + /// 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, + { + self.app.emit(&self.entity(), 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 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 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 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 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 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 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 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 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 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 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) - } + #[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 - } + fn borrow(&self) -> &App { + self.app + } } impl BorrowMut for Context<'_, T> { - fn borrow_mut(&mut self) -> &mut App { - self.app - } + fn borrow_mut(&mut self) -> &mut App { + self.app + } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index e4e9f3b58a..15b2c6f849 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -5,18 +5,18 @@ 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, + 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; @@ -25,218 +25,218 @@ use crate::util::atomic_incr_if_not_zero; use collections::HashMap; slotmap::new_key_type! { - /// A unique identifier for a entity across the application. - pub struct EntityId; + /// 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)) - } + 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 [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() - } + /// 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()) - } + 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>, + 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, + 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(), - }, - })), - } - } + 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() - } + #[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() - } + /// 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) - } + /// 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))) - } + /// 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); + /// 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 - } + 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); + /// 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, - } - } + 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()); - } + /// 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); + 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")) - } + 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" - ); - } + 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 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 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(); + 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() - } + 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::() - ) + 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, + entity: Option>, + pub id: EntityId, + entity_type: PhantomData, } impl core::ops::Deref for Lease { - type Target = T; + type Target = T; - fn deref(&self) -> &Self::Target { - self.entity.as_ref().unwrap().downcast_ref().unwrap() - } + 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() - } + 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") - } - } + fn drop(&mut self) { + if self.entity.is_some() && !panicking() { + panic!("Leases must be ended with EntityMap::end_lease") + } + } } #[derive(Deref, DerefMut)] @@ -244,622 +244,622 @@ 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, + 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, - } - } + 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 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 - } + /// 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 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) - } - } + /// 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."); - } + 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), - } - } + 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); - } - } + 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) - } - } + #[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 - } + #[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); - } + #[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 - } + #[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) - } + #[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)) - } + #[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() - } + 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>, + #[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, - } - } + #[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 - } + /// 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, - } - } + /// 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 - } + /// 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) - } + /// 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) - } + /// 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 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. + #[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. + 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) - } + /// 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, - } - } + #[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() - } + 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); - } + #[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 - } + #[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() - } + #[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()) - } + #[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)) - } + #[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>, + 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 - } + /// 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 - } + /// 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)?; + /// 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); + 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), - }) - } + 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); + /// 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." - ) - } - } + 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); + /// 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(), - } - } + 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() - } + 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 - } + #[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); - } + #[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 - } + #[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) - } + #[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)) - } + #[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>, + #[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() - } + 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, - } - } + 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, - }) - } + /// 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. 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") - } + /// 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)) - } + /// 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, - } - } + /// 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); - } + #[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 - } + #[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() - } + #[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()) - } + #[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)) - } + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } /// Controls whether backtraces are captured when entity handles are created. @@ -868,7 +868,7 @@ impl PartialOrd for WeakEntity { /// 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())); + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); /// Unique identifier for a specific entity handle instance. /// @@ -877,7 +877,7 @@ static LEAK_BACKTRACE: std::sync::LazyLock = #[cfg(any(test, feature = "leak-detection"))] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] pub(crate) struct HandleId { - id: u64, + id: u64, } /// Tracks entity handle allocations to detect leaks. @@ -931,8 +931,8 @@ pub(crate) struct HandleId { /// - `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, + next_handle_id: u64, + entity_handles: HashMap, } /// A snapshot of the set of alive entities at a point in time. @@ -942,179 +942,179 @@ pub(crate) struct LeakDetector { /// handles remain between the snapshot and the current state. #[cfg(any(test, feature = "leak-detection"))] pub struct LeakDetectorSnapshot { - entity_ids: collections::HashSet, + entity_ids: collections::HashSet, } #[cfg(any(test, feature = "leak-detection"))] struct EntityLeakData { - handles: HashMap>, - type_name: &'static str, + 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 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(); - } - } - } + /// 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 _; + /// 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); - } - } + 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(), - } - } + /// 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 _; + /// 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!( + 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}"); - } - } + 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; + fn drop(&mut self) { + use std::fmt::Write; - if self.entity_handles.is_empty() || std::thread::panicking() { - return; - } + 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!( + 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}"); - } + } + } + } + panic!("Exited with leaked handles:\n{out}"); + } } #[cfg(any(test, feature = "leak-detection"))] @@ -1122,157 +1122,157 @@ 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}; + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + use backtrace::{BacktraceFmt, BytesOrWideString, PrintFmt}; - let style = if fmt.alternate() { - PrintFmt::Full - } else { - PrintFmt::Short - }; + 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) - }; + // 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(()) - } + 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; + use crate::EntityMap; - struct TestEntity { - pub i: i32, - } + 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(); + #[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: 1 }); - let slot = entity_map.reserve::(); - entity_map.insert(slot, TestEntity { i: 2 }); + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 2 }); - let dropped = entity_map.take_dropped(); - assert_eq!(dropped.len(), 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], - ); - } + 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(); + #[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 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 strong = weak.upgrade(); + assert_eq!(strong, None); - let dropped = entity_map.take_dropped(); - assert_eq!(dropped.len(), 1); + 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], - ); - } + 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(); + #[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 slot = entity_map.reserve::(); + let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); - let snapshot = entity_map.leak_detector_snapshot(); + let snapshot = entity_map.leak_detector_snapshot(); - let slot = entity_map.reserve::(); - let temporary = entity_map.insert(slot, TestEntity { i: 2 }); - drop(temporary); + let slot = entity_map.reserve::(); + let temporary = entity_map.insert(slot, TestEntity { i: 2 }); + drop(temporary); - entity_map.assert_no_new_leaks(&snapshot); + entity_map.assert_no_new_leaks(&snapshot); - drop(pre_existing); - } + 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(); + #[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 slot = entity_map.reserve::(); + let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); - let snapshot = entity_map.leak_detector_snapshot(); + let snapshot = entity_map.leak_detector_snapshot(); - let slot = entity_map.reserve::(); - let leaked = entity_map.insert(slot, TestEntity { i: 2 }); + 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); + // `leaked` is still alive, so this should panic. + entity_map.assert_no_new_leaks(&snapshot); - drop(pre_existing); - drop(leaked); - } + drop(pre_existing); + drop(leaked); + } } diff --git a/crates/gpui/src/app/headless_app_context.rs b/crates/gpui/src/app/headless_app_context.rs index d8a9b1d9f2..ba0dd31e92 100644 --- a/crates/gpui/src/app/headless_app_context.rs +++ b/crates/gpui/src/app/headless_app_context.rs @@ -9,11 +9,11 @@ //! 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}, + 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; @@ -36,249 +36,249 @@ use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// ); /// ``` 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, + /// 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 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 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); + /// 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 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 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 = crate::http_client::FakeHttpClient::with_404_response(); - let app = App::new_app(platform, asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); + let text_system = Arc::new(TextSystem::new(platform_text_system)); + let http_client = crate::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, - } - } + 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}; + /// 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 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, - ) - } + 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(); - } + /// 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); - } + /// 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(); - } + /// 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(); - } + /// 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 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) - } + /// 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())? - } + /// 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 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 background executor. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } - /// Returns the foreground executor. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_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(); - } + 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 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 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 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 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 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 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 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 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 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 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) - } + 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/src/app/test_app.rs b/crates/gpui/src/app/test_app.rs index ef0f9be654..5d9bb49650 100644 --- a/crates/gpui/src/app/test_app.rs +++ b/crates/gpui/src/app/test_app.rs @@ -25,11 +25,11 @@ //! ``` 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, + 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}; @@ -38,570 +38,570 @@ use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// 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, + 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. + 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 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 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) - } + /// 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 = crate::http_client::FakeHttpClient::with_404_response(); - let text_system = Arc::new(TextSystem::new( - platform_text_system.unwrap_or_else(|| platform.text_system.clone()), - )); + 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 = crate::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(); + 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, - } - } + 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 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) - } + /// 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)) - } + /// 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)) - } + /// 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)) - } + /// 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() - }); + /// 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(), - } - } + 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() - }); + /// 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(), - } - } + 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(); - } + /// 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); - } + /// 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 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) - } + /// 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 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 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 foreground executor. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } - /// Get the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } + /// 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::()) - } + /// 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)); - } + /// 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)) - } + /// 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)) - } + /// 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 + // Platform simulation methods - /// Write text to the simulated clipboard. - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.platform.write_to_clipboard(item); - } + /// 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() - } + /// 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() - } + /// 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() - } + /// 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); - } + /// 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() - } + /// 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); - } + /// 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()) - } + /// Get all open windows. + pub fn windows(&self) -> Vec { + self.read(|cx| cx.windows()) + } } impl Default for TestApp { - fn default() -> Self { - Self::new() - } + 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, + handle: WindowHandle, + app: Rc, + platform: Rc, + background_executor: BackgroundExecutor, } impl TestAppWindow { - /// Get the window handle. - pub fn handle(&self) -> WindowHandle { - self.handle - } + /// 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") - } + /// 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 - } + /// 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) - } + /// 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() - } + /// 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 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 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 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 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 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 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 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 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 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(); - } + /// 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(); - }) - .unwrap(); - } + /// 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(); + }) + .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(), - } - } + 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::*}; + use super::*; + use crate::{FocusHandle, Focusable, div, prelude::*}; - struct Counter { - count: usize, - focus_handle: FocusHandle, - } + 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, - } - } + 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; - } - } + 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 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)) - } - } + 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(); + #[test] + fn test_basic_usage() { + let mut app = TestApp::new(); - let mut window = app.open_window(Counter::new); + let mut window = app.open_window(Counter::new); - window.update(|counter, _window, cx| { - counter.increment(cx); - }); + window.update(|counter, _window, cx| { + counter.increment(cx); + }); - window.read(|counter, _| { - assert_eq!(counter.count, 1); - }); + window.read(|counter, _| { + assert_eq!(counter.count, 1); + }); - drop(window); - app.update(|cx| cx.shutdown()); - } + drop(window); + app.update(|cx| cx.shutdown()); + } - #[test] - fn test_entity_creation() { - let mut app = TestApp::new(); + #[test] + fn test_entity_creation() { + let mut app = TestApp::new(); - let entity = app.new_entity(|cx| Counter { - count: 42, - focus_handle: cx.focus_handle(), - }); + 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.read_entity(&entity, |counter, _| { + assert_eq!(counter.count, 42); + }); - app.update_entity(&entity, |counter, _cx| { - counter.count += 1; - }); + app.update_entity(&entity, |counter, _cx| { + counter.count += 1; + }); - app.read_entity(&entity, |counter, _| { - assert_eq!(counter.count, 43); - }); - } + app.read_entity(&entity, |counter, _| { + assert_eq!(counter.count, 43); + }); + } - #[test] - fn test_globals() { - let mut app = TestApp::new(); + #[test] + fn test_globals() { + let mut app = TestApp::new(); - struct MyGlobal(String); - impl Global for MyGlobal {} + struct MyGlobal(String); + impl Global for MyGlobal {} - assert!(!app.has_global::()); + assert!(!app.has_global::()); - app.set_global(MyGlobal("hello".into())); + app.set_global(MyGlobal("hello".into())); - assert!(app.has_global::()); + assert!(app.has_global::()); - app.read_global::(|global, _| { - assert_eq!(global.0, "hello"); - }); + app.read_global::(|global, _| { + assert_eq!(global.0, "hello"); + }); - app.update_global::(|global, _| { - global.0 = "world".into(); - }); + app.update_global::(|global, _| { + global.0 = "world".into(); + }); - app.read_global::(|global, _| { - assert_eq!(global.0, "world"); - }); - } + app.read_global::(|global, _| { + assert_eq!(global.0, "world"); + }); + } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 950eca57aa..edf4094653 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,705 +1,705 @@ 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, Size, Task, TestDispatcher, TestPlatform, - TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds, - WindowHandle, WindowOptions, app::GpuiMode, window::ElementArenaScope, + 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, Size, 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, + 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, + #[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 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 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 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 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 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 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 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 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 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 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) - } + 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 = crate::http_client::FakeHttpClient::with_404_response(); - let text_system = Arc::new(TextSystem::new(platform.text_system())); + /// 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 = crate::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(); + 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())), - } - } + 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 }; - } + /// 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) - } + /// 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 - } + /// 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() - } + /// 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) - } + /// 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(); - } + /// 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)); - } + /// 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(()) - } + /// 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 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 - } + /// 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 `&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) - } + /// 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(); + /// 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() - } + // 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() - } + /// 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 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(); + /// 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) - } + // 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 - } + /// 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 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 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 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); - } + /// 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() - } + /// 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); - } + /// 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 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() - } + /// 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() - } + /// 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() + } - /// 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); - } + /// 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> { - let (tx, rx) = futures::channel::oneshot::channel(); - self.test_platform.expect_restart.borrow_mut().replace(tx); - rx - } + /// Returns true if there's an alert dialog open. + pub fn expect_restart(&self) -> oneshot::Receiver> { + 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); - } + /// 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() - } + /// 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())) - } + /// 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::() - } + /// 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 + /// 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)) - } + /// 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)) - } + /// 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)) - } + /// 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(), - } - } + /// 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(); - } + /// 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(); + /// 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() - } + 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); - } + /// 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() - } + 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); - } + /// 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() - } + 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(); - } + /// 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 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 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 - } + /// 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); + /// 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 _; + use futures::FutureExt as _; + use futures_concurrency::future::Race as _; - ( - async { - loop { - if entity.update(self, &mut predicate) { - return Ok(()); - } + ( + 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(); - } + 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)) - } + /// 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()); - } - }) - }); + /// 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 - } - } + 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 _}; + /// 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(); - }); + 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); + 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); - } - } + 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 _}; + /// 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 (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 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(); + 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; - } - } + 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); - } - } + rx.recv() + .await + .expect("view dropped with pending condition"); + } + drop(subscriptions); + } + } } use derive_more::{Deref, DerefMut}; @@ -709,457 +709,457 @@ use super::{Context, Entity}; /// 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, + #[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() - } + /// 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, - } - } + /// 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(); - } + /// 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) - } + /// 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 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() - } + /// 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 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 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 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 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 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 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 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 }, - }) - } + /// 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) - } + /// 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()) - } + /// 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); + /// 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::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::Paint); + let (request_layout_state, prepaint_state) = element.paint(window, cx); - window.invalidator.set_phase(DrawPhase::None); - window.refresh(); + window.invalidator.set_phase(DrawPhase::None); + window.refresh(); - drop(element); - cx.element_arena.borrow_mut().clear(); + drop(element); + cx.element_arena.borrow_mut().clear(); - (request_layout_state, prepaint_state) - }) - } + (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(); - } + /// 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 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 - } - } + /// 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 - } + /// 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 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 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 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 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 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 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 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 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 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 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) - } + 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; + type Result = T; - /// Get the underlying window handle underlying this context. - fn window_handle(&self) -> AnyWindowHandle { - self.window - } + /// 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 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 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 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") - } + 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() - } + /// 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, TestAppContext}; - use std::path::PathBuf; + use crate::{PathPromptOptions, TestAppContext}; + use std::path::PathBuf; - #[gpui::test] - async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) { - assert!(!cx.did_prompt_for_paths()); + #[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 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 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)); - } + 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, - }) - }); + #[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); + cx.simulate_path_prompt_response(|_options| None); - let response = receiver.await.unwrap().unwrap(); - assert_eq!(response, None); - } + let response = receiver.await.unwrap().unwrap(); + assert_eq!(response, None); + } } diff --git a/crates/gpui/src/app/visual_test_context.rs b/crates/gpui/src/app/visual_test_context.rs index b7d0e1e5f9..7278f53752 100644 --- a/crates/gpui/src/app/visual_test_context.rs +++ b/crates/gpui/src/app/visual_test_context.rs @@ -1,9 +1,9 @@ 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, + 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; @@ -19,466 +19,466 @@ use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// 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, + /// 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 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); + /// 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)); + // 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(); + // 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 text_system = Arc::new(TextSystem::new(platform.text_system())); - let http_client = crate::http_client::FakeHttpClient::with_404_response(); + let http_client = crate::http_client::FakeHttpClient::with_404_response(); - let mut app = App::new_app(platform.clone(), asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); + 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, - } - } + 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}; + /// 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 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, - ) - } + 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) - } + /// 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 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 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 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() - } + /// 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(); - } + /// 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); - } + /// 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) - } + /// 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) - } + /// 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) - } + /// 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) - } + /// 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::() - } + /// 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) - } + /// 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); - } + /// 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 - }) - } + /// 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(); - } + /// 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(); - } + /// 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 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 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 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 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 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(); - } + /// 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(); - } + /// 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); - } + /// 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() - } + /// 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(()); - } - } + /// 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")); - } + 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; - } - } + 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())? - } + /// 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(); - } + /// 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 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 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 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 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 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 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 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 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 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 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) - } + 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/src/arena.rs b/crates/gpui/src/arena.rs index 9898c8056a..21adb1d682 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -1,289 +1,289 @@ use std::{ - alloc::{self, handle_alloc_error}, - cell::Cell, - num::NonZeroUsize, - ops::{Deref, DerefMut}, - ptr::{self, NonNull}, - rc::Rc, + 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), + value: *mut u8, + drop: unsafe fn(*mut u8), } impl Drop for ArenaElement { - #[inline(always)] - fn drop(&mut self) { - unsafe { (self.drop)(self.value) }; - } + #[inline(always)] + fn drop(&mut self) { + unsafe { (self.drop)(self.value) }; + } } struct Chunk { - start: *mut u8, - end: *mut u8, - offset: *mut u8, + 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); - } - } + 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 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> { - let aligned = unsafe { self.offset.add(self.offset.align_offset(layout.align())) }; - let next = unsafe { aligned.add(layout.size()) }; + fn allocate(&mut self, layout: alloc::Layout) -> Option> { + let aligned = unsafe { self.offset.add(self.offset.align_offset(layout.align())) }; + let next = unsafe { aligned.add(layout.size()) }; - if next <= self.end { - self.offset = next; - NonNull::new(aligned) - } else { - None - } - } + if next <= self.end { + self.offset = next; + NonNull::new(aligned) + } else { + None + } + } - fn reset(&mut self) { - self.offset = self.start; - } + fn reset(&mut self) { + self.offset = self.start; + } } pub struct Arena { - chunks: Vec, - elements: Vec, - valid: Rc>, - current_chunk_index: usize, - chunk_size: NonZeroUsize, + chunks: Vec, + elements: Vec, + valid: Rc>, + current_chunk_index: usize, + chunk_size: NonZeroUsize, } impl Drop for Arena { - fn drop(&mut self) { - self.clear(); - } + fn drop(&mut self) { + self.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, - } - } + 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, + } + } - pub fn capacity(&self) -> usize { - self.chunks.len() * self.chunk_size.get() - } + pub fn capacity(&self) -> usize { + self.chunks.len() * self.chunk_size.get() + } - pub fn 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; - } + pub fn 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()) }; - } + #[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::()) }; - } + 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() - ); - } - }; + 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::, - }); + unsafe { inner_writer(ptr.cast(), f) }; + self.elements.push(ArenaElement { + value: ptr, + drop: drop::, + }); - ArenaBox { - ptr: ptr.cast(), - valid: self.valid.clone(), - } - } + ArenaBox { + ptr: ptr.cast(), + valid: self.valid.clone(), + } + } } pub struct ArenaBox { - ptr: *mut T, - valid: Rc>, + 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, - } - } + #[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" - ); - } + #[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; + type Target = T; - #[inline(always)] - fn deref(&self) -> &Self::Target { - self.validate(); - unsafe { &*self.ptr } - } + #[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 } - } + #[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 std::{cell::Cell, rc::Rc}; - use super::*; + 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); + #[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); + 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()); - } + // 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); + #[test] + fn test_arena_grow() { + let mut arena = Arena::new(8); + arena.alloc(|| 1u64); + arena.alloc(|| 2u64); - assert_eq!(arena.capacity(), 16); + assert_eq!(arena.capacity(), 16); - arena.alloc(|| 3u32); - arena.alloc(|| 4u32); + arena.alloc(|| 3u32); + arena.alloc(|| 4u32); - assert_eq!(arena.capacity(), 24); - } + 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); + #[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, 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); - } + 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); + #[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; - } + arena.clear(); + let _read_value = *value; + } } diff --git a/crates/gpui/src/asset_cache.rs b/crates/gpui/src/asset_cache.rs index 9afbba8a0e..c1323faa63 100644 --- a/crates/gpui/src/asset_cache.rs +++ b/crates/gpui/src/asset_cache.rs @@ -10,75 +10,75 @@ 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), + /// 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) - } + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } } impl From for Resource { - fn from(value: PathBuf) -> Self { - Self::Path(value.into()) - } + fn from(value: PathBuf) -> Self { + Self::Path(value.into()) + } } impl From> for Resource { - fn from(value: Arc) -> Self { - Self::Path(value) - } + 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 source of the asset. + type Source: Clone + Hash + Send; - /// The loaded asset - type Output: Clone + Send; + /// The loaded asset + type Output: Clone + Send; - /// Load the asset asynchronously - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl Future + Send + 'static; + /// 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), + #[doc(hidden)] + _Phantom(PhantomData, &'static dyn crate::seal::Sealed), } impl Asset for AssetLogger where - T: Asset>, - R: Clone + Send, - E: Clone + Send + std::fmt::Display, + T: Asset>, + R: Clone + Send, + E: Clone + Send + std::fmt::Display, { - type Source = T::Source; + type Source = T::Source; - type Output = T::Output; + 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)) - } + 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 { - let mut hasher = collections::FxHasher::default(); - data.hash(&mut hasher); - hasher.finish() + let mut hasher = collections::FxHasher::default(); + data.hash(&mut hasher); + hasher.finish() } diff --git a/crates/gpui/src/assets.rs b/crates/gpui/src/assets.rs index cb8b47ed01..66d6cb80e1 100644 --- a/crates/gpui/src/assets.rs +++ b/crates/gpui/src/assets.rs @@ -3,29 +3,29 @@ use smallvec::SmallVec; use image::{Delay, Frame}; use std::{ - borrow::Cow, - fmt, - hash::Hash, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, + 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>>; + /// 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>; + /// 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 load(&self, _path: &str) -> Result>> { + Ok(None) + } - fn list(&self, _path: &str) -> Result> { - Ok(vec![]) - } + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } } /// A unique identifier for the image cache @@ -35,99 +35,99 @@ pub struct ImageId(pub usize); #[derive(PartialEq, Eq, Hash, Clone)] #[expect(missing_docs)] pub struct RenderImageParams { - pub image_id: ImageId, - pub frame_index: usize, + 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]>, + /// 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 - } + 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); + /// 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(), - } - } + 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()) - } + /// 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. + 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 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 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() - } + /// 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() - } + 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; + 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:?}"); - } + #[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/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs index 96bebda168..f4de6dbe91 100644 --- a/crates/gpui/src/bounds_tree.rs +++ b/crates/gpui/src/bounds_tree.rs @@ -1,9 +1,9 @@ use crate::{Bounds, Half}; use std::{ - cmp, - fmt::Debug, - ops::{Add, Sub}, - ptr::NonNull, + cmp, + fmt::Debug, + ops::{Add, Sub}, + ptr::NonNull, }; /// Maximum children per internal node (R-tree style branching factor). @@ -20,482 +20,481 @@ const MAX_CHILDREN: usize = 12; #[derive(Debug)] pub(crate) struct BoundsTree where - U: Clone + Debug + Default + PartialEq, + 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, - /// Minimum ordering assigned to any subsequent insert. Raised before painting deferred - /// draws so overlays always sort above the main scene (and their orders can't fall inside a - /// content-filter order range from the main scene). 0 means no floor. - order_floor: u32, - /// Reusable stack for tree traversal during insertion. - insert_path: Vec, - /// Reusable stack for search operations. - search_stack: Vec>>, + /// 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, + /// Minimum ordering assigned to any subsequent insert. Raised before painting deferred + /// draws so overlays always sort above the main scene (and their orders can't fall inside a + /// content-filter order range from the main scene). 0 means no floor. + order_floor: u32, + /// 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, + 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, + /// 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, - }, + /// 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, + // 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 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 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 len(&self) -> usize { + self.len as usize + } - fn as_slice(&self) -> &[usize] { - &self.indices[..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, + 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.order_floor = 0; - self.insert_path.clear(); - self.search_stack.clear(); - } + /// Clears all nodes from the tree. + pub fn clear(&mut self) { + self.nodes.clear(); + self.root = None; + self.max_leaf = None; + self.order_floor = 0; + self.insert_path.clear(); + self.search_stack.clear(); + } - /// Raise the minimum ordering for subsequent inserts to `floor`. Relative ordering above the - /// floor is preserved (overlapping inserts still step above one another). - pub fn set_order_floor(&mut self, floor: u32) { - self.order_floor = self.order_floor.max(floor); - } + /// Raise the minimum ordering for subsequent inserts to `floor`. Relative ordering above the + /// floor is preserved (overlapping inserts still step above one another). + pub fn set_order_floor(&mut self, floor: u32) { + self.order_floor = self.order_floor.max(floor); + } - /// The highest ordering assigned to any bounds so far (0 if empty). - pub fn max_order(&self) -> u32 { - self.max_leaf.map_or(0, |idx| self.nodes[idx].max_order) - } + /// The highest ordering assigned to any bounds so far (0 if empty). + pub fn max_order(&self) -> u32 { + self.max_leaf.map_or(0, |idx| self.nodes[idx].max_order) + } - /// Inserts bounds with an ordering strictly greater than *every* existing bounds (not just - /// intersecting ones), and returns that ordering. Used for content-filter group boundaries - /// (which must sort after all previously-painted content so their order range can't collide - /// with unrelated non-overlapping content that reuses low orderings) and to raise the order - /// floor before painting deferred draws (so overlays always sort above the main scene). - pub fn insert_above_all(&mut self, new_bounds: Bounds) -> u32 { - let ordering = self.max_order() + 1; - let new_leaf_idx = self.insert_leaf(new_bounds, ordering); - self.max_leaf = Some(new_leaf_idx); - ordering - } + /// Inserts bounds with an ordering strictly greater than *every* existing bounds (not just + /// intersecting ones), and returns that ordering. Used for content-filter group boundaries + /// (which must sort after all previously-painted content so their order range can't collide + /// with unrelated non-overlapping content that reuses low orderings) and to raise the order + /// floor before painting deferred draws (so overlays always sort above the main scene). + pub fn insert_above_all(&mut self, new_bounds: Bounds) -> u32 { + let ordering = self.max_order() + 1; + let new_leaf_idx = self.insert_leaf(new_bounds, ordering); + self.max_leaf = Some(new_leaf_idx); + ordering + } - /// 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).max(self.order_floor); + /// 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).max(self.order_floor); - // Insert the new leaf - let new_leaf_idx = self.insert_leaf(new_bounds, ordering); + // 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, - }; + // 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 - } + 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; - }; + /// 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; - } - } + // 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])); + // Slow path: search the tree + self.search_stack.clear(); + self.search_stack.push(NonNull::from(&self.nodes[root_idx])); - let mut max_found = 0u32; + 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() }; + 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; - } + // 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; - } + // 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), - ); - } - } - } + 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 - } + 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 }, - }); + /// 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; - }; + 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; + // 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 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; - } + 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; + // 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"); - }; + 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); + 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(); + // 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; - } - } + 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]; + // 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); - } - } + 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; + 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 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, - }, - }); + 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(); + // 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; + 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; - } - } + // 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); + // 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; + 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); - } - } - } - } - } + // 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); - } + updated_child_idx = Some(node_idx); + } - new_leaf_idx - } + new_leaf_idx + } } impl Default for BoundsTree where - U: Clone + Debug + Default + PartialEq, + U: Clone + Debug + Default + PartialEq, { - fn default() -> Self { - BoundsTree { - nodes: Vec::new(), - root: None, - max_leaf: None, - order_floor: 0, - insert_path: Vec::new(), - search_stack: Vec::new(), - } - } + fn default() -> Self { + BoundsTree { + nodes: Vec::new(), + root: None, + max_leaf: None, + order_floor: 0, + insert_path: Vec::new(), + search_stack: Vec::new(), + } + } } #[cfg(test)] mod tests { - use super::*; - use crate::{Bounds, Point, Size}; - use rand::{Rng, SeedableRng}; + 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, - }, - }; + #[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 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 - } + // 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(); + #[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 }, - }; + // 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)); + 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); - } - } - } + // 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/src/color.rs b/crates/gpui/src/color.rs index be8169e81d..461dcda9ef 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -1,690 +1,690 @@ use anyhow::{Context as _, bail}; use schemars::{JsonSchema, json_schema}; use serde::{ - Deserialize, Deserializer, Serialize, Serializer, - de::{self, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, + de::{self, Visitor}, }; use std::borrow::Cow; use std::{ - fmt::{self, Display, Formatter}, - hash::{Hash, Hasher}, + 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 } + 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 } + 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; - } + 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, + /// 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)) - } + 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, - } - } - } + /// 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, + } + } + } } 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 - } + 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; + type Value = Rgba; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string in the format #rrggbb or #rrggbbaa") - } + 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) - } + 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 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})$" - }) - } + 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) - } + 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; + 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) - } + 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; + 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 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), - }; + 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, - } - } + 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; + 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(); + 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"; + 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 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 - }; + 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 - } + /// 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}"), - }; + (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., - }) - } + 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, + /// Hue, in a range from 0 to 1 + pub h: f32, - /// Saturation, in a range from 0 to 1 - pub s: f32, + /// Saturation, in a range from 0 to 1 + pub s: f32, - /// Lightness, in a range from 0 to 1 - pub l: f32, + /// Lightness, in a range from 0 to 1 + pub l: f32, - /// Alpha, in a range from 0 to 1 - pub a: f32, + /// Alpha, in a range from 0 to 1 + pub a: f32, } #[cfg(feature = "proptest")] mod property { - use super::Hsla; - use proptest::prelude::*; + 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 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 = (); + 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() - } - } + 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() - } + 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)) - } + 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))) - } + 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())); - } + 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 - ) - } + 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 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.), - } + 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., - } + 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., - } + 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., - } + Hsla { + h: 0., + s: 0., + l: 1., + a: 0., + } } /// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { - Hsla { - h: 0., - s: 0., - l: lightness.clamp(0., 1.), - a: opacity.clamp(0., 1.), - } + 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., - } + 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., - } + 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., - } + 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., - } + 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., - } + 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() - } + /// 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 red + pub const fn red() -> Self { + red() + } - /// The color green - pub const fn green() -> Self { - green() - } + /// The color green + pub const fn green() -> Self { + green() + } - /// The color blue - pub const fn blue() -> Self { - blue() - } + /// The color blue + pub const fn blue() -> Self { + blue() + } - /// The color black - pub const fn black() -> Self { - black() - } + /// The color black + pub const fn black() -> Self { + black() + } - /// The color white - pub const fn white() -> Self { - white() - } + /// The color white + pub const fn white() -> Self { + white() + } - /// The color transparent black - pub const fn transparent_black() -> Self { - transparent_black() - } + /// 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 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 - } + /// 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; + /// 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) - } - } + 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, - } - } + /// 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.); - } + /// 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.), - } - } + /// 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 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.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.), - } - } + /// 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 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.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; + 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 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 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 - }; + 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, - } - } + Hsla { + h, + s, + l, + a: color.a, + } + } } impl JsonSchema for Hsla { - fn schema_name() -> Cow<'static, str> { - Rgba::schema_name() - } + fn schema_name() -> Cow<'static, str> { + Rgba::schema_name() + } - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - Rgba::json_schema(generator) - } + 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) - } + 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()) - } + 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, + Solid = 0, + LinearGradient = 1, + PatternSlash = 2, + Checkerboard = 3, } /// A color space for color interpolation. @@ -695,102 +695,102 @@ pub(crate) enum BackgroundTag { #[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, + #[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"), - } - } + 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, + 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 - ), - } - } + 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, - } - } + 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; + 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() - } + 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() - } + 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() - } + Background { + solid: color.into(), + ..Default::default() + } } /// Creates a LinearGradient background color. @@ -801,16 +801,16 @@ pub fn solid_background(color: impl Into) -> Background { /// /// pub fn linear_gradient( - angle: f32, - from: impl Into, - to: impl Into, + 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() - } + Background { + tag: BackgroundTag::LinearGradient, + gradient_angle_or_pattern_height: angle, + colors: [from.into(), to.into()], + ..Default::default() + } } /// A color stop in a linear gradient. @@ -819,164 +819,164 @@ pub fn 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, + /// 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, - } + 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), - } - } + /// 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 - } - } + /// 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 - } + /// 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 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(), - } - } + /// 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() - } - } + 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() - } - } + fn from(value: Rgba) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: Hsla::from(value), + ..Default::default() + } + } } #[cfg(test)] mod tests { - use serde_json::json; + use serde_json::json; - use super::*; + use super::*; - #[test] - fn test_deserialize_three_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); + #[test] + fn test_deserialize_three_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); - assert_eq!(actual, rgba(0xff0099ff)) - } + assert_eq!(actual, rgba(0xff0099ff)) + } - #[test] - fn test_deserialize_four_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); + #[test] + fn test_deserialize_four_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); - assert_eq!(actual, rgba(0xff0099ff)) - } + assert_eq!(actual, rgba(0xff0099ff)) + } - #[test] - fn test_deserialize_six_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); + #[test] + fn test_deserialize_six_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); - assert_eq!(actual, rgba(0xff0099ff)) - } + assert_eq!(actual, rgba(0xff0099ff)) + } - #[test] - fn test_deserialize_eight_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); + #[test] + fn test_deserialize_eight_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); - assert_eq!(actual, rgba(0xff0099ff)) - } + 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(); + #[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)) - } + 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(); + #[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)) - } + 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); + #[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()); - } + 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); + #[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()); - } + 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()); + } } diff --git a/crates/gpui/src/colors.rs b/crates/gpui/src/colors.rs index ef11ef57fd..3b0f8774ac 100644 --- a/crates/gpui/src/colors.rs +++ b/crates/gpui/src/colors.rs @@ -7,71 +7,71 @@ use std::sync::Arc; /// 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, + /// 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() - } + 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 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 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), - } - } + /// 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 + pub fn get_global(cx: &App) -> &Arc { + &cx.global::().0 + } } /// Get [Colors] from the global state @@ -79,25 +79,25 @@ impl Colors { pub struct GlobalColors(pub Arc); impl Deref for GlobalColors { - type Target = Arc; + type Target = Arc; - fn deref(&self) -> &Self::Target { - &self.0 - } + 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; + /// Returns the default [`Colors`] + fn default_colors(&self) -> &Arc; } impl DefaultColors for App { - fn default_colors(&self) -> &Arc { - &self.global::().0 - } + fn default_colors(&self) -> &Arc { + &self.global::().0 + } } /// The appearance of the base GPUI colors, used to style GPUI elements @@ -105,18 +105,18 @@ impl DefaultColors for App { /// 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, + /// 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, - } - } + fn from(appearance: WindowAppearance) -> Self { + match appearance { + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, + } + } } diff --git a/crates/gpui/src/dock/dock_area.rs b/crates/gpui/src/dock/dock_area.rs index 88b1477c53..85f78bb70c 100644 --- a/crates/gpui/src/dock/dock_area.rs +++ b/crates/gpui/src/dock/dock_area.rs @@ -6,14 +6,13 @@ use crate::dock::panel::PanelEvent; use crate::dock::split_handle::{SplitHandle, SplitHandleDrag, SplitHandleEvent}; use crate::dock::tab_bar::{TabBar, TabBarEvent}; use crate::dock::{ - path_key, DockLayout, DockLayoutState, DockNode, DropTarget, DropZone, NodePath, PanelHandle, - PanelId, PanelRegistry, + DockLayout, DockLayoutState, DockNode, DropTarget, DropZone, NodePath, PanelHandle, PanelId, + PanelRegistry, path_key, }; use crate::{ - deferred, div, hsla, px, relative, size, App, AppContext, Axis, Bounds, Context, Div, - DragMoveEvent, ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, ParentElement, Pixels, Point, Render, SharedString, Stateful, Styled, - Subscription, Window, + App, AppContext, Axis, Bounds, Context, Div, DragMoveEvent, ElementId, Entity, EventEmitter, + FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Point, Render, + SharedString, Stateful, Styled, Subscription, Window, deferred, div, hsla, px, relative, size, }; use std::any::Any; use std::collections::{HashMap, HashSet}; @@ -26,30 +25,30 @@ use std::sync::Arc; /// [`DockEvent::LayoutChanged`]. #[derive(Clone, Debug)] pub enum DockEvent { - /// A panel was added to the dock area. - PanelAdded(PanelId), - /// A panel was removed from the dock area (via - /// [`DockArea::remove_panel`]) without a close negotiation. - PanelRemoved(PanelId), - /// A panel became the focused panel, either by user interaction or via - /// [`DockArea::focus_panel`]. - PanelFocused(PanelId), - /// A panel was moved to a new position in the layout (drag-to-dock or - /// programmatic move). - PanelMoved { - /// The panel that moved. - panel: PanelId, - /// Where it landed. - target: DropTarget, - }, - /// The layout tree changed shape for any reason (add, remove, move, - /// split resize). Good trigger for autosaving - /// [`DockArea::save_state`]. - LayoutChanged, - /// A panel was closed by the user after the full - /// [`should_close`](crate::dock::DockPanel::should_close) / - /// [`on_close`](crate::dock::DockPanel::on_close) negotiation. - PanelClosed(PanelId), + /// A panel was added to the dock area. + PanelAdded(PanelId), + /// A panel was removed from the dock area (via + /// [`DockArea::remove_panel`]) without a close negotiation. + PanelRemoved(PanelId), + /// A panel became the focused panel, either by user interaction or via + /// [`DockArea::focus_panel`]. + PanelFocused(PanelId), + /// A panel was moved to a new position in the layout (drag-to-dock or + /// programmatic move). + PanelMoved { + /// The panel that moved. + panel: PanelId, + /// Where it landed. + target: DropTarget, + }, + /// The layout tree changed shape for any reason (add, remove, move, + /// split resize). Good trigger for autosaving + /// [`DockArea::save_state`]. + LayoutChanged, + /// A panel was closed by the user after the full + /// [`should_close`](crate::dock::DockPanel::should_close) / + /// [`on_close`](crate::dock::DockPanel::on_close) negotiation. + PanelClosed(PanelId), } /// In-flight drag-to-dock state for one pointer drag. @@ -59,15 +58,15 @@ pub enum DockEvent { /// [`DragMoveEvent`](crate::DragMoveEvent)s, and consumed or cancelled on /// drop. Internal to [`DockArea`]. struct DockDragState { - /// The panel being dragged. - panel: PanelId, - /// Current cursor position in window coordinates. - position: Point, - /// The drop target currently under the cursor, if any. - hovered: Option, - /// Bounds of the hovered tab group / leaf, used to position the drop - /// indicator overlay. - hovered_bounds: Option>, + /// The panel being dragged. + panel: PanelId, + /// Current cursor position in window coordinates. + position: Point, + /// The drop target currently under the cursor, if any. + hovered: Option, + /// Bounds of the hovered tab group / leaf, used to position the drop + /// indicator overlay. + hovered_bounds: Option>, } /// A dockable workspace: renders a [`DockLayout`] tree of panels and manages @@ -100,862 +99,884 @@ struct DockDragState { /// [`Focusable`] keep inner focus; the dock only tracks *which* panel is /// focused via [`DockEvent::PanelFocused`]. pub struct DockArea { - layout: DockLayout, - panels: HashMap, - registry: Option>, - focus_handle: FocusHandle, - focused_panel: Option, - drag: Option, - /// One tab-strip entity per `Tabs` node, keyed by the node's current - /// path. Re-created when the tree changes shape and pruned each render. - /// The subscription keeps the strip's events routed back to this view. - tab_bars: HashMap, Subscription)>, - /// One split-handle entity per boundary of each `Split` node, keyed by - /// the node's current path and the boundary index. Holds transient drag - /// state (`SplitHandle::drag_origin`) across frames; pruned with the tab - /// bars each render. A split with N children keeps N-1 handles so every - /// pair of panels can be resized independently. - split_handles: HashMap<(NodePath, usize), (Entity, Subscription)>, + layout: DockLayout, + panels: HashMap, + registry: Option>, + focus_handle: FocusHandle, + focused_panel: Option, + drag: Option, + /// One tab-strip entity per `Tabs` node, keyed by the node's current + /// path. Re-created when the tree changes shape and pruned each render. + /// The subscription keeps the strip's events routed back to this view. + tab_bars: HashMap, Subscription)>, + /// One split-handle entity per boundary of each `Split` node, keyed by + /// the node's current path and the boundary index. Holds transient drag + /// state (`SplitHandle::drag_origin`) across frames; pruned with the tab + /// bars each render. A split with N children keeps N-1 handles so every + /// pair of panels can be resized independently. + split_handles: HashMap<(NodePath, usize), (Entity, Subscription)>, } impl DockArea { - /// Creates an empty dock area with no panels and no registry. - /// - /// Typically wrapped in an entity by the caller: - /// `cx.new(|cx| DockArea::new(cx))`. - pub fn new(cx: &mut Context) -> Self { - Self { - layout: DockLayout::new(), - panels: HashMap::new(), - registry: None, - focus_handle: cx.focus_handle(), - focused_panel: None, - drag: None, - tab_bars: HashMap::new(), - split_handles: HashMap::new(), - } - } + /// Creates an empty dock area with no panels and no registry. + /// + /// Typically wrapped in an entity by the caller: + /// `cx.new(|cx| DockArea::new(cx))`. + pub fn new(cx: &mut Context) -> Self { + Self { + layout: DockLayout::new(), + panels: HashMap::new(), + registry: None, + focus_handle: cx.focus_handle(), + focused_panel: None, + drag: None, + tab_bars: HashMap::new(), + split_handles: HashMap::new(), + } + } - /// Builder: sets the [`PanelRegistry`] used for layout persistence. - /// - /// Without a registry, [`save_state`](DockArea::save_state) returns an - /// empty snapshot and [`restore_state`](DockArea::restore_state) restores - /// nothing. - pub fn with_registry(mut self, registry: Arc) -> Self { - self.registry = Some(registry); - self - } + /// Builder: sets the [`PanelRegistry`] used for layout persistence. + /// + /// Without a registry, [`save_state`](DockArea::save_state) returns an + /// empty snapshot and [`restore_state`](DockArea::restore_state) restores + /// nothing. + pub fn with_registry(mut self, registry: Arc) -> Self { + self.registry = Some(registry); + self + } - /// Adds a panel at `target` (or as the root if `target` is `None` and the - /// layout is empty). - /// - /// Subscribes to the panel's [`PanelEvent`](crate::dock::PanelEvent)s so - /// title changes and close requests are handled. Emits - /// [`DockEvent::PanelAdded`] and [`DockEvent::LayoutChanged`]. - /// - /// Returns `false` (and does nothing) if a panel with the same id is - /// already present, or if the insertion failed (see - /// [`DockLayout::insert_panel`]). - pub fn add_panel( - &mut self, - panel: PanelHandle, - target: Option, - cx: &mut Context, - ) -> bool { - let id = panel.panel_id(); - if self.panels.contains_key(&id) { - return false; - } - if !self.layout.insert_panel(id, target) { - return false; - } - let mut panel = panel; - self.install_panel_subscription(&mut panel, cx); - self.panels.insert(id, panel); - self.emit_layout_changed(cx); - cx.emit(DockEvent::PanelAdded(id)); - true - } + /// Adds a panel at `target` (or as the root if `target` is `None` and the + /// layout is empty). + /// + /// Subscribes to the panel's [`PanelEvent`](crate::dock::PanelEvent)s so + /// title changes and close requests are handled. Emits + /// [`DockEvent::PanelAdded`] and [`DockEvent::LayoutChanged`]. + /// + /// Returns `false` (and does nothing) if a panel with the same id is + /// already present, or if the insertion failed (see + /// [`DockLayout::insert_panel`]). + pub fn add_panel( + &mut self, + panel: PanelHandle, + target: Option, + cx: &mut Context, + ) -> bool { + let id = panel.panel_id(); + if self.panels.contains_key(&id) { + return false; + } + if !self.layout.insert_panel(id, target) { + return false; + } + let mut panel = panel; + self.install_panel_subscription(&mut panel, cx); + self.panels.insert(id, panel); + self.emit_layout_changed(cx); + cx.emit(DockEvent::PanelAdded(id)); + true + } - /// Removes a panel without close negotiation and returns its handle. - /// - /// The caller regains ownership of the view (e.g. to re-dock it elsewhere - /// or drop it). Emits [`DockEvent::PanelRemoved`] and - /// [`DockEvent::LayoutChanged`]. Returns `None` if the id is unknown. - /// For user-initiated closes, prefer the request flow driven by - /// [`PanelEvent::CloseRequested`](crate::dock::PanelEvent::CloseRequested), - /// which honors [`DockPanel::should_close`](crate::dock::DockPanel::should_close). - pub fn remove_panel(&mut self, id: PanelId, cx: &mut Context) -> Option { - let mut handle = self.panels.remove(&id)?; - // Dropping the subscription unsubscribes from the panel's events. - handle.set_subscription(None); - self.layout.remove_panel(id); - if self.focused_panel == Some(id) { - self.focused_panel = None; - } - self.emit_layout_changed(cx); - cx.emit(DockEvent::PanelRemoved(id)); - Some(handle) - } + /// Removes a panel without close negotiation and returns its handle. + /// + /// The caller regains ownership of the view (e.g. to re-dock it elsewhere + /// or drop it). Emits [`DockEvent::PanelRemoved`] and + /// [`DockEvent::LayoutChanged`]. Returns `None` if the id is unknown. + /// For user-initiated closes, prefer the request flow driven by + /// [`PanelEvent::CloseRequested`](crate::dock::PanelEvent::CloseRequested), + /// which honors [`DockPanel::should_close`](crate::dock::DockPanel::should_close). + pub fn remove_panel(&mut self, id: PanelId, cx: &mut Context) -> Option { + let mut handle = self.panels.remove(&id)?; + // Dropping the subscription unsubscribes from the panel's events. + handle.set_subscription(None); + self.layout.remove_panel(id); + if self.focused_panel == Some(id) { + self.focused_panel = None; + } + self.emit_layout_changed(cx); + cx.emit(DockEvent::PanelRemoved(id)); + Some(handle) + } - /// Moves keyboard focus (and the tab-strip selection) to `panel`. - /// - /// Activates the panel's tab if it lives in a `Tabs` group, focuses the - /// panel's own focus handle if it has one, and emits - /// [`DockEvent::PanelFocused`]. No-op if the id is unknown. - pub fn focus_panel(&mut self, id: PanelId, window: &mut Window, cx: &mut Context) { - if !self.panels.contains_key(&id) { - return; - } - // Activate the panel's tab so the strip selection follows. - if let Some(path) = self.layout.find_panel(id) { - self.layout.set_tabs_active(&path, id); - } - self.focused_panel = Some(id); - // The panel's own focus handle is not reachable through the - // type-erased `AnyView`, so give the dock area keyboard focus; panels - // that implement `Focusable` re-establish inner focus on interaction. - self.focus_handle.focus(window, cx); - cx.emit(DockEvent::PanelFocused(id)); - cx.notify(); - } + /// Moves keyboard focus (and the tab-strip selection) to `panel`. + /// + /// Activates the panel's tab if it lives in a `Tabs` group, focuses the + /// panel's own focus handle if it has one, and emits + /// [`DockEvent::PanelFocused`]. No-op if the id is unknown. + pub fn focus_panel(&mut self, id: PanelId, window: &mut Window, cx: &mut Context) { + if !self.panels.contains_key(&id) { + return; + } + // Activate the panel's tab so the strip selection follows. + if let Some(path) = self.layout.find_panel(id) { + self.layout.set_tabs_active(&path, id); + } + self.focused_panel = Some(id); + // The panel's own focus handle is not reachable through the + // type-erased `AnyView`, so give the dock area keyboard focus; panels + // that implement `Focusable` re-establish inner focus on interaction. + self.focus_handle.focus(window, cx); + cx.emit(DockEvent::PanelFocused(id)); + cx.notify(); + } - /// Returns the current layout tree. - pub fn layout(&self) -> &DockLayout { - &self.layout - } + /// Returns the current layout tree. + pub fn layout(&self) -> &DockLayout { + &self.layout + } - /// Replaces the whole layout tree. - /// - /// Panels referenced by `layout` that have no registered handle are - /// dropped from the tree (via [`DockLayout::cleanup`]); panels that are - /// registered but unreferenced stay loaded but hidden until re-added. - /// Emits [`DockEvent::LayoutChanged`]. - pub fn set_layout(&mut self, mut layout: DockLayout, cx: &mut Context) { - // Panels the caller's tree references without a live handle are - // dropped; `remove_panel` runs `cleanup` to collapse the gaps. - let missing: Vec = layout - .panels() - .into_iter() - .filter(|id| !self.panels.contains_key(id)) - .collect(); - for id in missing { - layout.remove_panel(id); - } - self.layout = layout; - self.emit_layout_changed(cx); - } + /// Replaces the whole layout tree. + /// + /// Panels referenced by `layout` that have no registered handle are + /// dropped from the tree (via [`DockLayout::cleanup`]); panels that are + /// registered but unreferenced stay loaded but hidden until re-added. + /// Emits [`DockEvent::LayoutChanged`]. + pub fn set_layout(&mut self, mut layout: DockLayout, cx: &mut Context) { + // Panels the caller's tree references without a live handle are + // dropped; `remove_panel` runs `cleanup` to collapse the gaps. + let missing: Vec = layout + .panels() + .into_iter() + .filter(|id| !self.panels.contains_key(id)) + .collect(); + for id in missing { + layout.remove_panel(id); + } + self.layout = layout; + self.emit_layout_changed(cx); + } - /// Returns the panel handle for `id`, if registered. - pub fn panel(&self, id: PanelId) -> Option<&PanelHandle> { - self.panels.get(&id) - } + /// Returns the panel handle for `id`, if registered. + pub fn panel(&self, id: PanelId) -> Option<&PanelHandle> { + self.panels.get(&id) + } - /// Captures a serializable snapshot of the current layout. - /// - /// Requires a registry (see [`with_registry`](DockArea::with_registry)); - /// without one, returns an empty snapshot. Panels the registry declines - /// to key are omitted. Persist with `serde_json` or similar. - pub fn save_state(&self) -> DockLayoutState { - match &self.registry { - Some(registry) => DockLayoutState::capture(&self.layout, registry.as_ref()), - // Without a registry no panel can be keyed, so the snapshot is - // empty (but carries the current format version). - None => DockLayoutState::capture(&self.layout, &NoopPanelRegistry), - } - } + /// Captures a serializable snapshot of the current layout. + /// + /// Requires a registry (see [`with_registry`](DockArea::with_registry)); + /// without one, returns an empty snapshot. Panels the registry declines + /// to key are omitted. Persist with `serde_json` or similar. + pub fn save_state(&self) -> DockLayoutState { + match &self.registry { + Some(registry) => DockLayoutState::capture(&self.layout, registry.as_ref()), + // Without a registry no panel can be keyed, so the snapshot is + // empty (but carries the current format version). + None => DockLayoutState::capture(&self.layout, &NoopPanelRegistry), + } + } - /// Restores a previously saved snapshot, rebuilding panels through the - /// registry. - /// - /// Panels whose keys the registry cannot rebuild are skipped; the layout - /// is normalized afterwards. Existing panels not referenced by the - /// snapshot are kept registered (hidden) so their state survives a - /// layout switch; panels that *are* referenced are re-used rather than - /// rebuilt when their current id's key matches. - /// - /// Emits [`DockEvent::LayoutChanged`] if the tree changed. - /// - /// # Panics - /// - /// Does not panic on malformed input; unknown keys and bad indices are - /// dropped/clamped. Returns without effect if no registry is set. - pub fn restore_state( - &mut self, - state: &DockLayoutState, - window: &mut Window, - cx: &mut Context, - ) { - let Some(registry) = self.registry.clone() else { - return; - }; - // The tree shape with panels addressed by deterministic interim ids - // (hashed from the registry keys); real ids are resolved below. - let mut layout = state.to_layout(); + /// Restores a previously saved snapshot, rebuilding panels through the + /// registry. + /// + /// Panels whose keys the registry cannot rebuild are skipped; the layout + /// is normalized afterwards. Existing panels not referenced by the + /// snapshot are kept registered (hidden) so their state survives a + /// layout switch; panels that *are* referenced are re-used rather than + /// rebuilt when their current id's key matches. + /// + /// Emits [`DockEvent::LayoutChanged`] if the tree changed. + /// + /// # Panics + /// + /// Does not panic on malformed input; unknown keys and bad indices are + /// dropped/clamped. Returns without effect if no registry is set. + pub fn restore_state( + &mut self, + state: &DockLayoutState, + window: &mut Window, + cx: &mut Context, + ) { + let Some(registry) = self.registry.clone() else { + return; + }; + // The tree shape with panels addressed by deterministic interim ids + // (hashed from the registry keys); real ids are resolved below. + let mut layout = state.to_layout(); - // Rebuild the panels, mapping each key's interim id to the live - // panel's id. Re-use an already-registered panel when its key matches - // so per-panel state survives the layout switch. - let mut interim_to_real: HashMap = HashMap::new(); - let mut fresh: Vec = Vec::new(); - for key in state.keys() { - let interim = interim_id(&key); - if interim_to_real.contains_key(&interim) { - // The same key occurred twice in the snapshot; the duplicate - // occurrence is dropped when the tree is re-mapped below. - continue; - } - let reuse = self.panels.values().find(|handle| { - registry.panel_key(handle.panel_id()) == Some(key.clone()) - }); - match reuse { - Some(handle) => { - interim_to_real.insert(interim, handle.panel_id()); - } - None => match registry.build_panel(&key, window, cx) { - Some(handle) => { - interim_to_real.insert(interim, handle.panel_id()); - fresh.push(handle); - } - None => {} // unknown key; its node collapses away - }, - } - } + // Rebuild the panels, mapping each key's interim id to the live + // panel's id. Re-use an already-registered panel when its key matches + // so per-panel state survives the layout switch. + let mut interim_to_real: HashMap = HashMap::new(); + let mut fresh: Vec = Vec::new(); + for key in state.keys() { + let interim = interim_id(&key); + if interim_to_real.contains_key(&interim) { + // The same key occurred twice in the snapshot; the duplicate + // occurrence is dropped when the tree is re-mapped below. + continue; + } + let reuse = self + .panels + .values() + .find(|handle| registry.panel_key(handle.panel_id()) == Some(key.clone())); + match reuse { + Some(handle) => { + interim_to_real.insert(interim, handle.panel_id()); + } + None => match registry.build_panel(&key, window, cx) { + Some(handle) => { + interim_to_real.insert(interim, handle.panel_id()); + fresh.push(handle); + } + None => {} // unknown key; its node collapses away + }, + } + } - // Re-write the interim ids to real ids, dropping panels that could - // not be rebuilt and de-duplicating panels that appear more than once. - let mut used: HashSet = HashSet::new(); - let mut to_remove: Vec = Vec::new(); - if let Some(root) = layout.root_mut() { - Self::remap_node_ids(root, &interim_to_real, &mut used, &mut to_remove); - } - for id in to_remove { - layout.remove_panel(id); - } + // Re-write the interim ids to real ids, dropping panels that could + // not be rebuilt and de-duplicating panels that appear more than once. + let mut used: HashSet = HashSet::new(); + let mut to_remove: Vec = Vec::new(); + if let Some(root) = layout.root_mut() { + Self::remap_node_ids(root, &interim_to_real, &mut used, &mut to_remove); + } + for id in to_remove { + layout.remove_panel(id); + } - self.layout = layout; - for handle in fresh { - let id = handle.panel_id(); - if self.panels.contains_key(&id) { - continue; - } - let mut handle = handle; - self.install_panel_subscription(&mut handle, cx); - self.panels.insert(id, handle); - } - self.emit_layout_changed(cx); - } + self.layout = layout; + for handle in fresh { + let id = handle.panel_id(); + if self.panels.contains_key(&id) { + continue; + } + let mut handle = handle; + self.install_panel_subscription(&mut handle, cx); + self.panels.insert(id, handle); + } + self.emit_layout_changed(cx); + } - /// Undocks a panel into its own floating window. - /// - /// **Deferred**: floating panels depend on unverified multi-window - /// capabilities; see the [`floating`](crate::dock::FloatingPanelWindow) - /// docs. When implemented, this removes the panel from the layout (like - /// [`remove_panel`](DockArea::remove_panel) but without emitting - /// [`DockEvent::PanelRemoved`]) and opens a - /// [`FloatingPanelWindow`](crate::dock::FloatingPanelWindow) hosting it; - /// dropping the window back over a dock area re-docks the panel. Until - /// then, always returns `false`. - /// - /// Returns `true` if the panel was floated. - pub fn float_panel( - &mut self, - _id: PanelId, - _window: &mut Window, - _cx: &mut Context, - ) -> bool { - false - } + /// Undocks a panel into its own floating window. + /// + /// **Deferred**: floating panels depend on unverified multi-window + /// capabilities; see the [`floating`](crate::dock::FloatingPanelWindow) + /// docs. When implemented, this removes the panel from the layout (like + /// [`remove_panel`](DockArea::remove_panel) but without emitting + /// [`DockEvent::PanelRemoved`]) and opens a + /// [`FloatingPanelWindow`](crate::dock::FloatingPanelWindow) hosting it; + /// dropping the window back over a dock area re-docks the panel. Until + /// then, always returns `false`. + /// + /// Returns `true` if the panel was floated. + pub fn float_panel( + &mut self, + _id: PanelId, + _window: &mut Window, + _cx: &mut Context, + ) -> bool { + false + } - /// Hit-tests a cursor position against the five drop zones of a target. - /// - /// `point` is in window coordinates; `target_bounds` are the bounds of - /// the hovered tab group / leaf. The bounds are divided into a center - /// region ([`DropZone::Center`]) and four edge bands; the band width is a - /// fraction of the smaller dimension so narrow targets stay usable. - /// Returns `None` when `point` lies outside `target_bounds`. - /// - /// Pure and exposed for testing; the drag handlers call it with the - /// bounds cached in the internal drag state. - pub fn drop_zone_at(point: Point, target_bounds: Bounds) -> Option { - if !target_bounds.contains(&point) { - return None; - } - let width = target_bounds.size.width.0; - let height = target_bounds.size.height.0; - // Edge bands are a quarter of the smaller dimension, so narrow - // targets (deeply split columns) keep usable edge zones. - let band = (width.min(height) * 0.25).max(1.0); - let dx = point.x.0 - target_bounds.origin.x.0; - let dy = point.y.0 - target_bounds.origin.y.0; - if dx < band { - Some(DropZone::Left) - } else if dx > width - band { - Some(DropZone::Right) - } else if dy < band { - Some(DropZone::Top) - } else if dy > height - band { - Some(DropZone::Bottom) - } else { - Some(DropZone::Center) - } - } + /// Hit-tests a cursor position against the five drop zones of a target. + /// + /// `point` is in window coordinates; `target_bounds` are the bounds of + /// the hovered tab group / leaf. The bounds are divided into a center + /// region ([`DropZone::Center`]) and four edge bands; the band width is a + /// fraction of the smaller dimension so narrow targets stay usable. + /// Returns `None` when `point` lies outside `target_bounds`. + /// + /// Pure and exposed for testing; the drag handlers call it with the + /// bounds cached in the internal drag state. + pub fn drop_zone_at(point: Point, target_bounds: Bounds) -> Option { + if !target_bounds.contains(&point) { + return None; + } + let width = target_bounds.size.width.0; + let height = target_bounds.size.height.0; + // Edge bands are a quarter of the smaller dimension, so narrow + // targets (deeply split columns) keep usable edge zones. + let band = (width.min(height) * 0.25).max(1.0); + let dx = point.x.0 - target_bounds.origin.x.0; + let dy = point.y.0 - target_bounds.origin.y.0; + if dx < band { + Some(DropZone::Left) + } else if dx > width - band { + Some(DropZone::Right) + } else if dy < band { + Some(DropZone::Top) + } else if dy > height - band { + Some(DropZone::Bottom) + } else { + Some(DropZone::Center) + } + } - /// Renders the translucent drop indicator for the currently hovered - /// [`DropTarget`], if a drag is in flight. - /// - /// Painted via [`deferred`](crate::deferred) so it overlays panel content unclipped. The - /// indicator highlights the sub-rectangle of the target bounds that the - /// panel would occupy (half for edge zones, full for - /// [`DropZone::Center`]). - fn render_drop_indicator( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let drag = self.drag.as_ref()?; - let (target, bounds) = (drag.hovered?, drag.hovered_bounds?); - // The hovered bounds are in window coordinates; the dock area's root - // is laid out at the window origin in its primary embedding, so they - // double as root-relative coordinates for the absolutely positioned - // overlay. Display-only — docking hit-testing never depends on it. - let rect = match target.zone { - DropZone::Center => bounds, - DropZone::Left => Bounds::new( - bounds.origin, - size(bounds.size.width * 0.5, bounds.size.height), - ), - DropZone::Right => Bounds::new( - Point::new(bounds.right() - bounds.size.width * 0.5, bounds.top()), - size(bounds.size.width * 0.5, bounds.size.height), - ), - DropZone::Top => Bounds::new( - bounds.origin, - size(bounds.size.width, bounds.size.height * 0.5), - ), - DropZone::Bottom => Bounds::new( - Point::new(bounds.left(), bounds.bottom() - bounds.size.height * 0.5), - size(bounds.size.width, bounds.size.height * 0.5), - ), - }; - Some( - deferred( - div() - .absolute() - .left(px(rect.origin.x.0)) - .top(px(rect.origin.y.0)) - .w(px(rect.size.width.0)) - .h(px(rect.size.height.0)) - .rounded_md() - .bg(hsla(0.62, 0.7, 0.8, 0.25)), - ), - ) - } + /// Renders the translucent drop indicator for the currently hovered + /// [`DropTarget`], if a drag is in flight. + /// + /// Painted via [`deferred`](crate::deferred) so it overlays panel content unclipped. The + /// indicator highlights the sub-rectangle of the target bounds that the + /// panel would occupy (half for edge zones, full for + /// [`DropZone::Center`]). + fn render_drop_indicator( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let drag = self.drag.as_ref()?; + let (target, bounds) = (drag.hovered?, drag.hovered_bounds?); + // The hovered bounds are in window coordinates; the dock area's root + // is laid out at the window origin in its primary embedding, so they + // double as root-relative coordinates for the absolutely positioned + // overlay. Display-only — docking hit-testing never depends on it. + let rect = match target.zone { + DropZone::Center => bounds, + DropZone::Left => Bounds::new( + bounds.origin, + size(bounds.size.width * 0.5, bounds.size.height), + ), + DropZone::Right => Bounds::new( + Point::new(bounds.right() - bounds.size.width * 0.5, bounds.top()), + size(bounds.size.width * 0.5, bounds.size.height), + ), + DropZone::Top => Bounds::new( + bounds.origin, + size(bounds.size.width, bounds.size.height * 0.5), + ), + DropZone::Bottom => Bounds::new( + Point::new(bounds.left(), bounds.bottom() - bounds.size.height * 0.5), + size(bounds.size.width, bounds.size.height * 0.5), + ), + }; + Some(deferred( + div() + .absolute() + .left(px(rect.origin.x.0)) + .top(px(rect.origin.y.0)) + .w(px(rect.size.width.0)) + .h(px(rect.size.height.0)) + .rounded_md() + .bg(hsla(0.62, 0.7, 0.8, 0.25)), + )) + } - /// Begins a dock drag for `panel`. Called by the tab strip when a tab - /// drag leaves the strip's bounds. - fn begin_drag(&mut self, panel: PanelId, position: Point, cx: &mut Context) { - match &mut self.drag { - // Re-entered the strip mid-drag (the strip keeps emitting - // `DockDragStarted`); keep the hovered target intact. - Some(drag) if drag.panel == panel => { - drag.position = position; - } - _ => { - self.drag = Some(DockDragState { - panel, - position, - hovered: None, - hovered_bounds: None, - }); - } - } - cx.notify(); - } + /// Begins a dock drag for `panel`. Called by the tab strip when a tab + /// drag leaves the strip's bounds. + fn begin_drag(&mut self, panel: PanelId, position: Point, cx: &mut Context) { + match &mut self.drag { + // Re-entered the strip mid-drag (the strip keeps emitting + // `DockDragStarted`); keep the hovered target intact. + Some(drag) if drag.panel == panel => { + drag.position = position; + } + _ => { + self.drag = Some(DockDragState { + panel, + position, + hovered: None, + hovered_bounds: None, + }); + } + } + cx.notify(); + } - /// Updates the hovered drop target during a drag. Attached to panel - /// containers via `on_drag_move` with a payload identifying the dragged - /// panel. - /// - /// The calling handler stashes the candidate target (which panel, or - /// `None` for the dock area's outer edges) and its bounds in the drag - /// state; this method classifies the cursor within those bounds and - /// commits (or clears) the hovered target. - fn update_drag(&mut self, position: Point, _window: &Window, cx: &mut Context) { - let Some(drag) = self.drag.as_mut() else { - return; - }; - let Some(bounds) = drag.hovered_bounds else { - return; - }; - drag.position = position; - let panel = drag.hovered.and_then(|target| target.panel); - match Self::drop_zone_at(position, bounds) { - // A root-edge target (`panel: None`) may only ever be an edge - // zone; a center hit falls through to the no-target case. - Some(zone) if panel.is_some() || zone.is_split() => { - drag.hovered = Some(DropTarget { panel, zone }); - } - _ => { - drag.hovered = None; - drag.hovered_bounds = None; - } - } - cx.notify(); - } + /// Updates the hovered drop target during a drag. Attached to panel + /// containers via `on_drag_move` with a payload identifying the dragged + /// panel. + /// + /// The calling handler stashes the candidate target (which panel, or + /// `None` for the dock area's outer edges) and its bounds in the drag + /// state; this method classifies the cursor within those bounds and + /// commits (or clears) the hovered target. + fn update_drag(&mut self, position: Point, _window: &Window, cx: &mut Context) { + let Some(drag) = self.drag.as_mut() else { + return; + }; + let Some(bounds) = drag.hovered_bounds else { + return; + }; + drag.position = position; + let panel = drag.hovered.and_then(|target| target.panel); + match Self::drop_zone_at(position, bounds) { + // A root-edge target (`panel: None`) may only ever be an edge + // zone; a center hit falls through to the no-target case. + Some(zone) if panel.is_some() || zone.is_split() => { + drag.hovered = Some(DropTarget { panel, zone }); + } + _ => { + drag.hovered = None; + drag.hovered_bounds = None; + } + } + cx.notify(); + } - /// Completes the current drag, applying the hovered drop. - /// - /// No-op if no drag is in flight or nothing is hovered; emits - /// [`DockEvent::PanelMoved`] and [`DockEvent::LayoutChanged`] on success. - fn finish_drag(&mut self, cx: &mut Context) { - let Some(drag) = self.drag.take() else { - return; - }; - let Some(target) = drag.hovered else { - cx.notify(); - return; - }; - if self.layout.move_panel(drag.panel, target) { - cx.emit(DockEvent::PanelMoved { - panel: drag.panel, - target, - }); - self.emit_layout_changed(cx); - } else { - cx.notify(); - } - } + /// Completes the current drag, applying the hovered drop. + /// + /// No-op if no drag is in flight or nothing is hovered; emits + /// [`DockEvent::PanelMoved`] and [`DockEvent::LayoutChanged`] on success. + fn finish_drag(&mut self, cx: &mut Context) { + let Some(drag) = self.drag.take() else { + return; + }; + let Some(target) = drag.hovered else { + cx.notify(); + return; + }; + if self.layout.move_panel(drag.panel, target) { + cx.emit(DockEvent::PanelMoved { + panel: drag.panel, + target, + }); + self.emit_layout_changed(cx); + } else { + cx.notify(); + } + } - /// Emits [`DockEvent::LayoutChanged`] and repaints. - fn emit_layout_changed(&mut self, cx: &mut Context) { - cx.emit(DockEvent::LayoutChanged); - cx.notify(); - } + /// Emits [`DockEvent::LayoutChanged`] and repaints. + fn emit_layout_changed(&mut self, cx: &mut Context) { + cx.emit(DockEvent::LayoutChanged); + cx.notify(); + } - /// Installs the subscription routing a panel's [`PanelEvent`]s back to - /// this dock area. The subscription is stored on the handle so it is - /// dropped (and unsubscribed) when the panel leaves the dock. - fn install_panel_subscription(&mut self, handle: &mut PanelHandle, cx: &mut Context) { - let id = handle.panel_id(); - // Panels are type-erased (`AnyView`), so a typed `Context::subscribe` - // cannot reach them; subscribe by entity id and downcast the event. - let entity_id = handle.view().entity_id(); - let this = cx.weak_entity(); - let subscription = cx.new_subscription( - entity_id, - ( - std::any::TypeId::of::(), - Box::new(move |event: &dyn Any, cx: &mut App| { - let event = event - .downcast_ref::() - .expect("dock panel events are PanelEvent"); - let Some(entity) = this.upgrade() else { - return false; - }; - entity.update(cx, |dock, cx| dock.on_panel_event(id, event.clone(), cx)); - true - }), - ), - ); - handle.set_subscription(Some(subscription)); - } + /// Installs the subscription routing a panel's [`PanelEvent`]s back to + /// this dock area. The subscription is stored on the handle so it is + /// dropped (and unsubscribed) when the panel leaves the dock. + fn install_panel_subscription(&mut self, handle: &mut PanelHandle, cx: &mut Context) { + let id = handle.panel_id(); + // Panels are type-erased (`AnyView`), so a typed `Context::subscribe` + // cannot reach them; subscribe by entity id and downcast the event. + let entity_id = handle.view().entity_id(); + let this = cx.weak_entity(); + let subscription = cx.new_subscription( + entity_id, + ( + std::any::TypeId::of::(), + Box::new(move |event: &dyn Any, cx: &mut App| { + let event = event + .downcast_ref::() + .expect("dock panel events are PanelEvent"); + let Some(entity) = this.upgrade() else { + return false; + }; + entity.update(cx, |dock, cx| dock.on_panel_event(id, event.clone(), cx)); + true + }), + ), + ); + handle.set_subscription(Some(subscription)); + } - /// Handles a [`PanelEvent`] emitted by a held panel. - fn on_panel_event(&mut self, id: PanelId, event: PanelEvent, cx: &mut Context) { - match event { - PanelEvent::CloseRequested => { - // Panels initiate the close flow themselves — consulting - // `DockPanel::should_close` is not reachable through the - // type-erased view — so a request is a confirmed close. - let _ = self.remove_panel(id, cx); - } - PanelEvent::Focused => { - self.focused_panel = Some(id); - cx.emit(DockEvent::PanelFocused(id)); - cx.notify(); - } - PanelEvent::TitleChanged => { - // The cached title snapshot on the handle cannot be refreshed - // through `AnyView`; repaint so the strip re-reads what it can. - cx.notify(); - } - } - } + /// Handles a [`PanelEvent`] emitted by a held panel. + fn on_panel_event(&mut self, id: PanelId, event: PanelEvent, cx: &mut Context) { + match event { + PanelEvent::CloseRequested => { + // Panels initiate the close flow themselves — consulting + // `DockPanel::should_close` is not reachable through the + // type-erased view — so a request is a confirmed close. + let _ = self.remove_panel(id, cx); + } + PanelEvent::Focused => { + self.focused_panel = Some(id); + cx.emit(DockEvent::PanelFocused(id)); + cx.notify(); + } + PanelEvent::TitleChanged => { + // The cached title snapshot on the handle cannot be refreshed + // through `AnyView`; repaint so the strip re-reads what it can. + cx.notify(); + } + } + } - /// Handles an event emitted by one of the tab-strip entities. - fn on_tab_bar_event(&mut self, event: &TabBarEvent, cx: &mut Context) { - match event { - TabBarEvent::Reordered { tabs, active } => { - // Paths captured at subscribe time go stale on structural - // edits, so resolve the owning `Tabs` node from the panel set - // the strip just reported. - let Some(first) = tabs.first() else { - return; - }; - let Some(path) = self.layout.find_panel(*first) else { - return; - }; - let Some(node) = self.layout.node_at_mut(&path) else { - return; - }; - if let DockNode::Tabs { - panels, - active: current, - } = node - && panels.len() == tabs.len() - { - *panels = tabs.clone(); - *current = *active; - cx.notify(); - } - } - TabBarEvent::CloseRequested(id) => { - let _ = self.remove_panel(*id, cx); - } - TabBarEvent::DockDragStarted { panel, position } => { - self.begin_drag(*panel, *position, cx); - } - } - } + /// Handles an event emitted by one of the tab-strip entities. + fn on_tab_bar_event(&mut self, event: &TabBarEvent, cx: &mut Context) { + match event { + TabBarEvent::Reordered { tabs, active } => { + // Paths captured at subscribe time go stale on structural + // edits, so resolve the owning `Tabs` node from the panel set + // the strip just reported. + let Some(first) = tabs.first() else { + return; + }; + let Some(path) = self.layout.find_panel(*first) else { + return; + }; + let Some(node) = self.layout.node_at_mut(&path) else { + return; + }; + if let DockNode::Tabs { + panels, + active: current, + } = node && panels.len() == tabs.len() + { + *panels = tabs.clone(); + *current = *active; + cx.notify(); + } + } + TabBarEvent::CloseRequested(id) => { + let _ = self.remove_panel(*id, cx); + } + TabBarEvent::DockDragStarted { panel, position } => { + self.begin_drag(*panel, *position, cx); + } + } + } - /// Gets (creating and subscribing on first use) the tab strip for the - /// `Tabs` node at `path`, and syncs it with the node's current state. - fn tab_bar_for( - &mut self, - path: &NodePath, - panels: &[PanelId], - active: usize, - cx: &mut Context, - ) -> Entity { - let titles: Vec = panels - .iter() - .map(|id| { - self.panels - .get(id) - .map(|handle| handle.title().clone()) - .unwrap_or_default() - }) - .collect(); - let closable: Vec = panels - .iter() - .map(|id| self.panels.get(id).map(|handle| handle.closable()).unwrap_or(false)) - .collect(); - let bar = match self.tab_bars.get(path) { - Some((bar, _)) => bar.clone(), - None => { - let bar = cx.new(|_cx| TabBar::new(panels.to_vec(), active)); - let subscription = cx.subscribe(&bar, |this, _bar, event: &TabBarEvent, cx| { - this.on_tab_bar_event(event, cx); - }); - self.tab_bars.insert(path.clone(), (bar.clone(), subscription)); - bar - } - }; - bar.update(cx, |bar, cx| bar.sync(panels, active, &titles, &closable, cx)); - bar - } + /// Gets (creating and subscribing on first use) the tab strip for the + /// `Tabs` node at `path`, and syncs it with the node's current state. + fn tab_bar_for( + &mut self, + path: &NodePath, + panels: &[PanelId], + active: usize, + cx: &mut Context, + ) -> Entity { + let titles: Vec = panels + .iter() + .map(|id| { + self.panels + .get(id) + .map(|handle| handle.title().clone()) + .unwrap_or_default() + }) + .collect(); + let closable: Vec = panels + .iter() + .map(|id| { + self.panels + .get(id) + .map(|handle| handle.closable()) + .unwrap_or(false) + }) + .collect(); + let bar = match self.tab_bars.get(path) { + Some((bar, _)) => bar.clone(), + None => { + let bar = cx.new(|_cx| TabBar::new(panels.to_vec(), active)); + let subscription = cx.subscribe(&bar, |this, _bar, event: &TabBarEvent, cx| { + this.on_tab_bar_event(event, cx); + }); + self.tab_bars + .insert(path.clone(), (bar.clone(), subscription)); + bar + } + }; + bar.update(cx, |bar, cx| { + bar.sync(panels, active, &titles, &closable, cx) + }); + bar + } - /// Gets (creating and subscribing on first use) the split-handle entity - /// for the boundary at `index` of the `Split` node at `path`. - fn split_handle_for( - &mut self, - path: &NodePath, - index: usize, - direction: Axis, - cx: &mut Context, - ) -> Entity { - if let Some((handle, _)) = self.split_handles.get(&(path.clone(), index)) { - return handle.clone(); - } - let handle = cx.new(|_cx| SplitHandle::new(direction, path.clone(), index)); - let subscription = cx.subscribe(&handle, |this, _handle, event: &SplitHandleEvent, cx| { - match event { - SplitHandleEvent::ResizeRequested { path, index, ratio } => { - this.layout.resize_split_child(path, *index, *ratio); - this.emit_layout_changed(cx); - } - SplitHandleEvent::ResetRequested { path, index } => { - this.layout.resize_split_child(path, *index, SplitHandle::RESET_RATIO); - this.emit_layout_changed(cx); - } - } - }); - self.split_handles - .insert((path.clone(), index), (handle.clone(), subscription)); - handle - } + /// Gets (creating and subscribing on first use) the split-handle entity + /// for the boundary at `index` of the `Split` node at `path`. + fn split_handle_for( + &mut self, + path: &NodePath, + index: usize, + direction: Axis, + cx: &mut Context, + ) -> Entity { + if let Some((handle, _)) = self.split_handles.get(&(path.clone(), index)) { + return handle.clone(); + } + let handle = cx.new(|_cx| SplitHandle::new(direction, path.clone(), index)); + let subscription = + cx.subscribe( + &handle, + |this, _handle, event: &SplitHandleEvent, cx| match event { + SplitHandleEvent::ResizeRequested { path, index, ratio } => { + this.layout.resize_split_child(path, *index, *ratio); + this.emit_layout_changed(cx); + } + SplitHandleEvent::ResetRequested { path, index } => { + this.layout + .resize_split_child(path, *index, SplitHandle::RESET_RATIO); + this.emit_layout_changed(cx); + } + }, + ); + self.split_handles + .insert((path.clone(), index), (handle.clone(), subscription)); + handle + } - /// Routes a split-handle drag to the handle entity for the boundary at - /// `index` of the split at `path`. - fn route_split_drag( - &mut self, - path: &NodePath, - index: usize, - direction: Axis, - event: &DragMoveEvent, - cx: &mut Context, - ) { - let Some((handle, _)) = self.split_handles.get(&(path.clone(), index)) else { - return; - }; - // The handle drag works on the pair's own extent: start_ratio is the - // index child's share of the pair, and the drag delta is a fraction - // of the pair's combined on-screen extent. - let ratios = self.layout.split_ratios(path).unwrap_or_default(); - let total: f32 = ratios.iter().sum(); - let pair_total = ratios.get(index).copied().unwrap_or(0.0) - + ratios.get(index + 1).copied().unwrap_or(0.0); - let start_ratio = if pair_total > 0.0 { - ratios[index] / pair_total - } else { - SplitHandle::RESET_RATIO - }; - let full_extent = match direction { - Axis::Horizontal => event.bounds.size.width, - Axis::Vertical => event.bounds.size.height, - }; - let pair_extent = full_extent * pair_total / total.max(1.0); - let position = match direction { - Axis::Horizontal => event.event.position.x, - Axis::Vertical => event.event.position.y, - }; - let handle = handle.clone(); - handle.update(cx, |handle, cx| { - handle.drag_to(position, pair_extent, start_ratio, cx) - }); - } + /// Routes a split-handle drag to the handle entity for the boundary at + /// `index` of the split at `path`. + fn route_split_drag( + &mut self, + path: &NodePath, + index: usize, + direction: Axis, + event: &DragMoveEvent, + cx: &mut Context, + ) { + let Some((handle, _)) = self.split_handles.get(&(path.clone(), index)) else { + return; + }; + // The handle drag works on the pair's own extent: start_ratio is the + // index child's share of the pair, and the drag delta is a fraction + // of the pair's combined on-screen extent. + let ratios = self.layout.split_ratios(path).unwrap_or_default(); + let total: f32 = ratios.iter().sum(); + let pair_total = ratios.get(index).copied().unwrap_or(0.0) + + ratios.get(index + 1).copied().unwrap_or(0.0); + let start_ratio = if pair_total > 0.0 { + ratios[index] / pair_total + } else { + SplitHandle::RESET_RATIO + }; + let full_extent = match direction { + Axis::Horizontal => event.bounds.size.width, + Axis::Vertical => event.bounds.size.height, + }; + let pair_extent = full_extent * pair_total / total.max(1.0); + let position = match direction { + Axis::Horizontal => event.event.position.x, + Axis::Vertical => event.event.position.y, + }; + let handle = handle.clone(); + handle.update(cx, |handle, cx| { + handle.drag_to(position, pair_extent, start_ratio, cx) + }); + } - /// Ends a split-handle drag on the handle entity for the boundary at - /// `index` of the split at `path`. - fn end_split_drag( - &mut self, - path: &NodePath, - drag: &SplitHandleDrag, - cx: &mut Context, - ) { - if let Some((handle, _)) = self.split_handles.get(&(path.clone(), drag.index)) { - let handle = handle.clone(); - handle.update(cx, |handle, _cx| handle.end_drag()); - } - } + /// Ends a split-handle drag on the handle entity for the boundary at + /// `index` of the split at `path`. + fn end_split_drag(&mut self, path: &NodePath, drag: &SplitHandleDrag, cx: &mut Context) { + if let Some((handle, _)) = self.split_handles.get(&(path.clone(), drag.index)) { + let handle = handle.clone(); + handle.update(cx, |handle, _cx| handle.end_drag()); + } + } - /// Claims the hovered drop target for a panel container (`target` is the - /// panel, or the whole tab group, the container belongs to). - /// - /// Attached to every rendered panel container via `on_drag_move`; the - /// container's bounds come from the drag event itself, so no element - /// lookup is needed. Runs after the root's own `on_drag_move` (capture - /// phase), overriding any root-edge target with the exact per-panel one. - fn update_panel_drag( - &mut self, - target: &[PanelId], - event: &DragMoveEvent, - window: &Window, - cx: &mut Context, - ) { - let dragged = *event.drag(cx); - if target.contains(&dragged) { - // Dropping a panel onto itself or its own tab group is a no-op; - // clear any root-edge target the root handler may have set. - if let Some(drag) = self.drag.as_mut() { - drag.hovered = None; - drag.hovered_bounds = None; - } - return; - } - let Some(drag) = self.drag.as_mut() else { - return; - }; - if drag.panel != dragged { - return; - } - let target_panel = target.first().copied().unwrap_or(dragged); - drag.hovered = Some(DropTarget { - panel: Some(target_panel), - zone: DropZone::Center, - }); - drag.hovered_bounds = Some(event.bounds); - self.update_drag(event.event.position, window, cx); - } + /// Claims the hovered drop target for a panel container (`target` is the + /// panel, or the whole tab group, the container belongs to). + /// + /// Attached to every rendered panel container via `on_drag_move`; the + /// container's bounds come from the drag event itself, so no element + /// lookup is needed. Runs after the root's own `on_drag_move` (capture + /// phase), overriding any root-edge target with the exact per-panel one. + fn update_panel_drag( + &mut self, + target: &[PanelId], + event: &DragMoveEvent, + window: &Window, + cx: &mut Context, + ) { + let dragged = *event.drag(cx); + if target.contains(&dragged) { + // Dropping a panel onto itself or its own tab group is a no-op; + // clear any root-edge target the root handler may have set. + if let Some(drag) = self.drag.as_mut() { + drag.hovered = None; + drag.hovered_bounds = None; + } + return; + } + let Some(drag) = self.drag.as_mut() else { + return; + }; + if drag.panel != dragged { + return; + } + let target_panel = target.first().copied().unwrap_or(dragged); + drag.hovered = Some(DropTarget { + panel: Some(target_panel), + zone: DropZone::Center, + }); + drag.hovered_bounds = Some(event.bounds); + self.update_drag(event.event.position, window, cx); + } - /// Renders the subtree for `node` (at `path`), creating and syncing the - /// per-node chrome (tab bars, split handles) as it goes. - fn render_node(&mut self, node: &DockNode, path: &NodePath, cx: &mut Context) -> Stateful
{ - match node { - DockNode::Split { - direction, - ratios, - children, - } => { - let direction = *direction; - let mut container = div() - .flex() - .size_full() - .overflow_hidden() - .id(ElementId::named_usize("dock-split", path_key(path))) - .on_drag_move::( - cx.listener(move |this, event: &DragMoveEvent, _window, cx| { - let drag = event.drag(cx); - let path = drag.path.clone(); - let index = drag.index; - this.route_split_drag(&path, index, direction, event, cx); - }), - ) - .on_drop::( - cx.listener(move |this, drag: &SplitHandleDrag, _window, cx| { - this.end_split_drag(&drag.path, drag, cx); - }), - ); - match direction { - Axis::Horizontal => { - container = container.flex_row(); - } - Axis::Vertical => { - container = container.flex_col(); - } - } - for (index, child) in children.iter().enumerate() { - let mut child_path = path.clone(); - child_path.0.push(index); - let child = self.render_node(child, &child_path, cx); - // Each child is sized by its own ratio (the entries sum to - // 1.0), so a split with three or more panels keeps - // distinct sizes instead of flattening to one ratio. - let share = ratios - .get(index) - .copied() - .unwrap_or(1.0 / children.len() as f32); - let child = child - .flex_basis(relative(share)) - .flex_grow_0() - .flex_shrink_0(); - container = container.child(child); - // One handle per boundary, so every pair of panels can be - // resized independently. - if index + 1 < children.len() { - let handle = self.split_handle_for(path, index, direction, cx); - container = container.child(handle); - } - } - container - } - DockNode::Tabs { panels, active } => { - let active = (*active).min(panels.len().saturating_sub(1)); - let active_panel = panels[active]; - let target = panels.clone(); - let mut container = div() - .size_full() - .flex() - .flex_col() - .overflow_hidden() - .id(ElementId::named_usize("dock-panel", active_panel.raw() as usize)) - .on_drag_move::( - cx.listener(move |this, event: &DragMoveEvent, window, cx| { - this.update_panel_drag(&target, event, window, cx); - }), - ) - .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { - // The root's own drop listener is shadowed by panel - // hitboxes, so drops over panel content land here. - this.finish_drag(cx); - })); - let bar = self.tab_bar_for(path, panels, active, cx); - container = container.child(bar); - let content = self - .panels - .get(&active_panel) - .map(|handle| handle.view().clone()); - let mut content_wrapper = div().flex_1().min_h_0().overflow_hidden(); - if let Some(view) = content { - content_wrapper = content_wrapper.child(view); - } - container = container.child(content_wrapper); - container - } - DockNode::Panel(id) => { - let panel_id = *id; - let mut container = div() - .size_full() - .overflow_hidden() - .id(ElementId::named_usize("dock-panel", panel_id.raw() as usize)) - .on_drag_move::( - cx.listener(move |this, event: &DragMoveEvent, window, cx| { - this.update_panel_drag(&[panel_id], event, window, cx); - }), - ) - .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { - this.finish_drag(cx); - })); - if let Some(view) = self.panels.get(&panel_id).map(|handle| handle.view().clone()) { - container = container.child(view); - } - container - } - } - } + /// Renders the subtree for `node` (at `path`), creating and syncing the + /// per-node chrome (tab bars, split handles) as it goes. + fn render_node( + &mut self, + node: &DockNode, + path: &NodePath, + cx: &mut Context, + ) -> Stateful
{ + match node { + DockNode::Split { + direction, + ratios, + children, + } => { + let direction = *direction; + let mut container = div() + .flex() + .size_full() + .overflow_hidden() + .id(ElementId::named_usize("dock-split", path_key(path))) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, _window, cx| { + let drag = event.drag(cx); + let path = drag.path.clone(); + let index = drag.index; + this.route_split_drag(&path, index, direction, event, cx); + }, + )) + .on_drop::(cx.listener( + move |this, drag: &SplitHandleDrag, _window, cx| { + this.end_split_drag(&drag.path, drag, cx); + }, + )); + match direction { + Axis::Horizontal => { + container = container.flex_row(); + } + Axis::Vertical => { + container = container.flex_col(); + } + } + for (index, child) in children.iter().enumerate() { + let mut child_path = path.clone(); + child_path.0.push(index); + let child = self.render_node(child, &child_path, cx); + // Each child is sized by its own ratio (the entries sum to + // 1.0), so a split with three or more panels keeps + // distinct sizes instead of flattening to one ratio. + let share = ratios + .get(index) + .copied() + .unwrap_or(1.0 / children.len() as f32); + let child = child + .flex_basis(relative(share)) + .flex_grow_0() + .flex_shrink_0(); + container = container.child(child); + // One handle per boundary, so every pair of panels can be + // resized independently. + if index + 1 < children.len() { + let handle = self.split_handle_for(path, index, direction, cx); + container = container.child(handle); + } + } + container + } + DockNode::Tabs { panels, active } => { + let active = (*active).min(panels.len().saturating_sub(1)); + let active_panel = panels[active]; + let target = panels.clone(); + let mut container = div() + .size_full() + .flex() + .flex_col() + .overflow_hidden() + .id(ElementId::named_usize( + "dock-panel", + active_panel.raw() as usize, + )) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, window, cx| { + this.update_panel_drag(&target, event, window, cx); + }, + )) + .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { + // The root's own drop listener is shadowed by panel + // hitboxes, so drops over panel content land here. + this.finish_drag(cx); + })); + let bar = self.tab_bar_for(path, panels, active, cx); + container = container.child(bar); + let content = self + .panels + .get(&active_panel) + .map(|handle| handle.view().clone()); + let mut content_wrapper = div().flex_1().min_h_0().overflow_hidden(); + if let Some(view) = content { + content_wrapper = content_wrapper.child(view); + } + container = container.child(content_wrapper); + container + } + DockNode::Panel(id) => { + let panel_id = *id; + let mut container = div() + .size_full() + .overflow_hidden() + .id(ElementId::named_usize( + "dock-panel", + panel_id.raw() as usize, + )) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, window, cx| { + this.update_panel_drag(&[panel_id], event, window, cx); + }, + )) + .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { + this.finish_drag(cx); + })); + if let Some(view) = self + .panels + .get(&panel_id) + .map(|handle| handle.view().clone()) + { + container = container.child(view); + } + container + } + } + } } impl Render for DockArea { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // A drag that ended without a drop (released outside the dock area) - // leaves transient drag state behind; clear it on the next render. - if self.drag.is_some() && !cx.has_active_drag() { - self.drag = None; - } + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // A drag that ended without a drop (released outside the dock area) + // leaves transient drag state behind; clear it on the next render. + if self.drag.is_some() && !cx.has_active_drag() { + self.drag = None; + } - let mut root = div() - .size_full() - .relative() - .id("dock-area") - // Root-edge drop zones: hovering the outer band of the whole area - // splits the entire layout (target.panel: None). Per-panel - // containers run after this in the capture phase and override. - .on_drag_move::(cx.listener(|this, event: &DragMoveEvent, window, cx| { - let dragged = *event.drag(cx); - let Some(drag) = this.drag.as_mut() else { - return; - }; - if drag.panel != dragged { - return; - } - let candidate = match DockArea::drop_zone_at(event.event.position, event.bounds) { - Some(zone) if zone.is_split() => Some(DropTarget { panel: None, zone }), - _ => None, - }; - drag.hovered = candidate; - drag.hovered_bounds = Some(event.bounds); - this.update_drag(event.event.position, window, cx); - })) - .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { - this.finish_drag(cx); - })); + let mut root = div() + .size_full() + .relative() + .id("dock-area") + // Root-edge drop zones: hovering the outer band of the whole area + // splits the entire layout (target.panel: None). Per-panel + // containers run after this in the capture phase and override. + .on_drag_move::(cx.listener( + |this, event: &DragMoveEvent, window, cx| { + let dragged = *event.drag(cx); + let Some(drag) = this.drag.as_mut() else { + return; + }; + if drag.panel != dragged { + return; + } + let candidate = match DockArea::drop_zone_at(event.event.position, event.bounds) + { + Some(zone) if zone.is_split() => Some(DropTarget { panel: None, zone }), + _ => None, + }; + drag.hovered = candidate; + drag.hovered_bounds = Some(event.bounds); + this.update_drag(event.event.position, window, cx); + }, + )) + .on_drop::(cx.listener(|this, _drag: &PanelId, _window, cx| { + this.finish_drag(cx); + })); - if let Some(node) = self.layout.root().cloned() { - let path = NodePath::default(); - root = root.child(self.render_node(&node, &path, cx)); - } + if let Some(node) = self.layout.root().cloned() { + let path = NodePath::default(); + root = root.child(self.render_node(&node, &path, cx)); + } - // Drop chrome for nodes that no longer exist (their paths went stale - // after a structural edit); this also releases their subscriptions. - let mut live: HashSet = HashSet::new(); - self.collect_paths(&self.layout, &NodePath::default(), &mut live); - self.tab_bars.retain(|path, _| live.contains(path)); - self.split_handles - .retain(|(path, _), _| live.contains(path)); + // Drop chrome for nodes that no longer exist (their paths went stale + // after a structural edit); this also releases their subscriptions. + let mut live: HashSet = HashSet::new(); + self.collect_paths(&self.layout, &NodePath::default(), &mut live); + self.tab_bars.retain(|path, _| live.contains(path)); + self.split_handles + .retain(|(path, _), _| live.contains(path)); - if let Some(indicator) = self.render_drop_indicator(window, cx) { - root = root.child(indicator); - } - root - } + if let Some(indicator) = self.render_drop_indicator(window, cx) { + root = root.child(indicator); + } + root + } } impl Focusable for DockArea { - fn focus_handle(&self, cx: &App) -> FocusHandle { - let _ = cx; - self.focus_handle.clone() - } + fn focus_handle(&self, cx: &App) -> FocusHandle { + let _ = cx; + self.focus_handle.clone() + } } impl EventEmitter for DockArea {} @@ -965,69 +986,69 @@ impl EventEmitter for DockArea {} struct NoopPanelRegistry; impl PanelRegistry for NoopPanelRegistry { - fn panel_key(&self, _id: PanelId) -> Option { - None - } + fn panel_key(&self, _id: PanelId) -> Option { + None + } - fn build_panel(&self, _key: &str, _window: &mut Window, _cx: &mut App) -> Option { - None - } + fn build_panel(&self, _key: &str, _window: &mut Window, _cx: &mut App) -> Option { + None + } } impl DockArea { - /// Re-writes every panel id in `node` from its interim (key-hashed) id to - /// the rebuilt panel's real id, recording unmapped and duplicated ids for - /// removal by the caller (their nodes collapse via `DockLayout::cleanup`). - fn remap_node_ids( - node: &mut DockNode, - map: &HashMap, - used: &mut HashSet, - to_remove: &mut Vec, - ) { - match node { - DockNode::Panel(id) => Self::remap_id(id, map, used, to_remove), - DockNode::Tabs { panels, .. } => { - for panel in panels.iter_mut() { - Self::remap_id(panel, map, used, to_remove); - } - } - DockNode::Split { children, .. } => { - for child in children { - Self::remap_node_ids(child, map, used, to_remove); - } - } - } - } + /// Re-writes every panel id in `node` from its interim (key-hashed) id to + /// the rebuilt panel's real id, recording unmapped and duplicated ids for + /// removal by the caller (their nodes collapse via `DockLayout::cleanup`). + fn remap_node_ids( + node: &mut DockNode, + map: &HashMap, + used: &mut HashSet, + to_remove: &mut Vec, + ) { + match node { + DockNode::Panel(id) => Self::remap_id(id, map, used, to_remove), + DockNode::Tabs { panels, .. } => { + for panel in panels.iter_mut() { + Self::remap_id(panel, map, used, to_remove); + } + } + DockNode::Split { children, .. } => { + for child in children { + Self::remap_node_ids(child, map, used, to_remove); + } + } + } + } - fn remap_id( - id: &mut PanelId, - map: &HashMap, - used: &mut HashSet, - to_remove: &mut Vec, - ) { - let interim = *id; - match map.get(&interim) { - Some(&real) if used.insert(real) => *id = real, - _ => to_remove.push(interim), - } - } + fn remap_id( + id: &mut PanelId, + map: &HashMap, + used: &mut HashSet, + to_remove: &mut Vec, + ) { + let interim = *id; + match map.get(&interim) { + Some(&real) if used.insert(real) => *id = real, + _ => to_remove.push(interim), + } + } - /// Collects the paths of every node in `layout` (depth-first). - fn collect_paths(&self, layout: &DockLayout, path: &NodePath, out: &mut HashSet) { - let Some(root) = layout.root() else { - return; - }; - Self::collect_paths_in(root, path, out); - } + /// Collects the paths of every node in `layout` (depth-first). + fn collect_paths(&self, layout: &DockLayout, path: &NodePath, out: &mut HashSet) { + let Some(root) = layout.root() else { + return; + }; + Self::collect_paths_in(root, path, out); + } - fn collect_paths_in(node: &DockNode, path: &NodePath, out: &mut HashSet) { - out.insert(path.clone()); - if let DockNode::Split { children, .. } = node { - for (index, child) in children.iter().enumerate() { - let mut child_path = path.clone(); - child_path.0.push(index); - Self::collect_paths_in(child, &child_path, out); - } - } - } + fn collect_paths_in(node: &DockNode, path: &NodePath, out: &mut HashSet) { + out.insert(path.clone()); + if let DockNode::Split { children, .. } = node { + for (index, child) in children.iter().enumerate() { + let mut child_path = path.clone(); + child_path.0.push(index); + Self::collect_paths_in(child, &child_path, out); + } + } + } } diff --git a/crates/gpui/src/dock/floating.rs b/crates/gpui/src/dock/floating.rs index 8664c5c909..a442e5940a 100644 --- a/crates/gpui/src/dock/floating.rs +++ b/crates/gpui/src/dock/floating.rs @@ -27,12 +27,12 @@ //! Everything here is subject to change when the feature is implemented for //! real. +use crate::colors::DefaultColors; use crate::dock::PanelHandle; use crate::{ - div, px, Context, IntoElement, ParentElement, Pixels, Point, Render, Styled, Window, - WindowBounds, + Context, IntoElement, ParentElement, Pixels, Point, Render, Styled, Window, WindowBounds, div, + px, }; -use crate::colors::DefaultColors; /// A window hosting a single undocked panel. /// @@ -46,43 +46,53 @@ use crate::colors::DefaultColors; /// [`DockLayoutState`](crate::dock::DockLayoutState) can restore the window /// geometry. pub struct FloatingPanelWindow { - /// The panel hosted by this window. - panel: PanelHandle, - /// Last known window position, mirrored into layout snapshots. - #[allow(dead_code)] // read once floating-window geometry is persisted - origin: Point, + /// The panel hosted by this window. + panel: PanelHandle, + /// Last known window position, mirrored into layout snapshots. + #[allow(dead_code)] // read once floating-window geometry is persisted + origin: Point, } impl FloatingPanelWindow { - /// Creates the content view for a new floating window hosting `panel`. - /// - /// The caller is responsible for opening the window with - /// `cx.open_window` and remembering its handle so it can be closed when - /// the panel re-docks. `initial_bounds` comes from the saved layout, or - /// from a sensible default near the main window. - pub fn new( - panel: PanelHandle, - initial_bounds: Option, - cx: &mut Context, - ) -> Self { - let _ = cx; - let origin = initial_bounds - .map(|bounds| bounds.get_bounds().origin) - .unwrap_or_else(|| Point::new(px(0.0), px(0.0))); - Self { panel, origin } - } + /// Creates the content view for a new floating window hosting `panel`. + /// + /// The caller is responsible for opening the window with + /// `cx.open_window` and remembering its handle so it can be closed when + /// the panel re-docks. `initial_bounds` comes from the saved layout, or + /// from a sensible default near the main window. + pub fn new( + panel: PanelHandle, + initial_bounds: Option, + cx: &mut Context, + ) -> Self { + let _ = cx; + let origin = initial_bounds + .map(|bounds| bounds.get_bounds().origin) + .unwrap_or_else(|| Point::new(px(0.0), px(0.0))); + Self { panel, origin } + } } impl Render for FloatingPanelWindow { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let _ = window; - let title = self.panel.title().clone(); - let view = self.panel.view().clone(); - div().flex().flex_col().h_full().bg(cx.default_colors().clone().background) - .child( - div().flex().flex_row().items_center().w_full().px_2().py_1() - .child(div().flex_1().text_xs().truncate().child(title)), - ) - .child(div().flex_1().min_h_0().overflow_hidden().child(view)) - } + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let _ = window; + let title = self.panel.title().clone(); + let view = self.panel.view().clone(); + div() + .flex() + .flex_col() + .h_full() + .bg(cx.default_colors().clone().background) + .child( + div() + .flex() + .flex_row() + .items_center() + .w_full() + .px_2() + .py_1() + .child(div().flex_1().text_xs().truncate().child(title)), + ) + .child(div().flex_1().min_h_0().overflow_hidden().child(view)) + } } diff --git a/crates/gpui/src/dock/layout.rs b/crates/gpui/src/dock/layout.rs index 5edbbc8338..d5bee5ec65 100644 --- a/crates/gpui/src/dock/layout.rs +++ b/crates/gpui/src/dock/layout.rs @@ -39,31 +39,31 @@ use std::hash::{DefaultHasher, Hash, Hasher}; /// represented by [`DockLayout::root`] being `None`, never by empty nodes. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum DockNode { - /// A row or column of child nodes sized proportionally. - Split { - /// The axis along which children are laid out: [`Axis::Horizontal`] - /// places children side by side, [`Axis::Vertical`] stacks them. - direction: Axis, - /// The fraction of the available extent (along `direction`) assigned - /// to each child, in child order. Entries sum to `1.0`, so a split - /// with any number of panels can express distinct sizes (e.g. - /// `[0.5, 0.3, 0.2]` for three panels) instead of flattening to a - /// single shared ratio. Adjusted by dragging a split handle; see - /// [`DockLayout::resize_split_child`]. - ratios: Vec, - /// The children, in layout order. Never empty, never a single child, - /// and never contains a nested `Split` with the same `direction`. - children: Vec, - }, - /// A tab group showing one of several panels at a time. - Tabs { - /// Panels in tab order. Non-empty. - panels: Vec, - /// Index into `panels` of the visible tab. Always `< panels.len()`. - active: usize, - }, - /// A leaf holding exactly one panel. - Panel(PanelId), + /// A row or column of child nodes sized proportionally. + Split { + /// The axis along which children are laid out: [`Axis::Horizontal`] + /// places children side by side, [`Axis::Vertical`] stacks them. + direction: Axis, + /// The fraction of the available extent (along `direction`) assigned + /// to each child, in child order. Entries sum to `1.0`, so a split + /// with any number of panels can express distinct sizes (e.g. + /// `[0.5, 0.3, 0.2]` for three panels) instead of flattening to a + /// single shared ratio. Adjusted by dragging a split handle; see + /// [`DockLayout::resize_split_child`]. + ratios: Vec, + /// The children, in layout order. Never empty, never a single child, + /// and never contains a nested `Split` with the same `direction`. + children: Vec, + }, + /// A tab group showing one of several panels at a time. + Tabs { + /// Panels in tab order. Non-empty. + panels: Vec, + /// Index into `panels` of the visible tab. Always `< panels.len()`. + active: usize, + }, + /// A leaf holding exactly one panel. + Panel(PanelId), } /// Where, relative to a drop target, a dragged panel should be docked. @@ -74,52 +74,52 @@ pub enum DockNode { /// [`DockArea::drop_zone_at`](crate::dock::DockArea::drop_zone_at). #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DropZone { - /// Dock to the left of the target: split the target's node horizontally, - /// inserting the dragged panel as the new left child. - Left, - /// Dock to the right of the target (horizontal split, new right child). - Right, - /// Dock above the target (vertical split, new top child). - Top, - /// Dock below the target (vertical split, new bottom child). - Bottom, - /// Merge the dragged panel into the target as a new tab. The dragged - /// panel becomes the active tab of the resulting group. - Center, + /// Dock to the left of the target: split the target's node horizontally, + /// inserting the dragged panel as the new left child. + Left, + /// Dock to the right of the target (horizontal split, new right child). + Right, + /// Dock above the target (vertical split, new top child). + Top, + /// Dock below the target (vertical split, new bottom child). + Bottom, + /// Merge the dragged panel into the target as a new tab. The dragged + /// panel becomes the active tab of the resulting group. + Center, } impl DropZone { - /// All five zones, in declaration order. Useful for painting affordances. - pub const ALL: [DropZone; 5] = [ - DropZone::Left, - DropZone::Right, - DropZone::Top, - DropZone::Bottom, - DropZone::Center, - ]; + /// All five zones, in declaration order. Useful for painting affordances. + pub const ALL: [DropZone; 5] = [ + DropZone::Left, + DropZone::Right, + DropZone::Top, + DropZone::Bottom, + DropZone::Center, + ]; - /// Returns `true` if this zone splits the target rather than merging into - /// it as a tab — i.e. anything except [`DropZone::Center`]. - pub const fn is_split(self) -> bool { - !matches!(self, DropZone::Center) - } + /// Returns `true` if this zone splits the target rather than merging into + /// it as a tab — i.e. anything except [`DropZone::Center`]. + pub const fn is_split(self) -> bool { + !matches!(self, DropZone::Center) + } - /// Returns `true` if this zone merges the dragged panel into the target - /// as a tab ([`DropZone::Center`]). - pub const fn is_merge(self) -> bool { - matches!(self, DropZone::Center) - } + /// Returns `true` if this zone merges the dragged panel into the target + /// as a tab ([`DropZone::Center`]). + pub const fn is_merge(self) -> bool { + matches!(self, DropZone::Center) + } - /// Returns the split axis this zone implies, or `None` for - /// [`DropZone::Center`]. `Left`/`Right` split along - /// [`Axis::Horizontal`], `Top`/`Bottom` along [`Axis::Vertical`]. - pub const fn split_axis(self) -> Option { - match self { - DropZone::Left | DropZone::Right => Some(Axis::Horizontal), - DropZone::Top | DropZone::Bottom => Some(Axis::Vertical), - DropZone::Center => None, - } - } + /// Returns the split axis this zone implies, or `None` for + /// [`DropZone::Center`]. `Left`/`Right` split along + /// [`Axis::Horizontal`], `Top`/`Bottom` along [`Axis::Vertical`]. + pub const fn split_axis(self) -> Option { + match self { + DropZone::Left | DropZone::Right => Some(Axis::Horizontal), + DropZone::Top | DropZone::Bottom => Some(Axis::Vertical), + DropZone::Center => None, + } + } } /// A concrete place a dragged panel can be dropped. @@ -131,11 +131,11 @@ impl DropZone { /// `panel` is `None`. #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct DropTarget { - /// The panel whose tab group / leaf is targeted, or `None` to target an - /// outer edge of the root. - pub panel: Option, - /// The zone within the target. - pub zone: DropZone, + /// The panel whose tab group / leaf is targeted, or `None` to target an + /// outer edge of the root. + pub panel: Option, + /// The zone within the target. + pub zone: DropZone, } /// Path addressing a node within a [`DockLayout`]: child indices from the root. @@ -153,9 +153,9 @@ pub struct NodePath(pub Vec); /// Two different paths always produce different keys; equal paths produce /// equal keys. pub(crate) fn path_key(path: &NodePath) -> usize { - let mut hasher = DefaultHasher::new(); - path.0.hash(&mut hasher); - hasher.finish() as usize + let mut hasher = DefaultHasher::new(); + path.0.hash(&mut hasher); + hasher.finish() as usize } /// The dock layout tree: the shape of one [`DockArea`](crate::dock::DockArea)'s workspace. @@ -166,565 +166,560 @@ pub(crate) fn path_key(path: &NodePath) -> usize { /// [`cleanup`](DockLayout::cleanup) themselves. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct DockLayout { - root: Option, + root: Option, } impl DockLayout { - /// Creates an empty layout (no root node). - pub fn new() -> Self { - Self { root: None } - } + /// Creates an empty layout (no root node). + pub fn new() -> Self { + Self { root: None } + } - /// Returns the root node, or `None` if the layout is empty. - pub fn root(&self) -> Option<&DockNode> { - self.root.as_ref() - } + /// Returns the root node, or `None` if the layout is empty. + pub fn root(&self) -> Option<&DockNode> { + self.root.as_ref() + } - /// Returns the root node mutably, or `None` if the layout is empty. - pub(crate) fn root_mut(&mut self) -> Option<&mut DockNode> { - self.root.as_mut() - } + /// Returns the root node mutably, or `None` if the layout is empty. + pub(crate) fn root_mut(&mut self) -> Option<&mut DockNode> { + self.root.as_mut() + } - /// Returns `true` if `panel` occurs anywhere in the tree. - pub fn contains(&self, panel: PanelId) -> bool { - let Some(root) = self.root.as_ref() else { - return false; - }; - let mut stack = vec![root]; - while let Some(node) = stack.pop() { - match node { - DockNode::Panel(id) => { - if *id == panel { - return true; - } - } - DockNode::Tabs { panels, .. } => { - if panels.contains(&panel) { - return true; - } - } - DockNode::Split { children, .. } => stack.extend(children), - } - } - false - } + /// Returns `true` if `panel` occurs anywhere in the tree. + pub fn contains(&self, panel: PanelId) -> bool { + let Some(root) = self.root.as_ref() else { + return false; + }; + let mut stack = vec![root]; + while let Some(node) = stack.pop() { + match node { + DockNode::Panel(id) => { + if *id == panel { + return true; + } + } + DockNode::Tabs { panels, .. } => { + if panels.contains(&panel) { + return true; + } + } + DockNode::Split { children, .. } => stack.extend(children), + } + } + false + } - /// Returns the path to the node containing `panel`, or `None` if the - /// panel is not in the tree. - /// - /// For a `Panel` leaf the path addresses the leaf itself; for a tab in a - /// `Tabs` group it addresses the `Tabs` node (inspect the node to get the - /// tab index). - pub fn find_panel(&self, panel: PanelId) -> Option { - let root = self.root.as_ref()?; - Self::find_panel_in(root, panel, &mut Vec::new()) - } + /// Returns the path to the node containing `panel`, or `None` if the + /// panel is not in the tree. + /// + /// For a `Panel` leaf the path addresses the leaf itself; for a tab in a + /// `Tabs` group it addresses the `Tabs` node (inspect the node to get the + /// tab index). + pub fn find_panel(&self, panel: PanelId) -> Option { + let root = self.root.as_ref()?; + Self::find_panel_in(root, panel, &mut Vec::new()) + } - /// Depth-first search recording the path taken; the path of the node - /// containing `panel` when found. - fn find_panel_in(node: &DockNode, panel: PanelId, path: &mut Vec) -> Option { - match node { - DockNode::Panel(id) if *id == panel => Some(NodePath(path.clone())), - DockNode::Panel(_) => None, - DockNode::Tabs { panels, .. } => { - if panels.contains(&panel) { - Some(NodePath(path.clone())) - } else { - None - } - } - DockNode::Split { children, .. } => { - for (index, child) in children.iter().enumerate() { - path.push(index); - if let Some(found) = Self::find_panel_in(child, panel, path) { - return Some(found); - } - path.pop(); - } - None - } - } - } + /// Depth-first search recording the path taken; the path of the node + /// containing `panel` when found. + fn find_panel_in(node: &DockNode, panel: PanelId, path: &mut Vec) -> Option { + match node { + DockNode::Panel(id) if *id == panel => Some(NodePath(path.clone())), + DockNode::Panel(_) => None, + DockNode::Tabs { panels, .. } => { + if panels.contains(&panel) { + Some(NodePath(path.clone())) + } else { + None + } + } + DockNode::Split { children, .. } => { + for (index, child) in children.iter().enumerate() { + path.push(index); + if let Some(found) = Self::find_panel_in(child, panel, path) { + return Some(found); + } + path.pop(); + } + None + } + } + } - /// Inserts `panel` at `target`. - /// - /// - `target: None` — the layout must be empty; `panel` becomes the root. - /// If the layout is *not* empty this is a no-op returning `false` - /// (choose a concrete [`DropTarget`] instead). - /// - Edge zones — wraps/splits the target node along - /// [`DropZone::split_axis`]. If the target already sits inside a split - /// with the same axis, the panel is inserted as a sibling instead of - /// nesting. - /// - [`DropZone::Center`] — appends `panel` to the target's tab group - /// (converting a `Panel` leaf into `Tabs`) and makes it active. - /// - /// Returns `true` if the tree changed. Fails (returns `false`) if - /// `panel` is already present — use [`move_panel`](DockLayout::move_panel) - /// to relocate — or if the target panel no longer exists. - /// - /// # Panics - /// - /// Panics in debug builds if `target.panel` is `None` and - /// `target.zone` is [`DropZone::Center`]. - pub fn insert_panel(&mut self, panel: PanelId, target: Option) -> bool { - if self.contains(panel) { - return false; - } + /// Inserts `panel` at `target`. + /// + /// - `target: None` — the layout must be empty; `panel` becomes the root. + /// If the layout is *not* empty this is a no-op returning `false` + /// (choose a concrete [`DropTarget`] instead). + /// - Edge zones — wraps/splits the target node along + /// [`DropZone::split_axis`]. If the target already sits inside a split + /// with the same axis, the panel is inserted as a sibling instead of + /// nesting. + /// - [`DropZone::Center`] — appends `panel` to the target's tab group + /// (converting a `Panel` leaf into `Tabs`) and makes it active. + /// + /// Returns `true` if the tree changed. Fails (returns `false`) if + /// `panel` is already present — use [`move_panel`](DockLayout::move_panel) + /// to relocate — or if the target panel no longer exists. + /// + /// # Panics + /// + /// Panics in debug builds if `target.panel` is `None` and + /// `target.zone` is [`DropZone::Center`]. + pub fn insert_panel(&mut self, panel: PanelId, target: Option) -> bool { + if self.contains(panel) { + return false; + } - let Some(target) = target else { - if self.root.is_some() { - return false; - } - self.root = Some(DockNode::Panel(panel)); - return true; - }; + let Some(target) = target else { + if self.root.is_some() { + return false; + } + self.root = Some(DockNode::Panel(panel)); + return true; + }; - debug_assert!( - target.panel.is_some() || target.zone != DropZone::Center, - "a root-edge target must use an edge zone, never Center" - ); + debug_assert!( + target.panel.is_some() || target.zone != DropZone::Center, + "a root-edge target must use an edge zone, never Center" + ); - let Some(target_panel) = target.panel else { - // An outer edge of the whole tree: split the root itself. - let changed = self.insert_at_root_edge(panel, target.zone); - if changed { - self.cleanup(); - } - return changed; - }; + let Some(target_panel) = target.panel else { + // An outer edge of the whole tree: split the root itself. + let changed = self.insert_at_root_edge(panel, target.zone); + if changed { + self.cleanup(); + } + return changed; + }; - if !self.contains(target_panel) { - return false; - } + if !self.contains(target_panel) { + return false; + } - if target.zone.is_merge() { - self.insert_into_tabs(panel, target_panel); - } else { - self.insert_at_edge(panel, target_panel, target.zone); - } - self.cleanup(); - true - } + if target.zone.is_merge() { + self.insert_into_tabs(panel, target_panel); + } else { + self.insert_at_edge(panel, target_panel, target.zone); + } + self.cleanup(); + true + } - /// Inserts `panel` at an outer edge of the root, splitting the whole tree - /// unless the root is already a split along the same axis (in which case - /// the panel becomes a sibling). - fn insert_at_root_edge(&mut self, panel: PanelId, zone: DropZone) -> bool { - let Some(mut root) = self.root.take() else { - return false; - }; - let axis = zone - .split_axis() - .expect("root-edge zones always imply a split axis"); - let before = matches!(zone, DropZone::Left | DropZone::Top); + /// Inserts `panel` at an outer edge of the root, splitting the whole tree + /// unless the root is already a split along the same axis (in which case + /// the panel becomes a sibling). + fn insert_at_root_edge(&mut self, panel: PanelId, zone: DropZone) -> bool { + let Some(mut root) = self.root.take() else { + return false; + }; + let axis = zone + .split_axis() + .expect("root-edge zones always imply a split axis"); + let before = matches!(zone, DropZone::Left | DropZone::Top); - if let DockNode::Split { - direction, - ratios, - children, - } = &mut root - { - if *direction == axis { - if before { - split_ratio_at(ratios, 0); - children.insert(0, DockNode::Panel(panel)); - } else { - split_ratio_at(ratios, ratios.len() - 1); - children.push(DockNode::Panel(panel)); - } - self.root = Some(root); - return true; - } - } + if let DockNode::Split { + direction, + ratios, + children, + } = &mut root + { + if *direction == axis { + if before { + split_ratio_at(ratios, 0); + children.insert(0, DockNode::Panel(panel)); + } else { + split_ratio_at(ratios, ratios.len() - 1); + children.push(DockNode::Panel(panel)); + } + self.root = Some(root); + return true; + } + } - self.root = Some(DockNode::Split { - direction: axis, - ratios: vec![0.5, 0.5], - children: if before { - vec![DockNode::Panel(panel), root] - } else { - vec![root, DockNode::Panel(panel)] - }, - }); - true - } + self.root = Some(DockNode::Split { + direction: axis, + ratios: vec![0.5, 0.5], + children: if before { + vec![DockNode::Panel(panel), root] + } else { + vec![root, DockNode::Panel(panel)] + }, + }); + true + } - /// Inserts `panel` relative to the node containing `target_panel`, - /// splitting it unless it already sits in a same-axis split (then the - /// panel is inserted as a sibling instead). - fn insert_at_edge(&mut self, panel: PanelId, target_panel: PanelId, zone: DropZone) { - let axis = zone.split_axis().expect("edge zones imply a split axis"); - let path = self - .find_panel(target_panel) - .expect("caller checked the target exists"); - let before = matches!(zone, DropZone::Left | DropZone::Top); + /// Inserts `panel` relative to the node containing `target_panel`, + /// splitting it unless it already sits in a same-axis split (then the + /// panel is inserted as a sibling instead). + fn insert_at_edge(&mut self, panel: PanelId, target_panel: PanelId, zone: DropZone) { + let axis = zone.split_axis().expect("edge zones imply a split axis"); + let path = self + .find_panel(target_panel) + .expect("caller checked the target exists"); + let before = matches!(zone, DropZone::Left | DropZone::Top); - // If the target sits inside a split along the same axis, insert the - // panel as a sibling rather than nesting a split inside a split. The - // new panel splits the target child's share in half, so every child - // keeps a distinct, independently resizable ratio. - let mut parent = path.0.clone(); - if parent.pop().is_some() { - let parent_path = NodePath(parent); - if let Some(DockNode::Split { - direction, - ratios, - children, - .. - }) = self.node_at_mut(&parent_path) - { - if *direction == axis { - let index = *path.0.last().expect("non-root path has a last index"); - split_ratio_at(ratios, index); - children.insert( - if before { index } else { index + 1 }, - DockNode::Panel(panel), - ); - return; - } - } - } + // If the target sits inside a split along the same axis, insert the + // panel as a sibling rather than nesting a split inside a split. The + // new panel splits the target child's share in half, so every child + // keeps a distinct, independently resizable ratio. + let mut parent = path.0.clone(); + if parent.pop().is_some() { + let parent_path = NodePath(parent); + if let Some(DockNode::Split { + direction, + ratios, + children, + .. + }) = self.node_at_mut(&parent_path) + { + if *direction == axis { + let index = *path.0.last().expect("non-root path has a last index"); + split_ratio_at(ratios, index); + children.insert( + if before { index } else { index + 1 }, + DockNode::Panel(panel), + ); + return; + } + } + } - // Otherwise wrap the target node in a fresh split. - let old_node = self - .node_at(&path) - .expect("path was just derived") - .clone(); - let new_node = DockNode::Panel(panel); - let replacement = DockNode::Split { - direction: axis, - ratios: vec![0.5, 0.5], - children: if before { - vec![new_node, old_node] - } else { - vec![old_node, new_node] - }, - }; - *self - .node_at_mut(&path) - .expect("path was just derived") = replacement; - } + // Otherwise wrap the target node in a fresh split. + let old_node = self.node_at(&path).expect("path was just derived").clone(); + let new_node = DockNode::Panel(panel); + let replacement = DockNode::Split { + direction: axis, + ratios: vec![0.5, 0.5], + children: if before { + vec![new_node, old_node] + } else { + vec![old_node, new_node] + }, + }; + *self.node_at_mut(&path).expect("path was just derived") = replacement; + } - /// Appends `panel` to the tab group containing `target_panel` (converting - /// a `Panel` leaf into a `Tabs` node) and makes it active. - fn insert_into_tabs(&mut self, panel: PanelId, target_panel: PanelId) { - let path = self - .find_panel(target_panel) - .expect("caller checked the target exists"); - let node = self.node_at_mut(&path).expect("path was just derived"); - match node { - DockNode::Panel(_) => { - *node = DockNode::Tabs { - panels: vec![target_panel, panel], - active: 1, - }; - } - DockNode::Tabs { panels, active } => { - panels.push(panel); - *active = panels.len() - 1; - } - DockNode::Split { .. } => unreachable!("find_panel never addresses a Split"), - } - } + /// Appends `panel` to the tab group containing `target_panel` (converting + /// a `Panel` leaf into a `Tabs` node) and makes it active. + fn insert_into_tabs(&mut self, panel: PanelId, target_panel: PanelId) { + let path = self + .find_panel(target_panel) + .expect("caller checked the target exists"); + let node = self.node_at_mut(&path).expect("path was just derived"); + match node { + DockNode::Panel(_) => { + *node = DockNode::Tabs { + panels: vec![target_panel, panel], + active: 1, + }; + } + DockNode::Tabs { panels, active } => { + panels.push(panel); + *active = panels.len() - 1; + } + DockNode::Split { .. } => unreachable!("find_panel never addresses a Split"), + } + } - /// Removes `panel` from the tree, running [`cleanup`](DockLayout::cleanup) - /// to collapse nodes left empty or single-childed. - /// - /// Returns `true` if the panel was present. Removing the last panel sets - /// the root to `None`. - pub fn remove_panel(&mut self, panel: PanelId) -> bool { - let Some(root) = self.root.as_mut() else { - return false; - }; - if !Self::remove_from_node(root, panel) { - return false; - } - self.cleanup(); - true - } + /// Removes `panel` from the tree, running [`cleanup`](DockLayout::cleanup) + /// to collapse nodes left empty or single-childed. + /// + /// Returns `true` if the panel was present. Removing the last panel sets + /// the root to `None`. + pub fn remove_panel(&mut self, panel: PanelId) -> bool { + let Some(root) = self.root.as_mut() else { + return false; + }; + if !Self::remove_from_node(root, panel) { + return false; + } + self.cleanup(); + true + } - /// Recursively removes `panel` from `node`, returning whether it was found. - fn remove_from_node(node: &mut DockNode, panel: PanelId) -> bool { - match node { - DockNode::Panel(id) => *id == panel, - DockNode::Tabs { panels, active } => { - let Some(index) = panels.iter().position(|&other| other == panel) else { - return false; - }; - panels.remove(index); - if !panels.is_empty() { - *active = (*active).min(panels.len() - 1); - } - true - } - DockNode::Split { - ratios, children, .. - } => { - for (index, child) in children.iter_mut().enumerate() { - // Only a direct `Panel` leaf (or an emptied node below) - // is dropped from this split; the collapsed child's ratio - // is dropped with it and the rest renormalized so the - // freed space is redistributed proportionally. - let is_direct_leaf = matches!(child, DockNode::Panel(_)); - let removed = match child { - DockNode::Panel(id) => *id == panel, - other => Self::remove_from_node(other, panel), - }; - if removed { - if is_direct_leaf { - children.remove(index); - if index < ratios.len() { - ratios.remove(index); - } - renormalize_ratios(ratios); - } - return true; - } - } - false - } - } - } + /// Recursively removes `panel` from `node`, returning whether it was found. + fn remove_from_node(node: &mut DockNode, panel: PanelId) -> bool { + match node { + DockNode::Panel(id) => *id == panel, + DockNode::Tabs { panels, active } => { + let Some(index) = panels.iter().position(|&other| other == panel) else { + return false; + }; + panels.remove(index); + if !panels.is_empty() { + *active = (*active).min(panels.len() - 1); + } + true + } + DockNode::Split { + ratios, children, .. + } => { + for (index, child) in children.iter_mut().enumerate() { + // Only a direct `Panel` leaf (or an emptied node below) + // is dropped from this split; the collapsed child's ratio + // is dropped with it and the rest renormalized so the + // freed space is redistributed proportionally. + let is_direct_leaf = matches!(child, DockNode::Panel(_)); + let removed = match child { + DockNode::Panel(id) => *id == panel, + other => Self::remove_from_node(other, panel), + }; + if removed { + if is_direct_leaf { + children.remove(index); + if index < ratios.len() { + ratios.remove(index); + } + renormalize_ratios(ratios); + } + return true; + } + } + false + } + } + } - /// Atomically moves `panel` to `target`. - /// - /// Equivalent to [`remove_panel`](DockLayout::remove_panel) followed by - /// [`insert_panel`](DockLayout::insert_panel), but a no-op (returning - /// `false`) if `panel` is not present or the drop would land the panel - /// back in its own position (e.g. `Center` onto its own group). Moving - /// the only panel of a group away collapses the group. - pub fn move_panel(&mut self, panel: PanelId, target: DropTarget) -> bool { - if !self.contains(panel) { - return false; - } - // Dropping onto the panel's own node — its group for `Center`, its own - // node for edge zones — would target a node that no longer exists - // after the removal below, so treat every self-drop as a no-op. - if target.panel == Some(panel) { - return false; - } - let removed = self.remove_panel(panel); - debug_assert!(removed, "panel presence was checked above"); - let inserted = self.insert_panel(panel, Some(target)); - debug_assert!(inserted, "drop target must remain valid after the removal"); - inserted - } + /// Atomically moves `panel` to `target`. + /// + /// Equivalent to [`remove_panel`](DockLayout::remove_panel) followed by + /// [`insert_panel`](DockLayout::insert_panel), but a no-op (returning + /// `false`) if `panel` is not present or the drop would land the panel + /// back in its own position (e.g. `Center` onto its own group). Moving + /// the only panel of a group away collapses the group. + pub fn move_panel(&mut self, panel: PanelId, target: DropTarget) -> bool { + if !self.contains(panel) { + return false; + } + // Dropping onto the panel's own node — its group for `Center`, its own + // node for edge zones — would target a node that no longer exists + // after the removal below, so treat every self-drop as a no-op. + if target.panel == Some(panel) { + return false; + } + let removed = self.remove_panel(panel); + debug_assert!(removed, "panel presence was checked above"); + let inserted = self.insert_panel(panel, Some(target)); + debug_assert!(inserted, "drop target must remain valid after the removal"); + inserted + } - /// Sets the share of the first child of the `Split` node at `path` and - /// redistributes the remaining extent proportionally among the other - /// children, keeping the `Split.ratios` sum at `1.0`. - /// - /// This two-argument form is kept for source compatibility with callers - /// that only resize a two-panel split (where "first child's share" and - /// "share of the first pair" coincide). For splits with three or more - /// children — or when only one boundary should move — use - /// [`resize_split_child`](Self::resize_split_child), which adjusts a - /// single pair without touching the others. - /// - /// # Panics - /// - /// Panics if `path` does not address a [`DockNode::Split`]. - pub fn resize_split(&mut self, path: &NodePath, ratio: f32) { - let ratio = ratio.clamp(0.05, 0.95); - let Some(node) = self.node_at_mut(path) else { - panic!("resize_split: path {path:?} does not address a node"); - }; - let DockNode::Split { ratios, .. } = node else { - panic!("resize_split: path {path:?} does not address a Split node"); - }; - let rest: f32 = ratios.iter().skip(1).sum(); - let Some(first) = ratios.first_mut() else { - panic!("resize_split: split at {path:?} has no children"); - }; - *first = ratio; - if rest > 0.0 { - let scale = (1.0 - ratio) / rest; - for share in ratios.iter_mut().skip(1) { - *share *= scale; - } - } - } + /// Sets the share of the first child of the `Split` node at `path` and + /// redistributes the remaining extent proportionally among the other + /// children, keeping the `Split.ratios` sum at `1.0`. + /// + /// This two-argument form is kept for source compatibility with callers + /// that only resize a two-panel split (where "first child's share" and + /// "share of the first pair" coincide). For splits with three or more + /// children — or when only one boundary should move — use + /// [`resize_split_child`](Self::resize_split_child), which adjusts a + /// single pair without touching the others. + /// + /// # Panics + /// + /// Panics if `path` does not address a [`DockNode::Split`]. + pub fn resize_split(&mut self, path: &NodePath, ratio: f32) { + let ratio = ratio.clamp(0.05, 0.95); + let Some(node) = self.node_at_mut(path) else { + panic!("resize_split: path {path:?} does not address a node"); + }; + let DockNode::Split { ratios, .. } = node else { + panic!("resize_split: path {path:?} does not address a Split node"); + }; + let rest: f32 = ratios.iter().skip(1).sum(); + let Some(first) = ratios.first_mut() else { + panic!("resize_split: split at {path:?} has no children"); + }; + *first = ratio; + if rest > 0.0 { + let scale = (1.0 - ratio) / rest; + for share in ratios.iter_mut().skip(1) { + *share *= scale; + } + } + } - /// Sets the share of child `index` within its pair (children `index` and - /// `index + 1`) of the `Split` node at `path`. - /// - /// `ratio` is the fraction of the pair's combined extent given to child - /// `index` (so `0.5` makes the pair even); the two children's entries are - /// rewritten proportionally and the rest of the split is untouched, so - /// each panel keeps its own distinct ratio even when the split has three - /// or more children. `ratio` is clamped to `[0.05, 0.95]` so neither - /// child can be squeezed out entirely; pixel-level minimum extents (see - /// the min-size constants in `split_handle`) are enforced by the caller. - /// - /// # Panics - /// - /// Panics if `path` does not address a [`DockNode::Split`], or if - /// `index` is not a boundary between two children. - pub fn resize_split_child(&mut self, path: &NodePath, index: usize, ratio: f32) { - let Some(node) = self.node_at_mut(path) else { - panic!("resize_split: path {path:?} does not address a node"); - }; - let DockNode::Split { ratios, .. } = node else { - panic!("resize_split: path {path:?} does not address a Split node"); - }; - assert!( - index + 1 < ratios.len(), - "resize_split_child: boundary {index} out of range for a split with {} children", - ratios.len(), - ); - let ratio = ratio.clamp(0.05, 0.95); - let pair = ratios[index] + ratios[index + 1]; - ratios[index] = ratio * pair; - ratios[index + 1] = (1.0 - ratio) * pair; - } + /// Sets the share of child `index` within its pair (children `index` and + /// `index + 1`) of the `Split` node at `path`. + /// + /// `ratio` is the fraction of the pair's combined extent given to child + /// `index` (so `0.5` makes the pair even); the two children's entries are + /// rewritten proportionally and the rest of the split is untouched, so + /// each panel keeps its own distinct ratio even when the split has three + /// or more children. `ratio` is clamped to `[0.05, 0.95]` so neither + /// child can be squeezed out entirely; pixel-level minimum extents (see + /// the min-size constants in `split_handle`) are enforced by the caller. + /// + /// # Panics + /// + /// Panics if `path` does not address a [`DockNode::Split`], or if + /// `index` is not a boundary between two children. + pub fn resize_split_child(&mut self, path: &NodePath, index: usize, ratio: f32) { + let Some(node) = self.node_at_mut(path) else { + panic!("resize_split: path {path:?} does not address a node"); + }; + let DockNode::Split { ratios, .. } = node else { + panic!("resize_split: path {path:?} does not address a Split node"); + }; + assert!( + index + 1 < ratios.len(), + "resize_split_child: boundary {index} out of range for a split with {} children", + ratios.len(), + ); + let ratio = ratio.clamp(0.05, 0.95); + let pair = ratios[index] + ratios[index + 1]; + ratios[index] = ratio * pair; + ratios[index + 1] = (1.0 - ratio) * pair; + } - /// Re-establishes the [`DockNode`] invariants after structural edits. - /// - /// Concretely: removes empty `Tabs` nodes, replaces single-child `Split`s - /// with their child, flattens same-direction `Split` nests, and clamps - /// `active` tab indices into range. All public mutators call this - /// internally; call it manually only after mutating nodes obtained via - /// interior references (which the API avoids exposing for this reason). - pub fn cleanup(&mut self) { - self.root = self.root.take().and_then(Self::cleanup_node); - } + /// Re-establishes the [`DockNode`] invariants after structural edits. + /// + /// Concretely: removes empty `Tabs` nodes, replaces single-child `Split`s + /// with their child, flattens same-direction `Split` nests, and clamps + /// `active` tab indices into range. All public mutators call this + /// internally; call it manually only after mutating nodes obtained via + /// interior references (which the API avoids exposing for this reason). + pub fn cleanup(&mut self) { + self.root = self.root.take().and_then(Self::cleanup_node); + } - /// Normalizes a single node, returning `None` when it collapses away. - fn cleanup_node(node: DockNode) -> Option { - match node { - DockNode::Panel(_) => Some(node), - DockNode::Tabs { - mut panels, - mut active, - } => { - if panels.is_empty() { - return None; - } - active = active.min(panels.len() - 1); - Some(DockNode::Tabs { panels, active }) - } - DockNode::Split { - direction, - mut ratios, - children, - } => { - let mut clean_children = Vec::new(); - let mut clean_ratios = Vec::new(); - for (index, child) in children.into_iter().enumerate() { - // A child that collapsed away (empty tabs, removed leaf) - // frees its share; the rest are renormalized below. - let Some(child) = Self::cleanup_node(child) else { - continue; - }; - let child_share = ratios.get(index).copied().unwrap_or(0.5); - match child { - DockNode::Split { - direction: nested_direction, - ratios: nested_ratios, - children: nested_children, - } if nested_direction == direction => { - // Flatten direct same-direction split nests, - // scaling the nested ratios by this child's share - // so the relative proportions are preserved. - for (nested_child, nested_ratio) in - nested_children.into_iter().zip(nested_ratios) - { - clean_children.push(nested_child); - clean_ratios.push(child_share * nested_ratio); - } - } - other => { - clean_children.push(other); - clean_ratios.push(child_share); - } - } - } - // Re-establish the "sum to 1" invariant: drops collapsed - // children's freed space proportionally and absorbs drift. - renormalize_ratios(&mut clean_ratios); - match clean_children.len() { - 0 => None, - 1 => Some(clean_children.pop().expect("len == 1")), - _ => Some(DockNode::Split { - direction, - ratios: clean_ratios, - children: clean_children, - }), - } - } - } - } + /// Normalizes a single node, returning `None` when it collapses away. + fn cleanup_node(node: DockNode) -> Option { + match node { + DockNode::Panel(_) => Some(node), + DockNode::Tabs { + mut panels, + mut active, + } => { + if panels.is_empty() { + return None; + } + active = active.min(panels.len() - 1); + Some(DockNode::Tabs { panels, active }) + } + DockNode::Split { + direction, + mut ratios, + children, + } => { + let mut clean_children = Vec::new(); + let mut clean_ratios = Vec::new(); + for (index, child) in children.into_iter().enumerate() { + // A child that collapsed away (empty tabs, removed leaf) + // frees its share; the rest are renormalized below. + let Some(child) = Self::cleanup_node(child) else { + continue; + }; + let child_share = ratios.get(index).copied().unwrap_or(0.5); + match child { + DockNode::Split { + direction: nested_direction, + ratios: nested_ratios, + children: nested_children, + } if nested_direction == direction => { + // Flatten direct same-direction split nests, + // scaling the nested ratios by this child's share + // so the relative proportions are preserved. + for (nested_child, nested_ratio) in + nested_children.into_iter().zip(nested_ratios) + { + clean_children.push(nested_child); + clean_ratios.push(child_share * nested_ratio); + } + } + other => { + clean_children.push(other); + clean_ratios.push(child_share); + } + } + } + // Re-establish the "sum to 1" invariant: drops collapsed + // children's freed space proportionally and absorbs drift. + renormalize_ratios(&mut clean_ratios); + match clean_children.len() { + 0 => None, + 1 => Some(clean_children.pop().expect("len == 1")), + _ => Some(DockNode::Split { + direction, + ratios: clean_ratios, + children: clean_children, + }), + } + } + } + } - /// Visits every [`PanelId`] in the tree, in depth-first order. - pub fn panels(&self) -> Vec { - let mut panels = Vec::new(); - if let Some(root) = &self.root { - Self::collect_panels(root, &mut panels); - } - panels - } + /// Visits every [`PanelId`] in the tree, in depth-first order. + pub fn panels(&self) -> Vec { + let mut panels = Vec::new(); + if let Some(root) = &self.root { + Self::collect_panels(root, &mut panels); + } + panels + } - fn collect_panels(node: &DockNode, out: &mut Vec) { - match node { - DockNode::Panel(id) => out.push(*id), - DockNode::Tabs { panels, .. } => out.extend_from_slice(panels), - DockNode::Split { children, .. } => { - for child in children { - Self::collect_panels(child, out); - } - } - } - } + fn collect_panels(node: &DockNode, out: &mut Vec) { + match node { + DockNode::Panel(id) => out.push(*id), + DockNode::Tabs { panels, .. } => out.extend_from_slice(panels), + DockNode::Split { children, .. } => { + for child in children { + Self::collect_panels(child, out); + } + } + } + } - /// Returns the per-child ratios of the `Split` node at `path`, or `None` - /// if `path` does not address a split. The entries sum to `1.0`. - pub fn split_ratios(&self, path: &NodePath) -> Option> { - match self.node_at(path) { - Some(DockNode::Split { ratios, .. }) => Some(ratios.clone()), - _ => None, - } - } + /// Returns the per-child ratios of the `Split` node at `path`, or `None` + /// if `path` does not address a split. The entries sum to `1.0`. + pub fn split_ratios(&self, path: &NodePath) -> Option> { + match self.node_at(path) { + Some(DockNode::Split { ratios, .. }) => Some(ratios.clone()), + _ => None, + } + } - /// Makes `panel` the active tab of the `Tabs` node at `path`, if present. - pub fn set_tabs_active(&mut self, path: &NodePath, panel: PanelId) -> bool { - let Some(node) = self.node_at_mut(path) else { - return false; - }; - let DockNode::Tabs { panels, active } = node else { - return false; - }; - match panels.iter().position(|&other| other == panel) { - Some(index) => { - *active = index; - true - } - None => false, - } - } + /// Makes `panel` the active tab of the `Tabs` node at `path`, if present. + pub fn set_tabs_active(&mut self, path: &NodePath, panel: PanelId) -> bool { + let Some(node) = self.node_at_mut(path) else { + return false; + }; + let DockNode::Tabs { panels, active } = node else { + return false; + }; + match panels.iter().position(|&other| other == panel) { + Some(index) => { + *active = index; + true + } + None => false, + } + } - /// Returns the node at `path`, or `None` if the path is invalid. - fn node_at(&self, path: &NodePath) -> Option<&DockNode> { - let mut node = self.root.as_ref()?; - for &index in &path.0 { - let DockNode::Split { children, .. } = node else { - return None; - }; - node = children.get(index)?; - } - Some(node) - } + /// Returns the node at `path`, or `None` if the path is invalid. + fn node_at(&self, path: &NodePath) -> Option<&DockNode> { + let mut node = self.root.as_ref()?; + for &index in &path.0 { + let DockNode::Split { children, .. } = node else { + return None; + }; + node = children.get(index)?; + } + Some(node) + } - /// Returns a mutable reference to the node at `path`, or `None`. - pub(crate) fn node_at_mut(&mut self, path: &NodePath) -> Option<&mut DockNode> { - let mut node = self.root.as_mut()?; - for &index in &path.0 { - let DockNode::Split { children, .. } = node else { - return None; - }; - node = children.get_mut(index)?; - } - Some(node) - } + /// Returns a mutable reference to the node at `path`, or `None`. + pub(crate) fn node_at_mut(&mut self, path: &NodePath) -> Option<&mut DockNode> { + let mut node = self.root.as_mut()?; + for &index in &path.0 { + let DockNode::Split { children, .. } = node else { + return None; + }; + node = children.get_mut(index)?; + } + Some(node) + } } /// Application-provided bridge between panel string keys and live panels. @@ -768,18 +763,18 @@ impl DockLayout { /// } /// ``` pub trait PanelRegistry: 'static { - /// Returns the stable string key for a live panel, or `None` if the panel - /// is transient and should be omitted from saved layouts. - fn panel_key(&self, id: PanelId) -> Option; + /// Returns the stable string key for a live panel, or `None` if the panel + /// is transient and should be omitted from saved layouts. + fn panel_key(&self, id: PanelId) -> Option; - /// Rebuilds the panel identified by `key`, or returns `None` if the key - /// is unknown (the panel is then skipped during restore). - /// - /// Called on the main thread during - /// [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state); the registry may create entities with - /// `cx.new` and perform per-panel setup, but should not open windows or - /// otherwise mutate the dock area. - fn build_panel(&self, key: &str, window: &mut Window, cx: &mut App) -> Option; + /// Rebuilds the panel identified by `key`, or returns `None` if the key + /// is unknown (the panel is then skipped during restore). + /// + /// Called on the main thread during + /// [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state); the registry may create entities with + /// `cx.new` and perform per-panel setup, but should not open windows or + /// otherwise mutate the dock area. + fn build_panel(&self, key: &str, window: &mut Window, cx: &mut App) -> Option; } /// A serde-serializable snapshot of a [`DockLayout`], with panels referenced @@ -796,17 +791,17 @@ pub trait PanelRegistry: 'static { /// rejected by the caller before restoring. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct DockLayoutState { - /// Snapshot format version; written by [`capture`](DockLayoutState::capture), - /// checked by the caller on load. - pub version: u32, - root: Option, - /// Registry keys for panels that existed when the snapshot was taken but - /// whose `panel_key` returned `Some` while they were not reachable in the - /// tree (reserved for floating panels; see - /// [`FloatingPanelWindow`](crate::dock::FloatingPanelWindow)). Empty until floating support - /// lands. - #[serde(default)] - pub floating: BTreeMap, + /// Snapshot format version; written by [`capture`](DockLayoutState::capture), + /// checked by the caller on load. + pub version: u32, + root: Option, + /// Registry keys for panels that existed when the snapshot was taken but + /// whose `panel_key` returned `Some` while they were not reachable in the + /// tree (reserved for floating panels; see + /// [`FloatingPanelWindow`](crate::dock::FloatingPanelWindow)). Empty until floating support + /// lands. + #[serde(default)] + pub floating: BTreeMap, } /// Reserved per-floating-panel data inside [`DockLayoutState`](crate::dock::DockLayoutState). @@ -817,193 +812,191 @@ pub struct DockLayoutState { /// [`DockLayoutState::capture`]. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct SerializedFloating { - /// Logical x position of the floating window, in pixels. - pub x: f32, - /// Logical y position of the floating window, in pixels. - pub y: f32, - /// Width of the floating window, in pixels. - pub width: f32, - /// Height of the floating window, in pixels. - pub height: f32, + /// Logical x position of the floating window, in pixels. + pub x: f32, + /// Logical y position of the floating window, in pixels. + pub y: f32, + /// Width of the floating window, in pixels. + pub width: f32, + /// Height of the floating window, in pixels. + pub height: f32, } /// A serialized [`DockNode`] with panel keys instead of ids. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] enum SerializedNode { - /// Serialized [`DockNode::Split`]. - Split { - /// See [`DockNode::Split::direction`]. - direction: Axis, - /// See [`DockNode::Split::ratios`]. - ratios: Vec, - /// See [`DockNode::Split::children`]. - children: Vec, - }, - /// Serialized [`DockNode::Tabs`]; `active` is an index into `panels`. - Tabs { - /// Registry keys of the tabbed panels, in tab order. - panels: Vec, - /// Active tab index. - active: usize, - }, - /// Serialized [`DockNode::Panel`], holding the panel's registry key. - Panel(String), + /// Serialized [`DockNode::Split`]. + Split { + /// See [`DockNode::Split::direction`]. + direction: Axis, + /// See [`DockNode::Split::ratios`]. + ratios: Vec, + /// See [`DockNode::Split::children`]. + children: Vec, + }, + /// Serialized [`DockNode::Tabs`]; `active` is an index into `panels`. + Tabs { + /// Registry keys of the tabbed panels, in tab order. + panels: Vec, + /// Active tab index. + active: usize, + }, + /// Serialized [`DockNode::Panel`], holding the panel's registry key. + Panel(String), } impl DockLayoutState { - /// The snapshot format version written by - /// [`capture`](DockLayoutState::capture). - /// - /// v2: `Split` nodes store a per-child `ratios` vector instead of the - /// single `ratio` of v1. Snapshots written with v1 cannot be read by a - /// v2 reader; reject stale versions before restoring. - pub const VERSION: u32 = 2; + /// The snapshot format version written by + /// [`capture`](DockLayoutState::capture). + /// + /// v2: `Split` nodes store a per-child `ratios` vector instead of the + /// single `ratio` of v1. Snapshots written with v1 cannot be read by a + /// v2 reader; reject stale versions before restoring. + pub const VERSION: u32 = 2; - /// Snapshots `layout`, translating panel ids to string keys via - /// `registry`. - /// - /// Panels whose [`PanelRegistry::panel_key`] returns `None` are omitted - /// from the snapshot; their nodes are collapsed as if removed. - pub fn capture(layout: &DockLayout, registry: &dyn PanelRegistry) -> Self { - let root = layout - .root - .as_ref() - .and_then(|node| Self::serialize_node(node, registry)); - Self { - version: Self::VERSION, - root, - floating: BTreeMap::new(), - } - } + /// Snapshots `layout`, translating panel ids to string keys via + /// `registry`. + /// + /// Panels whose [`PanelRegistry::panel_key`] returns `None` are omitted + /// from the snapshot; their nodes are collapsed as if removed. + pub fn capture(layout: &DockLayout, registry: &dyn PanelRegistry) -> Self { + let root = layout + .root + .as_ref() + .and_then(|node| Self::serialize_node(node, registry)); + Self { + version: Self::VERSION, + root, + floating: BTreeMap::new(), + } + } - /// Serializes `node`, dropping panels without a key; `None` when the node - /// collapses away entirely. - fn serialize_node(node: &DockNode, registry: &dyn PanelRegistry) -> Option { - match node { - DockNode::Panel(id) => registry.panel_key(*id).map(SerializedNode::Panel), - DockNode::Tabs { panels, active } => { - let active_panel = panels.get(*active).copied(); - let mut serialized = Vec::new(); - let mut serialized_active = 0; - for id in panels.iter() { - if let Some(key) = registry.panel_key(*id) { - if Some(*id) == active_panel { - serialized_active = serialized.len(); - } - serialized.push(key); - } - } - if serialized.is_empty() { - return None; - } - serialized_active = serialized_active.min(serialized.len() - 1); - Some(SerializedNode::Tabs { - panels: serialized, - active: serialized_active, - }) - } - DockNode::Split { - direction, - ratios, - children, - } => { - let children: Vec = children - .iter() - .filter_map(|child| Self::serialize_node(child, registry)) - .collect(); - match children.len() { - 0 => None, - 1 => children.into_iter().next(), - _ => Some(SerializedNode::Split { - direction: *direction, - ratios: ratios.clone(), - children, - }), - } - } - } - } + /// Serializes `node`, dropping panels without a key; `None` when the node + /// collapses away entirely. + fn serialize_node(node: &DockNode, registry: &dyn PanelRegistry) -> Option { + match node { + DockNode::Panel(id) => registry.panel_key(*id).map(SerializedNode::Panel), + DockNode::Tabs { panels, active } => { + let active_panel = panels.get(*active).copied(); + let mut serialized = Vec::new(); + let mut serialized_active = 0; + for id in panels.iter() { + if let Some(key) = registry.panel_key(*id) { + if Some(*id) == active_panel { + serialized_active = serialized.len(); + } + serialized.push(key); + } + } + if serialized.is_empty() { + return None; + } + serialized_active = serialized_active.min(serialized.len() - 1); + Some(SerializedNode::Tabs { + panels: serialized, + active: serialized_active, + }) + } + DockNode::Split { + direction, + ratios, + children, + } => { + let children: Vec = children + .iter() + .filter_map(|child| Self::serialize_node(child, registry)) + .collect(); + match children.len() { + 0 => None, + 1 => children.into_iter().next(), + _ => Some(SerializedNode::Split { + direction: *direction, + ratios: ratios.clone(), + children, + }), + } + } + } + } - /// Rebuilds the pure tree shape, leaving view reconstruction to the - /// caller (see [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state), which resolves keys through - /// the registry). - /// - /// The returned layout is normalized ([`DockLayout::cleanup`] has run). - pub fn to_layout(&self) -> DockLayout { - let root = self.root.as_ref().and_then(Self::deserialize_node); - let mut layout = DockLayout { root }; - layout.cleanup(); - layout - } + /// Rebuilds the pure tree shape, leaving view reconstruction to the + /// caller (see [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state), which resolves keys through + /// the registry). + /// + /// The returned layout is normalized ([`DockLayout::cleanup`] has run). + pub fn to_layout(&self) -> DockLayout { + let root = self.root.as_ref().and_then(Self::deserialize_node); + let mut layout = DockLayout { root }; + layout.cleanup(); + layout + } - /// Deserializes a single node, addressing panels by deterministic interim - /// ids (see [`interim_id`]); `None` when the node collapses away. - fn deserialize_node(node: &SerializedNode) -> Option { - match node { - SerializedNode::Panel(key) => Some(DockNode::Panel(interim_id(key))), - SerializedNode::Tabs { panels, active } => { - if panels.is_empty() { - return None; - } - let panels: Vec = panels.iter().map(|key| interim_id(key)).collect(); - Some(DockNode::Tabs { - active: (*active).min(panels.len() - 1), - panels, - }) - } - SerializedNode::Split { - direction, - ratios, - children, - } => { - let children: Vec = children - .iter() - .filter_map(Self::deserialize_node) - .collect(); - match children.len() { - 0 => None, - 1 => children.into_iter().next(), - _ => { - // Evenly sized by default; `cleanup` (run by the - // caller) renormalizes and reconciles lengths. - let mut ratios = ratios.clone(); - if ratios.len() != children.len() { - ratios = vec![1.0 / children.len() as f32; children.len()]; - } - Some(DockNode::Split { - direction: *direction, - ratios, - children, - }) - } - } - } - } - } + /// Deserializes a single node, addressing panels by deterministic interim + /// ids (see [`interim_id`]); `None` when the node collapses away. + fn deserialize_node(node: &SerializedNode) -> Option { + match node { + SerializedNode::Panel(key) => Some(DockNode::Panel(interim_id(key))), + SerializedNode::Tabs { panels, active } => { + if panels.is_empty() { + return None; + } + let panels: Vec = panels.iter().map(|key| interim_id(key)).collect(); + Some(DockNode::Tabs { + active: (*active).min(panels.len() - 1), + panels, + }) + } + SerializedNode::Split { + direction, + ratios, + children, + } => { + let children: Vec = + children.iter().filter_map(Self::deserialize_node).collect(); + match children.len() { + 0 => None, + 1 => children.into_iter().next(), + _ => { + // Evenly sized by default; `cleanup` (run by the + // caller) renormalizes and reconciles lengths. + let mut ratios = ratios.clone(); + if ratios.len() != children.len() { + ratios = vec![1.0 / children.len() as f32; children.len()]; + } + Some(DockNode::Split { + direction: *direction, + ratios, + children, + }) + } + } + } + } + } - /// Collects the registry keys referenced by this snapshot, depth-first. - /// - /// Used by [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state) to rebuild panels - /// (and learn their real ids) before re-keying the tree. - pub(crate) fn keys(&self) -> Vec { - let mut keys = Vec::new(); - if let Some(root) = &self.root { - Self::collect_keys(root, &mut keys); - } - keys - } + /// Collects the registry keys referenced by this snapshot, depth-first. + /// + /// Used by [`DockArea::restore_state`](crate::dock::DockArea::restore_state)(crate::dock::DockArea::restore_state) to rebuild panels + /// (and learn their real ids) before re-keying the tree. + pub(crate) fn keys(&self) -> Vec { + let mut keys = Vec::new(); + if let Some(root) = &self.root { + Self::collect_keys(root, &mut keys); + } + keys + } - fn collect_keys(node: &SerializedNode, out: &mut Vec) { - match node { - SerializedNode::Panel(key) => out.push(key.clone()), - SerializedNode::Tabs { panels, .. } => out.extend(panels.iter().cloned()), - SerializedNode::Split { children, .. } => { - for child in children { - Self::collect_keys(child, out); - } - } - } - } + fn collect_keys(node: &SerializedNode, out: &mut Vec) { + match node { + SerializedNode::Panel(key) => out.push(key.clone()), + SerializedNode::Tabs { panels, .. } => out.extend(panels.iter().cloned()), + SerializedNode::Split { children, .. } => { + for child in children { + Self::collect_keys(child, out); + } + } + } + } } /// Maps a registry key to a deterministic interim [`PanelId`]. @@ -1013,9 +1006,9 @@ impl DockLayoutState { /// rebuilds the real panels and re-maps the tree using the same hash, so the /// two passes agree. pub(crate) fn interim_id(key: &str) -> PanelId { - let mut hasher = DefaultHasher::new(); - key.hash(&mut hasher); - PanelId::new(hasher.finish()) + let mut hasher = DefaultHasher::new(); + key.hash(&mut hasher); + PanelId::new(hasher.finish()) } /// Splits the share of child `index` in half and inserts the new child's @@ -1025,179 +1018,204 @@ pub(crate) fn interim_id(key: &str) -> PanelId { /// split: the newcomer takes half of the target child's extent, and the /// target keeps the other half. fn split_ratio_at(ratios: &mut Vec, index: usize) { - let half = ratios[index] / 2.0; - ratios[index] = half; - ratios.insert(index, half); + let half = ratios[index] / 2.0; + ratios[index] = half; + ratios.insert(index, half); } /// Normalizes `ratios` to sum to `1.0`, so freed shares (from removed or /// collapsed children) are redistributed proportionally and float drift is /// absorbed. No-op for an empty or all-zero vector. fn renormalize_ratios(ratios: &mut Vec) { - let sum: f32 = ratios.iter().sum(); - if sum > 0.0 { - for ratio in ratios.iter_mut() { - *ratio /= sum; - } - } + let sum: f32 = ratios.iter().sum(); + if sum > 0.0 { + for ratio in ratios.iter_mut() { + *ratio /= sum; + } + } } #[cfg(test)] mod tests { - use super::*; - use crate::Axis; + use super::*; + use crate::Axis; - fn panel(id: u64) -> DockNode { - DockNode::Panel(PanelId::new(id)) - } + fn panel(id: u64) -> DockNode { + DockNode::Panel(PanelId::new(id)) + } - /// Builds `[a] → [a | b] → [a | b | c]` all on one axis, exercising the - /// sibling-insert path that used to flatten to a single shared ratio. - fn three_panel_horizontal_layout() -> DockLayout { - let mut layout = DockLayout::new(); - assert!(layout.insert_panel(PanelId::new(1), None)); - assert!(layout.insert_panel( - PanelId::new(2), - Some(DropTarget { panel: Some(PanelId::new(1)), zone: DropZone::Right }) - )); - assert!(layout.insert_panel( - PanelId::new(3), - Some(DropTarget { panel: Some(PanelId::new(2)), zone: DropZone::Right }) - )); - layout - } + /// Builds `[a] → [a | b] → [a | b | c]` all on one axis, exercising the + /// sibling-insert path that used to flatten to a single shared ratio. + fn three_panel_horizontal_layout() -> DockLayout { + let mut layout = DockLayout::new(); + assert!(layout.insert_panel(PanelId::new(1), None)); + assert!(layout.insert_panel( + PanelId::new(2), + Some(DropTarget { + panel: Some(PanelId::new(1)), + zone: DropZone::Right + }) + )); + assert!(layout.insert_panel( + PanelId::new(3), + Some(DropTarget { + panel: Some(PanelId::new(2)), + zone: DropZone::Right + }) + )); + layout + } - #[test] - fn three_panels_on_one_axis_keep_distinct_ratios() { - let layout = three_panel_horizontal_layout(); - let root = layout.root().unwrap(); - let DockNode::Split { direction, ratios, children } = root else { - panic!("expected a single split at the root"); - }; - assert_eq!(*direction, Axis::Horizontal); - assert_eq!(children.len(), 3); - // Inserting c to the right of b halves b's share: [1/2, 1/4, 1/4]. - assert_eq!(ratios, &vec![0.5, 0.25, 0.25]); - let sum: f32 = ratios.iter().sum(); - assert!((sum - 1.0).abs() < 1e-6, "ratios must sum to 1, got {sum}"); - } + #[test] + fn three_panels_on_one_axis_keep_distinct_ratios() { + let layout = three_panel_horizontal_layout(); + let root = layout.root().unwrap(); + let DockNode::Split { + direction, + ratios, + children, + } = root + else { + panic!("expected a single split at the root"); + }; + assert_eq!(*direction, Axis::Horizontal); + assert_eq!(children.len(), 3); + // Inserting c to the right of b halves b's share: [1/2, 1/4, 1/4]. + assert_eq!(ratios, &vec![0.5, 0.25, 0.25]); + let sum: f32 = ratios.iter().sum(); + assert!((sum - 1.0).abs() < 1e-6, "ratios must sum to 1, got {sum}"); + } - #[test] - fn resize_split_child_adjusts_only_the_target_pair() { - let mut layout = three_panel_horizontal_layout(); - let path = NodePath::default(); - // Give the second panel 2/3 of its pair with the third: - // pair = [0.25, 0.25] → scaled so the second panel holds 2/3. - layout.resize_split_child(&path, 1, 2.0 / 3.0); - let ratios = layout.split_ratios(&path).unwrap(); - // The first panel's share is untouched. - assert!((ratios[0] - 0.5).abs() < 1e-6); - assert!((ratios[1] - 1.0 / 3.0).abs() < 1e-6); - assert!((ratios[2] - 1.0 / 6.0).abs() < 1e-6); - let sum: f32 = ratios.iter().sum(); - assert!((sum - 1.0).abs() < 1e-6); - } + #[test] + fn resize_split_child_adjusts_only_the_target_pair() { + let mut layout = three_panel_horizontal_layout(); + let path = NodePath::default(); + // Give the second panel 2/3 of its pair with the third: + // pair = [0.25, 0.25] → scaled so the second panel holds 2/3. + layout.resize_split_child(&path, 1, 2.0 / 3.0); + let ratios = layout.split_ratios(&path).unwrap(); + // The first panel's share is untouched. + assert!((ratios[0] - 0.5).abs() < 1e-6); + assert!((ratios[1] - 1.0 / 3.0).abs() < 1e-6); + assert!((ratios[2] - 1.0 / 6.0).abs() < 1e-6); + let sum: f32 = ratios.iter().sum(); + assert!((sum - 1.0).abs() < 1e-6); + } - #[test] - fn resize_split_is_source_compatible_and_affects_first_child() { - let mut layout = three_panel_horizontal_layout(); - // The old two-argument form gives the first child its share of the - // whole and redistributes the rest proportionally: [1/2, 1/4, 1/4] - // with ratio 0.7 → [0.7, 0.15, 0.15]. - layout.resize_split(&NodePath::default(), 0.7); - let ratios = layout.split_ratios(&NodePath::default()).unwrap(); - assert!((ratios[0] - 0.7).abs() < 1e-6); - assert!((ratios[1] - 0.15).abs() < 1e-6); - assert!((ratios[2] - 0.15).abs() < 1e-6); - } + #[test] + fn resize_split_is_source_compatible_and_affects_first_child() { + let mut layout = three_panel_horizontal_layout(); + // The old two-argument form gives the first child its share of the + // whole and redistributes the rest proportionally: [1/2, 1/4, 1/4] + // with ratio 0.7 → [0.7, 0.15, 0.15]. + layout.resize_split(&NodePath::default(), 0.7); + let ratios = layout.split_ratios(&NodePath::default()).unwrap(); + assert!((ratios[0] - 0.7).abs() < 1e-6); + assert!((ratios[1] - 0.15).abs() < 1e-6); + assert!((ratios[2] - 0.15).abs() < 1e-6); + } - #[test] - fn remove_panel_renormalizes_remaining_ratios() { - let mut layout = three_panel_horizontal_layout(); - // [1/2, 1/4, 1/4] → remove panel 2 → [1/2, 1/4] renormalized to [2/3, 1/3]. - assert!(layout.remove_panel(PanelId::new(2))); - let root = layout.root().unwrap(); - let DockNode::Split { ratios, children, .. } = root else { - panic!("expected a split after removal"); - }; - assert_eq!(children.len(), 2); - assert!((ratios[0] - 2.0 / 3.0).abs() < 1e-6); - assert!((ratios[1] - 1.0 / 3.0).abs() < 1e-6); - } + #[test] + fn remove_panel_renormalizes_remaining_ratios() { + let mut layout = three_panel_horizontal_layout(); + // [1/2, 1/4, 1/4] → remove panel 2 → [1/2, 1/4] renormalized to [2/3, 1/3]. + assert!(layout.remove_panel(PanelId::new(2))); + let root = layout.root().unwrap(); + let DockNode::Split { + ratios, children, .. + } = root + else { + panic!("expected a split after removal"); + }; + assert_eq!(children.len(), 2); + assert!((ratios[0] - 2.0 / 3.0).abs() < 1e-6); + assert!((ratios[1] - 1.0 / 3.0).abs() < 1e-6); + } - #[test] - fn cleanup_flattens_same_axis_nest_and_scales_ratios() { - // Hand-construct a same-axis nest: root [a | inner], where inner is - // itself a horizontal split [b | c] with ratios [3/4, 1/4]. - let mut layout = DockLayout::new(); - layout.root = Some(DockNode::Split { - direction: Axis::Horizontal, - ratios: vec![0.5, 0.5], - children: vec![ - panel(1), - DockNode::Split { - direction: Axis::Horizontal, - ratios: vec![0.75, 0.25], - children: vec![panel(2), panel(3)], - }, - ], - }); - layout.cleanup(); - let root = layout.root().unwrap(); - let DockNode::Split { ratios, children, .. } = root else { - panic!("expected a flattened split at the root"); - }; - assert_eq!(children.len(), 3); - // Inner ratios scaled by the inner node's share: [0.5, 0.375, 0.125]. - assert!((ratios[0] - 0.5).abs() < 1e-6); - assert!((ratios[1] - 0.375).abs() < 1e-6); - assert!((ratios[2] - 0.125).abs() < 1e-6); - } + #[test] + fn cleanup_flattens_same_axis_nest_and_scales_ratios() { + // Hand-construct a same-axis nest: root [a | inner], where inner is + // itself a horizontal split [b | c] with ratios [3/4, 1/4]. + let mut layout = DockLayout::new(); + layout.root = Some(DockNode::Split { + direction: Axis::Horizontal, + ratios: vec![0.5, 0.5], + children: vec![ + panel(1), + DockNode::Split { + direction: Axis::Horizontal, + ratios: vec![0.75, 0.25], + children: vec![panel(2), panel(3)], + }, + ], + }); + layout.cleanup(); + let root = layout.root().unwrap(); + let DockNode::Split { + ratios, children, .. + } = root + else { + panic!("expected a flattened split at the root"); + }; + assert_eq!(children.len(), 3); + // Inner ratios scaled by the inner node's share: [0.5, 0.375, 0.125]. + assert!((ratios[0] - 0.5).abs() < 1e-6); + assert!((ratios[1] - 0.375).abs() < 1e-6); + assert!((ratios[2] - 0.125).abs() < 1e-6); + } - #[test] - fn layout_state_round_trips_distinct_ratios() { - let mut layout = three_panel_horizontal_layout(); - layout.resize_split_child(&NodePath::default(), 1, 0.8); - let registry = TestRegistry; - let state = DockLayoutState::capture(&layout, ®istry); - assert_eq!(state.version, DockLayoutState::VERSION); - let restored = state.to_layout(); - // Panel ids are re-derived from registry keys on restore, so compare - // the tree *shape* (direction, ratios, structure) rather than ids. - let DockNode::Split { - direction, - ratios, - children, - } = restored.root().unwrap() - else { - panic!("expected a split at the restored root"); - }; - assert_eq!(*direction, Axis::Horizontal); - let original_ratios = layout.split_ratios(&NodePath::default()).unwrap(); - assert_eq!(ratios, &original_ratios); - assert_eq!(children.len(), 3); - } + #[test] + fn layout_state_round_trips_distinct_ratios() { + let mut layout = three_panel_horizontal_layout(); + layout.resize_split_child(&NodePath::default(), 1, 0.8); + let registry = TestRegistry; + let state = DockLayoutState::capture(&layout, ®istry); + assert_eq!(state.version, DockLayoutState::VERSION); + let restored = state.to_layout(); + // Panel ids are re-derived from registry keys on restore, so compare + // the tree *shape* (direction, ratios, structure) rather than ids. + let DockNode::Split { + direction, + ratios, + children, + } = restored.root().unwrap() + else { + panic!("expected a split at the restored root"); + }; + assert_eq!(*direction, Axis::Horizontal); + let original_ratios = layout.split_ratios(&NodePath::default()).unwrap(); + assert_eq!(ratios, &original_ratios); + assert_eq!(children.len(), 3); + } - #[test] - fn insert_before_splits_the_target_child() { - let mut layout = three_panel_horizontal_layout(); - // [1/2, 1/4, 1/4]; inserting d to the LEFT of panel 2 halves panel 2's - // share and puts d in front of it. - assert!(layout.insert_panel( - PanelId::new(4), - Some(DropTarget { panel: Some(PanelId::new(2)), zone: DropZone::Left }) - )); - let ratios = layout.split_ratios(&NodePath::default()).unwrap(); - assert_eq!(ratios, vec![0.5, 0.125, 0.125, 0.25]); - } + #[test] + fn insert_before_splits_the_target_child() { + let mut layout = three_panel_horizontal_layout(); + // [1/2, 1/4, 1/4]; inserting d to the LEFT of panel 2 halves panel 2's + // share and puts d in front of it. + assert!(layout.insert_panel( + PanelId::new(4), + Some(DropTarget { + panel: Some(PanelId::new(2)), + zone: DropZone::Left + }) + )); + let ratios = layout.split_ratios(&NodePath::default()).unwrap(); + assert_eq!(ratios, vec![0.5, 0.125, 0.125, 0.25]); + } - struct TestRegistry; - impl PanelRegistry for TestRegistry { - fn panel_key(&self, id: PanelId) -> Option { - Some(format!("panel-{}", id.raw())) - } - fn build_panel(&self, _key: &str, _window: &mut Window, _cx: &mut App) -> Option { - None - } - } + struct TestRegistry; + impl PanelRegistry for TestRegistry { + fn panel_key(&self, id: PanelId) -> Option { + Some(format!("panel-{}", id.raw())) + } + fn build_panel( + &self, + _key: &str, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + } } diff --git a/crates/gpui/src/dock/mod.rs b/crates/gpui/src/dock/mod.rs index 98a1b9995a..95ece56e1b 100644 --- a/crates/gpui/src/dock/mod.rs +++ b/crates/gpui/src/dock/mod.rs @@ -85,6 +85,6 @@ pub use dock_area::{DockArea, DockEvent}; pub use floating::FloatingPanelWindow; pub(crate) use layout::path_key; pub use layout::{ - DockLayout, DockLayoutState, DockNode, DropTarget, DropZone, NodePath, PanelRegistry, + DockLayout, DockLayoutState, DockNode, DropTarget, DropZone, NodePath, PanelRegistry, }; pub use panel::{DockPanel, PanelEvent, PanelHandle, PanelId}; diff --git a/crates/gpui/src/dock/panel.rs b/crates/gpui/src/dock/panel.rs index 174f0f8f1b..40ff374916 100644 --- a/crates/gpui/src/dock/panel.rs +++ b/crates/gpui/src/dock/panel.rs @@ -8,8 +8,8 @@ //! needs without downcasting. use crate::{ - AnyElement, AnyView, App, Context, Entity, EventEmitter, Render, SharedString, Subscription, - Window, + AnyElement, AnyView, App, Context, Entity, EventEmitter, Render, SharedString, Subscription, + Window, }; use serde::{Deserialize, Serialize}; use std::fmt; @@ -31,25 +31,25 @@ use std::fmt; pub struct PanelId(u64); impl PanelId { - /// Creates a panel id from a raw numeric value. - /// - /// The value only needs to be unique within the owning - /// [`DockArea`](crate::dock::DockArea)(crate::dock::DockArea); a simple per-application counter - /// (or a hash of a stable name) is sufficient. - pub const fn new(raw: u64) -> Self { - Self(raw) - } + /// Creates a panel id from a raw numeric value. + /// + /// The value only needs to be unique within the owning + /// [`DockArea`](crate::dock::DockArea)(crate::dock::DockArea); a simple per-application counter + /// (or a hash of a stable name) is sufficient. + pub const fn new(raw: u64) -> Self { + Self(raw) + } - /// Returns the raw numeric value backing this id. - pub const fn raw(self) -> u64 { - self.0 - } + /// Returns the raw numeric value backing this id. + pub const fn raw(self) -> u64 { + self.0 + } } impl fmt::Display for PanelId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "panel-{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "panel-{}", self.0) + } } /// Events a dock panel can emit to its containing [`DockArea`](crate::dock::DockArea)(crate::dock::DockArea). @@ -60,24 +60,24 @@ impl fmt::Display for PanelId { /// flow — without the panel needing a direct reference to the dock. #[derive(Clone, Debug)] pub enum PanelEvent { - /// The panel asked to be closed (e.g. its own close affordance was - /// invoked). - /// - /// The dock area does not remove the panel unconditionally: it first - /// consults [`DockPanel::should_close`], then calls [`DockPanel::on_close`] - /// and removes the panel only if closing was confirmed. - CloseRequested, - /// The panel's content gained keyboard focus. - /// - /// The dock area uses this to keep its own `focused_panel` bookkeeping in - /// sync and to emit - /// [`DockEvent::PanelFocused`](crate::dock::DockEvent::PanelFocused). - Focused, - /// The panel's title changed; the tab strip should re-render the label. - /// - /// The new title is read back through [`DockPanel::title`] rather than - /// carried in the event, so panels never have to clone it. - TitleChanged, + /// The panel asked to be closed (e.g. its own close affordance was + /// invoked). + /// + /// The dock area does not remove the panel unconditionally: it first + /// consults [`DockPanel::should_close`], then calls [`DockPanel::on_close`] + /// and removes the panel only if closing was confirmed. + CloseRequested, + /// The panel's content gained keyboard focus. + /// + /// The dock area uses this to keep its own `focused_panel` bookkeeping in + /// sync and to emit + /// [`DockEvent::PanelFocused`](crate::dock::DockEvent::PanelFocused). + Focused, + /// The panel's title changed; the tab strip should re-render the label. + /// + /// The new title is read back through [`DockPanel::title`] rather than + /// carried in the event, so panels never have to clone it. + TitleChanged, } /// A view that can live inside a [`DockArea`](crate::dock::DockArea)(crate::dock::DockArea). @@ -120,56 +120,56 @@ pub enum PanelEvent { /// } /// ``` pub trait DockPanel: Render + EventEmitter + 'static { - /// Returns the stable id of this panel. - /// - /// Must return the same value for the whole lifetime of the view and must - /// be unique among all panels added to one dock area. - fn panel_id(&self) -> PanelId; + /// Returns the stable id of this panel. + /// + /// Must return the same value for the whole lifetime of the view and must + /// be unique among all panels added to one dock area. + fn panel_id(&self) -> PanelId; - /// Returns the plain-text title shown in the tab strip and, where - /// relevant, in window titles for floated panels. - /// - /// Called on every render of the containing tab bar, so it should be - /// cheap. Emit [`PanelEvent::TitleChanged`] after changing whatever state - /// feeds this. - fn title(&self, cx: &App) -> SharedString; + /// Returns the plain-text title shown in the tab strip and, where + /// relevant, in window titles for floated panels. + /// + /// Called on every render of the containing tab bar, so it should be + /// cheap. Emit [`PanelEvent::TitleChanged`] after changing whatever state + /// feeds this. + fn title(&self, cx: &App) -> SharedString; - /// Returns the element rendered inside this panel's tab. - /// - /// The default tab bar renders [`title`](DockPanel::title) when this is - /// not customized, but panels may return richer content (icon, dirty - /// indicator, close-on-middle-click affordances). The returned element - /// must not handle close or drag interactions itself — the tab strip - /// overlays those. - fn tab_content(&self, cx: &App) -> AnyElement; + /// Returns the element rendered inside this panel's tab. + /// + /// The default tab bar renders [`title`](DockPanel::title) when this is + /// not customized, but panels may return richer content (icon, dirty + /// indicator, close-on-middle-click affordances). The returned element + /// must not handle close or drag interactions itself — the tab strip + /// overlays those. + fn tab_content(&self, cx: &App) -> AnyElement; - /// Whether this panel shows a close button and can be closed by the user. - /// - /// Non-closable panels can still be removed programmatically via - /// [`DockArea::remove_panel`](crate::dock::DockArea::remove_panel). - /// Defaults to `true`. - fn closable(&self) -> bool { - true - } + /// Whether this panel shows a close button and can be closed by the user. + /// + /// Non-closable panels can still be removed programmatically via + /// [`DockArea::remove_panel`](crate::dock::DockArea::remove_panel). + /// Defaults to `true`. + fn closable(&self) -> bool { + true + } - /// Called when the user has asked to close this panel, before removal. - /// - /// Return `true` to allow the close, `false` to veto it (for example - /// after showing an "unsaved changes" dialog). This may be called on the - /// same event-loop turn as the close request, so asynchronous - /// confirmations should veto now and re-trigger closing later through - /// [`DockArea::remove_panel`](crate::dock::DockArea::remove_panel). - /// Defaults to `true`. - fn should_close(&mut self, _window: &mut Window, _cx: &mut Context) -> bool { - true - } + /// Called when the user has asked to close this panel, before removal. + /// + /// Return `true` to allow the close, `false` to veto it (for example + /// after showing an "unsaved changes" dialog). This may be called on the + /// same event-loop turn as the close request, so asynchronous + /// confirmations should veto now and re-trigger closing later through + /// [`DockArea::remove_panel`](crate::dock::DockArea::remove_panel). + /// Defaults to `true`. + fn should_close(&mut self, _window: &mut Window, _cx: &mut Context) -> bool { + true + } - /// Called after a close was confirmed and before the panel is removed - /// from the layout. - /// - /// Use this to release resources tied to the dock (subscriptions, - /// scratch entities). The default implementation does nothing. - fn on_close(&mut self, _window: &mut Window, _cx: &mut Context) {} + /// Called after a close was confirmed and before the panel is removed + /// from the layout. + /// + /// Use this to release resources tied to the dock (subscriptions, + /// scratch entities). The default implementation does nothing. + fn on_close(&mut self, _window: &mut Window, _cx: &mut Context) {} } /// A type-erased panel plus the metadata the dock chrome needs. @@ -182,74 +182,74 @@ pub trait DockPanel: Render + EventEmitter + 'static { /// Construct with [`PanelHandle::new`]; pass to /// [`DockArea::add_panel`](crate::dock::DockArea::add_panel)(crate::dock::DockArea::add_panel). pub struct PanelHandle { - id: PanelId, - view: AnyView, - title: SharedString, - closable: bool, - /// Subscription to the panel's [`PanelEvent`]s while it is held by a - /// dock area, installed by [`DockArea`](crate::dock::DockArea) when the - /// panel is added. - subscription: Option, + id: PanelId, + view: AnyView, + title: SharedString, + closable: bool, + /// Subscription to the panel's [`PanelEvent`]s while it is held by a + /// dock area, installed by [`DockArea`](crate::dock::DockArea) when the + /// panel is added. + subscription: Option, } impl PanelHandle { - /// Wraps a panel view, snapshotting its current metadata. - /// - /// `panel` must implement [`DockPanel`](crate::dock::DockPanel). The handle keeps the view alive - /// for as long as it is stored in the dock area. - /// - /// # Panics - /// - /// Does not panic, but adding two handles with the same - /// [`DockPanel::panel_id`] to one dock area is rejected there. - pub fn new(panel: Entity

, cx: &App) -> Self { - let id = panel.read(cx).panel_id(); - let title = panel.read(cx).title(cx); - let closable = panel.read(cx).closable(); - Self { - id, - view: panel.into(), - title, - closable, - subscription: None, - } - } + /// Wraps a panel view, snapshotting its current metadata. + /// + /// `panel` must implement [`DockPanel`](crate::dock::DockPanel). The handle keeps the view alive + /// for as long as it is stored in the dock area. + /// + /// # Panics + /// + /// Does not panic, but adding two handles with the same + /// [`DockPanel::panel_id`] to one dock area is rejected there. + pub fn new(panel: Entity

, cx: &App) -> Self { + let id = panel.read(cx).panel_id(); + let title = panel.read(cx).title(cx); + let closable = panel.read(cx).closable(); + Self { + id, + view: panel.into(), + title, + closable, + subscription: None, + } + } - /// Returns the id the panel reported at snapshot time. - pub fn panel_id(&self) -> PanelId { - self.id - } + /// Returns the id the panel reported at snapshot time. + pub fn panel_id(&self) -> PanelId { + self.id + } - /// Returns the cached tab title. - /// - /// May be stale between a title change and the dock area processing - /// [`PanelEvent::TitleChanged`]; treat as display-only. - pub fn title(&self) -> &SharedString { - &self.title - } + /// Returns the cached tab title. + /// + /// May be stale between a title change and the dock area processing + /// [`PanelEvent::TitleChanged`]; treat as display-only. + pub fn title(&self) -> &SharedString { + &self.title + } - /// Returns the cached value of [`DockPanel::closable`]. - pub fn closable(&self) -> bool { - self.closable - } + /// Returns the cached value of [`DockPanel::closable`]. + pub fn closable(&self) -> bool { + self.closable + } - /// Returns the type-erased panel view. - pub fn view(&self) -> &AnyView { - &self.view - } + /// Returns the type-erased panel view. + pub fn view(&self) -> &AnyView { + &self.view + } - /// Returns the subscription to this panel's [`PanelEvent`]s, if the dock - /// area has installed one. - #[allow(dead_code)] // reserved for the dock's panel-event bookkeeping - pub(crate) fn subscription(&self) -> &Option { - &self.subscription - } + /// Returns the subscription to this panel's [`PanelEvent`]s, if the dock + /// area has installed one. + #[allow(dead_code)] // reserved for the dock's panel-event bookkeeping + pub(crate) fn subscription(&self) -> &Option { + &self.subscription + } - /// Installs (or replaces) the subscription to this panel's [`PanelEvent`]s. - /// - /// Used by [`DockArea`](crate::dock::DockArea) when the panel is added or - /// restored; the previous subscription, if any, is dropped. - pub(crate) fn set_subscription(&mut self, subscription: Option) { - self.subscription = subscription; - } + /// Installs (or replaces) the subscription to this panel's [`PanelEvent`]s. + /// + /// Used by [`DockArea`](crate::dock::DockArea) when the panel is added or + /// restored; the previous subscription, if any, is dropped. + pub(crate) fn set_subscription(&mut self, subscription: Option) { + self.subscription = subscription; + } } diff --git a/crates/gpui/src/dock/split_handle.rs b/crates/gpui/src/dock/split_handle.rs index 6dd8468cc1..038f01dd27 100644 --- a/crates/gpui/src/dock/split_handle.rs +++ b/crates/gpui/src/dock/split_handle.rs @@ -6,47 +6,46 @@ //! split's `ratio` through //! [`DockLayout::resize_split`](crate::dock::DockLayout::resize_split). -use crate::{ - div, px, App, AppContext, Axis, ClickEvent, Context, ElementId, EventEmitter, - InteractiveElement, IntoElement, Pixels, Point, Render, StatefulInteractiveElement, Styled, - Window, -}; use crate::colors::DefaultColors; +use crate::{ + App, AppContext, Axis, ClickEvent, Context, ElementId, EventEmitter, InteractiveElement, + IntoElement, Pixels, Point, Render, StatefulInteractiveElement, Styled, Window, div, px, +}; use super::{NodePath, path_key}; /// Events emitted by a [`SplitHandle`] toward its owning [`DockArea`]. #[derive(Clone, Debug)] pub(crate) enum SplitHandleEvent { - /// The user dragged the handle; the boundary at `index` of the split at - /// `path` should be resized to `ratio` (child `index`'s share of the - /// pair, already clamped to the allowed range). - ResizeRequested { - /// Path of the split node to resize. - path: NodePath, - /// The boundary being moved: between children `index` and `index + 1`. - index: usize, - /// Desired new ratio, already clamped to the allowed range. - ratio: f32, - }, - /// The user double-clicked the handle; the boundary at `index` of the - /// split at `path` should be reset to [`SplitHandle::RESET_RATIO`]. - ResetRequested { - /// Path of the split node to resize. - path: NodePath, - /// The boundary being moved: between children `index` and `index + 1`. - index: usize, - }, + /// The user dragged the handle; the boundary at `index` of the split at + /// `path` should be resized to `ratio` (child `index`'s share of the + /// pair, already clamped to the allowed range). + ResizeRequested { + /// Path of the split node to resize. + path: NodePath, + /// The boundary being moved: between children `index` and `index + 1`. + index: usize, + /// Desired new ratio, already clamped to the allowed range. + ratio: f32, + }, + /// The user double-clicked the handle; the boundary at `index` of the + /// split at `path` should be reset to [`SplitHandle::RESET_RATIO`]. + ResetRequested { + /// Path of the split node to resize. + path: NodePath, + /// The boundary being moved: between children `index` and `index + 1`. + index: usize, + }, } /// The payload of a handle drag; carried by the drag-and-drop system so the /// split container can identify which handle is being dragged. #[derive(Clone, Debug)] pub(crate) struct SplitHandleDrag { - /// Path of the split being resized. - pub(crate) path: NodePath, - /// The boundary being dragged: between children `index` and `index + 1`. - pub(crate) index: usize, + /// Path of the split being resized. + pub(crate) path: NodePath, + /// The boundary being dragged: between children `index` and `index + 1`. + pub(crate) index: usize, } /// A resize handle between two children of a split node. @@ -73,171 +72,168 @@ pub(crate) struct SplitHandleDrag { /// in the tree; paths are re-derived on every render, never stored across /// frames. pub(crate) struct SplitHandle { - /// Axis along which the parent split lays out its children; the handle - /// itself extends along the perpendicular axis. - direction: Axis, - /// Path of the split node this handle resizes, valid for the current - /// frame only. - path: NodePath, - /// Boundary this handle moves: between children `index` and `index + 1`. - index: usize, - /// Pointer position where the current drag started, if dragging. - drag_origin: Option, + /// Axis along which the parent split lays out its children; the handle + /// itself extends along the perpendicular axis. + direction: Axis, + /// Path of the split node this handle resizes, valid for the current + /// frame only. + path: NodePath, + /// Boundary this handle moves: between children `index` and `index + 1`. + index: usize, + /// Pointer position where the current drag started, if dragging. + drag_origin: Option, } impl SplitHandle { - /// Thickness of the handle's interactive area, in logical pixels. The - /// visual divider may be thinner; the wider hitbox makes the handle - /// grabbable. - pub(crate) const HITBOX: Pixels = Pixels(6.0); + /// Thickness of the handle's interactive area, in logical pixels. The + /// visual divider may be thinner; the wider hitbox makes the handle + /// grabbable. + pub(crate) const HITBOX: Pixels = Pixels(6.0); - /// Minimum extent, in logical pixels, that a split child may be resized - /// to by dragging. Expressed as a fraction of the parent extent when - /// computing the drag clamp. - pub(crate) const MIN_CHILD_EXTENT: Pixels = Pixels(120.0); + /// Minimum extent, in logical pixels, that a split child may be resized + /// to by dragging. Expressed as a fraction of the parent extent when + /// computing the drag clamp. + pub(crate) const MIN_CHILD_EXTENT: Pixels = Pixels(120.0); - /// The ratio a double-click resets to (even split). - pub(crate) const RESET_RATIO: f32 = 0.5; + /// The ratio a double-click resets to (even split). + pub(crate) const RESET_RATIO: f32 = 0.5; - /// Creates a handle for the boundary at `index` of the split at `path`. - pub(crate) fn new(direction: Axis, path: NodePath, index: usize) -> Self { - Self { - direction, - path, - index, - drag_origin: None, - } - } + /// Creates a handle for the boundary at `index` of the split at `path`. + pub(crate) fn new(direction: Axis, path: NodePath, index: usize) -> Self { + Self { + direction, + path, + index, + drag_origin: None, + } + } - /// Begins a drag, remembering the pointer origin. - pub(crate) fn begin_drag(&mut self, origin: Pixels) { - self.drag_origin = Some(origin); - } + /// Begins a drag, remembering the pointer origin. + pub(crate) fn begin_drag(&mut self, origin: Pixels) { + self.drag_origin = Some(origin); + } - /// Applies an in-progress drag: converts the pointer delta to a ratio - /// delta relative to the pair extent and emits a - /// [`SplitHandleEvent::ResizeRequested`]. - /// - /// `start_ratio` is the share of the `index` child within its pair at - /// drag start, re-read from the layout by the owning dock area on every - /// move so external edits during the drag are respected; `pair_extent` is - /// the combined on-screen extent of the two children, in pixels. - pub(crate) fn drag_to( - &mut self, - position: Pixels, - pair_extent: Pixels, - start_ratio: f32, - cx: &mut Context, - ) { - let Some(origin) = self.drag_origin else { - return; - }; - if pair_extent.0 <= 0.0 { - return; - } - // Keep both children above MIN_CHILD_EXTENT, but never clamp harder - // than a quarter of the pair so tiny parents stay resizable. - let min_ratio = (Self::MIN_CHILD_EXTENT.0 / pair_extent.0).min(0.25); - let max_ratio = 1.0 - min_ratio; - let ratio = (start_ratio + (position.0 - origin.0) / pair_extent.0) - .clamp(min_ratio, max_ratio); - cx.emit(SplitHandleEvent::ResizeRequested { - path: self.path.clone(), - index: self.index, - ratio, - }); - cx.notify(); - } + /// Applies an in-progress drag: converts the pointer delta to a ratio + /// delta relative to the pair extent and emits a + /// [`SplitHandleEvent::ResizeRequested`]. + /// + /// `start_ratio` is the share of the `index` child within its pair at + /// drag start, re-read from the layout by the owning dock area on every + /// move so external edits during the drag are respected; `pair_extent` is + /// the combined on-screen extent of the two children, in pixels. + pub(crate) fn drag_to( + &mut self, + position: Pixels, + pair_extent: Pixels, + start_ratio: f32, + cx: &mut Context, + ) { + let Some(origin) = self.drag_origin else { + return; + }; + if pair_extent.0 <= 0.0 { + return; + } + // Keep both children above MIN_CHILD_EXTENT, but never clamp harder + // than a quarter of the pair so tiny parents stay resizable. + let min_ratio = (Self::MIN_CHILD_EXTENT.0 / pair_extent.0).min(0.25); + let max_ratio = 1.0 - min_ratio; + let ratio = + (start_ratio + (position.0 - origin.0) / pair_extent.0).clamp(min_ratio, max_ratio); + cx.emit(SplitHandleEvent::ResizeRequested { + path: self.path.clone(), + index: self.index, + ratio, + }); + cx.notify(); + } - /// Ends the current drag, if any. - pub(crate) fn end_drag(&mut self) { - self.drag_origin = None; - } + /// Ends the current drag, if any. + pub(crate) fn end_drag(&mut self) { + self.drag_origin = None; + } - /// Emits a [`SplitHandleEvent::ResetRequested`] for a double-click. - pub(crate) fn reset(&mut self, cx: &mut Context) { - cx.emit(SplitHandleEvent::ResetRequested { - path: self.path.clone(), - index: self.index, - }); - cx.notify(); - } + /// Emits a [`SplitHandleEvent::ResetRequested`] for a double-click. + pub(crate) fn reset(&mut self, cx: &mut Context) { + cx.emit(SplitHandleEvent::ResetRequested { + path: self.path.clone(), + index: self.index, + }); + cx.notify(); + } } impl EventEmitter for SplitHandle {} impl Render for SplitHandle { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let direction = self.direction; - let handle = cx.entity(); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let direction = self.direction; + let handle = cx.entity(); - // Begins the drag on the handle (recording the pointer origin) and - // returns the ghost view shown under the pointer. - let ghost_ctor = move |_drag: &SplitHandleDrag, - origin: Point, - _window: &mut Window, - cx: &mut App| { - handle.update(cx, |handle, _cx| { - handle.begin_drag(if direction == Axis::Horizontal { - origin.x - } else { - origin.y - }); - }); - cx.new(|_cx| SplitDragGhost { direction }) - }; + // Begins the drag on the handle (recording the pointer origin) and + // returns the ghost view shown under the pointer. + let ghost_ctor = move |_drag: &SplitHandleDrag, + origin: Point, + _window: &mut Window, + cx: &mut App| { + handle.update(cx, |handle, _cx| { + handle.begin_drag(if direction == Axis::Horizontal { + origin.x + } else { + origin.y + }); + }); + cx.new(|_cx| SplitDragGhost { direction }) + }; - let mut root = div() - .id(ElementId::named_usize("dock-split-handle", path_key(&self.path))) - .flex_none() - .bg(colors.separator) - .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { - if event.click_count() >= 2 { - this.reset(cx); - } - })); + let mut root = div() + .id(ElementId::named_usize( + "dock-split-handle", + path_key(&self.path), + )) + .flex_none() + .bg(colors.separator) + .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + if event.click_count() >= 2 { + this.reset(cx); + } + })); - // A horizontal split stacks children side by side, so its divider is - // a vertical bar and vice versa. - match direction { - Axis::Horizontal => { - root = root - .w(px(Self::HITBOX.0)) - .h_full() - .cursor_col_resize(); - } - Axis::Vertical => { - root = root - .w_full() - .h(px(Self::HITBOX.0)) - .cursor_row_resize(); - } - } + // A horizontal split stacks children side by side, so its divider is + // a vertical bar and vice versa. + match direction { + Axis::Horizontal => { + root = root.w(px(Self::HITBOX.0)).h_full().cursor_col_resize(); + } + Axis::Vertical => { + root = root.w_full().h(px(Self::HITBOX.0)).cursor_row_resize(); + } + } - root.on_drag( - SplitHandleDrag { - path: self.path.clone(), - index: self.index, - }, - ghost_ctor, - ) - } + root.on_drag( + SplitHandleDrag { + path: self.path.clone(), + index: self.index, + }, + ghost_ctor, + ) + } } /// The floating view shown under the pointer while a handle is being dragged. struct SplitDragGhost { - direction: Axis, + direction: Axis, } impl Render for SplitDragGhost { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let mut ghost = div().bg(colors.border).rounded_sm(); - match self.direction { - Axis::Horizontal => ghost = ghost.w(px(2.0)).h(px(64.0)), - Axis::Vertical => ghost = ghost.w(px(64.0)).h(px(2.0)), - } - ghost - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let mut ghost = div().bg(colors.border).rounded_sm(); + match self.direction { + Axis::Horizontal => ghost = ghost.w(px(2.0)).h(px(64.0)), + Axis::Vertical => ghost = ghost.w(px(64.0)).h(px(2.0)), + } + ghost + } } diff --git a/crates/gpui/src/dock/tab_bar.rs b/crates/gpui/src/dock/tab_bar.rs index 52b982744e..8a4602ecb8 100644 --- a/crates/gpui/src/dock/tab_bar.rs +++ b/crates/gpui/src/dock/tab_bar.rs @@ -7,46 +7,46 @@ //! tracks the active tab, hosts close buttons, and is the drag source for //! both tab reordering and dock drags. -use crate::{ - div, px, App, AppContext, ClickEvent, Context, DragMoveEvent, ElementId, EventEmitter, - InteractiveElement, IntoElement, ParentElement, Pixels, Point, Render, ScrollDelta, - ScrollWheelEvent, SharedString, StatefulInteractiveElement, Styled, Window, -}; use crate::colors::DefaultColors; +use crate::{ + App, AppContext, ClickEvent, Context, DragMoveEvent, ElementId, EventEmitter, + InteractiveElement, IntoElement, ParentElement, Pixels, Point, Render, ScrollDelta, + ScrollWheelEvent, SharedString, StatefulInteractiveElement, Styled, Window, div, px, +}; use super::PanelId; /// Events emitted by a [`TabBar`] toward its owning [`DockArea`]. #[derive(Clone, Debug)] pub(crate) enum TabBarEvent { - /// The tab order or the active tab changed in place. - Reordered { - /// The new tab order (mirrors the owning `Tabs` node's `panels`). - tabs: Vec, - /// Index of the active tab in `tabs`. - active: usize, - }, - /// The user clicked the close button of a tab. - CloseRequested(PanelId), - /// A tab was dragged out of the strip; the dock area takes over the drag. - DockDragStarted { - /// The panel being dragged. - panel: PanelId, - /// Pointer position in window coordinates. - position: Point, - }, + /// The tab order or the active tab changed in place. + Reordered { + /// The new tab order (mirrors the owning `Tabs` node's `panels`). + tabs: Vec, + /// Index of the active tab in `tabs`. + active: usize, + }, + /// The user clicked the close button of a tab. + CloseRequested(PanelId), + /// A tab was dragged out of the strip; the dock area takes over the drag. + DockDragStarted { + /// The panel being dragged. + panel: PanelId, + /// Pointer position in window coordinates. + position: Point, + }, } /// A single tab's computed geometry within the strip, cached during render /// for hit-testing (close button, reorder, drag start). pub(crate) struct TabGeometry { - /// The panel this tab shows. - #[allow(dead_code)] // retained for future hit-testing of individual tabs - pub panel: PanelId, - /// Left edge of the tab relative to the strip. - pub x: Pixels, - /// Width of the tab. - pub width: Pixels, + /// The panel this tab shows. + #[allow(dead_code)] // retained for future hit-testing of individual tabs + pub panel: PanelId, + /// Left edge of the tab relative to the strip. + pub x: Pixels, + /// Width of the tab. + pub width: Pixels, } /// The tab strip for one `Tabs` node. @@ -75,252 +75,261 @@ pub(crate) struct TabGeometry { /// `active < tabs.len()`; the dock area re-creates or syncs the strip /// whenever the tree changes rather than the strip mutating the tree itself. pub(crate) struct TabBar { - /// Panels in tab order, mirroring the owning `Tabs` node. - tabs: Vec, - /// Index of the active tab. - active: usize, - /// Horizontal scroll offset for overflowed strips. - scroll_offset: Pixels, - /// Per-tab geometry from the last frame, for hit-testing. - geometry: Vec, - /// Tab titles from the last sync, rendered as the tab labels. - titles: Vec, - /// Per-tab closability from the last sync. - closable: Vec, + /// Panels in tab order, mirroring the owning `Tabs` node. + tabs: Vec, + /// Index of the active tab. + active: usize, + /// Horizontal scroll offset for overflowed strips. + scroll_offset: Pixels, + /// Per-tab geometry from the last frame, for hit-testing. + geometry: Vec, + /// Tab titles from the last sync, rendered as the tab labels. + titles: Vec, + /// Per-tab closability from the last sync. + closable: Vec, } impl TabBar { - /// Minimum width a tab is allowed to shrink to before the strip starts - /// scrolling instead. - pub(crate) const MIN_TAB_WIDTH: Pixels = Pixels(80.0); + /// Minimum width a tab is allowed to shrink to before the strip starts + /// scrolling instead. + pub(crate) const MIN_TAB_WIDTH: Pixels = Pixels(80.0); - /// Creates a strip for the given tabs; `active` is clamped into range. - pub(crate) fn new(tabs: Vec, active: usize) -> Self { - Self { - active: active.min(tabs.len().saturating_sub(1)), - tabs, - scroll_offset: Pixels(0.0), - geometry: Vec::new(), - titles: Vec::new(), - closable: Vec::new(), - } - } + /// Creates a strip for the given tabs; `active` is clamped into range. + pub(crate) fn new(tabs: Vec, active: usize) -> Self { + Self { + active: active.min(tabs.len().saturating_sub(1)), + tabs, + scroll_offset: Pixels(0.0), + geometry: Vec::new(), + titles: Vec::new(), + closable: Vec::new(), + } + } - /// Syncs the strip with the owning node after a tree edit, preserving - /// scroll position where possible. - /// - /// Called from the dock area during render, so it must not notify. - pub(crate) fn sync( - &mut self, - tabs: &[PanelId], - active: usize, - titles: &[SharedString], - closable: &[bool], - _cx: &mut Context, - ) { - self.tabs = tabs.to_vec(); - self.titles = titles.to_vec(); - self.closable = closable.to_vec(); - self.active = active.min(self.tabs.len().saturating_sub(1)); - } + /// Syncs the strip with the owning node after a tree edit, preserving + /// scroll position where possible. + /// + /// Called from the dock area during render, so it must not notify. + pub(crate) fn sync( + &mut self, + tabs: &[PanelId], + active: usize, + titles: &[SharedString], + closable: &[bool], + _cx: &mut Context, + ) { + self.tabs = tabs.to_vec(); + self.titles = titles.to_vec(); + self.closable = closable.to_vec(); + self.active = active.min(self.tabs.len().saturating_sub(1)); + } - /// Returns the index of the tab containing `point` (strip-relative - /// coordinates), using the cached geometry. - pub(crate) fn tab_index_at(&self, point: Point) -> Option { - let count = self.geometry.partition_point(|tab| tab.x.0 <= point.x.0); - if count == 0 { - return None; - } - // `count - 1` is the last tab whose left edge is left of the point; - // tabs are contiguous, so that tab contains the point. - Some(count - 1) - } + /// Returns the index of the tab containing `point` (strip-relative + /// coordinates), using the cached geometry. + pub(crate) fn tab_index_at(&self, point: Point) -> Option { + let count = self.geometry.partition_point(|tab| tab.x.0 <= point.x.0); + if count == 0 { + return None; + } + // `count - 1` is the last tab whose left edge is left of the point; + // tabs are contiguous, so that tab contains the point. + Some(count - 1) + } - /// Reorders the tab at `from` to position `to`, keeping the active tab - /// pointing at the same panel, and emits [`TabBarEvent::Reordered`] so - /// the dock area can rewrite the owning node. - /// - /// No-op if either index is out of range or `from == to`. - pub(crate) fn move_tab(&mut self, from: usize, to: usize, cx: &mut Context) { - if from >= self.tabs.len() || to >= self.tabs.len() || from == to { - return; - } - let active_panel = self.tabs.get(self.active).copied(); - let panel = self.tabs.remove(from); - let insert_at = if to > from { to - 1 } else { to }; - self.tabs.insert(insert_at, panel); - self.active = active_panel - .and_then(|p| self.tabs.iter().position(|tab| *tab == p)) - .unwrap_or(0); - cx.emit(TabBarEvent::Reordered { - tabs: self.tabs.clone(), - active: self.active, - }); - cx.notify(); - } + /// Reorders the tab at `from` to position `to`, keeping the active tab + /// pointing at the same panel, and emits [`TabBarEvent::Reordered`] so + /// the dock area can rewrite the owning node. + /// + /// No-op if either index is out of range or `from == to`. + pub(crate) fn move_tab(&mut self, from: usize, to: usize, cx: &mut Context) { + if from >= self.tabs.len() || to >= self.tabs.len() || from == to { + return; + } + let active_panel = self.tabs.get(self.active).copied(); + let panel = self.tabs.remove(from); + let insert_at = if to > from { to - 1 } else { to }; + self.tabs.insert(insert_at, panel); + self.active = active_panel + .and_then(|p| self.tabs.iter().position(|tab| *tab == p)) + .unwrap_or(0); + cx.emit(TabBarEvent::Reordered { + tabs: self.tabs.clone(), + active: self.active, + }); + cx.notify(); + } - /// Makes the tab at `index` active, scrolling it into view first. - fn activate(&mut self, index: usize, window: &mut Window, cx: &mut Context) { - if index >= self.tabs.len() { - return; - } - self.active = index; - self.scroll_tab_into_view(index, window, cx); - cx.emit(TabBarEvent::Reordered { - tabs: self.tabs.clone(), - active: self.active, - }); - cx.notify(); - } + /// Makes the tab at `index` active, scrolling it into view first. + fn activate(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if index >= self.tabs.len() { + return; + } + self.active = index; + self.scroll_tab_into_view(index, window, cx); + cx.emit(TabBarEvent::Reordered { + tabs: self.tabs.clone(), + active: self.active, + }); + cx.notify(); + } - /// Scrolls the strip so the tab at `index` is fully visible. - /// - /// The strip does not know the exact width of the visible viewport, so - /// this approximates it with the window width — good enough to bring an - /// overflowing tab back into view. - fn scroll_tab_into_view(&mut self, index: usize, window: &mut Window, _cx: &mut Context) { - let Some(geometry) = self.geometry.get(index) else { - return; - }; - let viewport = window.viewport_size().width.0; - let left = geometry.x.0; - let right = geometry.x.0 + geometry.width.0; - let scrolled = self.scroll_offset.0; - if left < scrolled { - self.scroll_offset = Pixels(left.max(0.0)); - } else if right > scrolled + viewport { - self.scroll_offset = Pixels((right - viewport).max(0.0)); - } - } + /// Scrolls the strip so the tab at `index` is fully visible. + /// + /// The strip does not know the exact width of the visible viewport, so + /// this approximates it with the window width — good enough to bring an + /// overflowing tab back into view. + fn scroll_tab_into_view(&mut self, index: usize, window: &mut Window, _cx: &mut Context) { + let Some(geometry) = self.geometry.get(index) else { + return; + }; + let viewport = window.viewport_size().width.0; + let left = geometry.x.0; + let right = geometry.x.0 + geometry.width.0; + let scrolled = self.scroll_offset.0; + if left < scrolled { + self.scroll_offset = Pixels(left.max(0.0)); + } else if right > scrolled + viewport { + self.scroll_offset = Pixels((right - viewport).max(0.0)); + } + } } impl EventEmitter for TabBar {} impl Render for TabBar { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); - // Recompute per-tab geometry from the current order and scroll offset. - self.geometry = self - .tabs - .iter() - .enumerate() - .map(|(index, &panel)| TabGeometry { - panel, - x: Pixels(index as f32 * Self::MIN_TAB_WIDTH.0 - self.scroll_offset.0), - width: Self::MIN_TAB_WIDTH, - }) - .collect(); + // Recompute per-tab geometry from the current order and scroll offset. + self.geometry = self + .tabs + .iter() + .enumerate() + .map(|(index, &panel)| TabGeometry { + panel, + x: Pixels(index as f32 * Self::MIN_TAB_WIDTH.0 - self.scroll_offset.0), + width: Self::MIN_TAB_WIDTH, + }) + .collect(); - let mut root = div() - .flex() - .flex_row() - .items_center() - .h(px(32.0)) - .w_full() - .overflow_hidden() - .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { - let delta = match event.delta { - ScrollDelta::Pixels(delta) => delta.x.0, - ScrollDelta::Lines(delta) => delta.x * 20.0, - }; - this.scroll_offset = Pixels((this.scroll_offset.0 + delta).max(0.0)); - cx.notify(); - })) - .on_drag_move::(cx.listener(|this, event: &DragMoveEvent, _window, cx| { - let dragged = *event.drag(cx); - // Ignore drags of panels that don't belong to this strip - // (e.g. a dock-level drag from another group passing over). - if !this.tabs.contains(&dragged) { - return; - } - if event.bounds.contains(&event.event.position) { - if let Some(to) = this.tab_index_at(event.event.position) { - if let Some(from) = this.tabs.iter().position(|tab| *tab == dragged) { - this.move_tab(from, to, cx); - } - } - } else { - // The tab left the strip: hand the drag to the dock area. - cx.emit(TabBarEvent::DockDragStarted { - panel: dragged, - position: event.event.position, - }); - } - })); + let mut root = div() + .flex() + .flex_row() + .items_center() + .h(px(32.0)) + .w_full() + .overflow_hidden() + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { + let delta = match event.delta { + ScrollDelta::Pixels(delta) => delta.x.0, + ScrollDelta::Lines(delta) => delta.x * 20.0, + }; + this.scroll_offset = Pixels((this.scroll_offset.0 + delta).max(0.0)); + cx.notify(); + })) + .on_drag_move::(cx.listener( + |this, event: &DragMoveEvent, _window, cx| { + let dragged = *event.drag(cx); + // Ignore drags of panels that don't belong to this strip + // (e.g. a dock-level drag from another group passing over). + if !this.tabs.contains(&dragged) { + return; + } + if event.bounds.contains(&event.event.position) { + if let Some(to) = this.tab_index_at(event.event.position) { + if let Some(from) = this.tabs.iter().position(|tab| *tab == dragged) { + this.move_tab(from, to, cx); + } + } + } else { + // The tab left the strip: hand the drag to the dock area. + cx.emit(TabBarEvent::DockDragStarted { + panel: dragged, + position: event.event.position, + }); + } + }, + )); - for (index, &panel) in self.tabs.iter().enumerate() { - let active = index == self.active; - let title = self - .titles - .get(index) - .cloned() - .unwrap_or_else(|| SharedString::from("Tab")); - let closable = self.closable.get(index).copied().unwrap_or(false); + for (index, &panel) in self.tabs.iter().enumerate() { + let active = index == self.active; + let title = self + .titles + .get(index) + .cloned() + .unwrap_or_else(|| SharedString::from("Tab")); + let closable = self.closable.get(index).copied().unwrap_or(false); - // Ghost shown under the pointer while this tab is being dragged. - let ghost_title = title.clone(); - let ghost_ctor = move |_panel: &PanelId, - _origin: Point, - _window: &mut Window, - cx: &mut App| { - cx.new(|_cx| TabDragGhost { title: ghost_title.clone() }) - }; + // Ghost shown under the pointer while this tab is being dragged. + let ghost_title = title.clone(); + let ghost_ctor = move |_panel: &PanelId, + _origin: Point, + _window: &mut Window, + cx: &mut App| { + cx.new(|_cx| TabDragGhost { + title: ghost_title.clone(), + }) + }; - let mut tab = div() - .id(ElementId::named_usize("dock-tab", panel.raw() as usize)) - .w(px(Self::MIN_TAB_WIDTH.0)) - .flex_none() - .h_full() - .cursor_pointer() - .bg(if active { colors.selected } else { colors.background }) - .text_color(if active { colors.text } else { colors.disabled }) - .child(title) - .on_click(cx.listener(move |this, _event: &ClickEvent, window, cx| { - this.activate(index, window, cx); - })) - .on_drag(panel, ghost_ctor); + let mut tab = div() + .id(ElementId::named_usize("dock-tab", panel.raw() as usize)) + .w(px(Self::MIN_TAB_WIDTH.0)) + .flex_none() + .h_full() + .cursor_pointer() + .bg(if active { + colors.selected + } else { + colors.background + }) + .text_color(if active { colors.text } else { colors.disabled }) + .child(title) + .on_click(cx.listener(move |this, _event: &ClickEvent, window, cx| { + this.activate(index, window, cx); + })) + .on_drag(panel, ghost_ctor); - if closable { - tab = tab.child( - div() - .id(ElementId::named_usize("dock-tab-close", panel.raw() as usize)) - .cursor_pointer() - .text_xs() - .text_color(colors.disabled) - .child("✕") - .on_click(cx.listener( - move |_this, _event: &ClickEvent, _window, cx| { - cx.stop_propagation(); - cx.emit(TabBarEvent::CloseRequested(panel)); - }, - )), - ); - } + if closable { + tab = tab.child( + div() + .id(ElementId::named_usize( + "dock-tab-close", + panel.raw() as usize, + )) + .cursor_pointer() + .text_xs() + .text_color(colors.disabled) + .child("✕") + .on_click(cx.listener(move |_this, _event: &ClickEvent, _window, cx| { + cx.stop_propagation(); + cx.emit(TabBarEvent::CloseRequested(panel)); + })), + ); + } - root = root.child(tab); - } + root = root.child(tab); + } - root - } + root + } } /// The floating view shown under the pointer while a tab is being dragged. struct TabDragGhost { - title: SharedString, + title: SharedString, } impl Render for TabDragGhost { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - div() - .px_2() - .py_1() - .rounded_md() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .shadow_md() - .child(self.title.clone()) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .px_2() + .py_1() + .rounded_md() + .bg(colors.background) + .border_1() + .border_color(colors.border) + .shadow_md() + .child(self.title.clone()) + } } diff --git a/crates/gpui/src/effect_stack/card.rs b/crates/gpui/src/effect_stack/card.rs index ea68c8197c..3a91e3f0d1 100644 --- a/crates/gpui/src/effect_stack/card.rs +++ b/crates/gpui/src/effect_stack/card.rs @@ -7,8 +7,8 @@ //! card would land. use crate::{ - colors::DefaultColors, div, px, AnyView, IntoElement, ParentElement, Pixels, RenderOnce, - SharedString, Styled, Window, + AnyView, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled, Window, + colors::DefaultColors, div, px, }; use super::data::{EffectCardKind, EffectId}; @@ -50,205 +50,209 @@ use super::data::{EffectCardKind, EffectId}; /// implementation; treat this as the contract.) #[derive(IntoElement)] pub struct EffectCard { - id: EffectId, - kind: EffectCardKind, - title: SharedString, - subtitle: Option, - enabled: bool, - expanded: bool, - removable: bool, - reorderable: bool, - badge_count: Option, - params: Option, - drag_ghost: bool, + id: EffectId, + kind: EffectCardKind, + title: SharedString, + subtitle: Option, + enabled: bool, + expanded: bool, + removable: bool, + reorderable: bool, + badge_count: Option, + params: Option, + drag_ghost: bool, } impl EffectCard { - /// Creates a card for the given effect. - /// - /// Defaults: [`EffectCardKind::Effect`], enabled, collapsed, removable - /// and reorderable, no subtitle, badge, params view, or drag ghost. - pub fn new(id: EffectId) -> Self { - Self { - id, - kind: EffectCardKind::Effect, - title: SharedString::default(), - subtitle: None, - enabled: true, - expanded: false, - removable: true, - reorderable: true, - badge_count: None, - params: None, - drag_ghost: false, - } - } + /// Creates a card for the given effect. + /// + /// Defaults: [`EffectCardKind::Effect`], enabled, collapsed, removable + /// and reorderable, no subtitle, badge, params view, or drag ghost. + pub fn new(id: EffectId) -> Self { + Self { + id, + kind: EffectCardKind::Effect, + title: SharedString::default(), + subtitle: None, + enabled: true, + expanded: false, + removable: true, + reorderable: true, + badge_count: None, + params: None, + drag_ghost: false, + } + } - /// The effect this card represents. - pub fn id(&self) -> EffectId { - self.id - } + /// The effect this card represents. + pub fn id(&self) -> EffectId { + self.id + } - /// Sets the card's role in the chain. Source/output cards drop the - /// drag handle, enable toggle, and remove button regardless of the - /// `removable`/`reorderable` flags. - pub fn kind(mut self, kind: EffectCardKind) -> Self { - self.kind = kind; - self - } + /// Sets the card's role in the chain. Source/output cards drop the + /// drag handle, enable toggle, and remove button regardless of the + /// `removable`/`reorderable` flags. + pub fn kind(mut self, kind: EffectCardKind) -> Self { + self.kind = kind; + self + } - /// Sets the primary header label. - pub fn title(mut self, title: impl Into) -> Self { - self.title = title.into(); - self - } + /// Sets the primary header label. + pub fn title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } - /// Sets the optional muted secondary line (e.g. a LUT filename). - pub fn subtitle(mut self, subtitle: Option) -> Self { - self.subtitle = subtitle; - self - } + /// Sets the optional muted secondary line (e.g. a LUT filename). + pub fn subtitle(mut self, subtitle: Option) -> Self { + self.subtitle = subtitle; + self + } - /// Sets whether the effect is enabled. Disabled cards are dimmed. - pub fn enabled(mut self, enabled: bool) -> Self { - self.enabled = enabled; - self - } + /// Sets whether the effect is enabled. Disabled cards are dimmed. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } - /// Sets whether the parameter area is shown. Has no visual effect when - /// no params view was provided via [`params`](EffectCard::params). - pub fn expanded(mut self, expanded: bool) -> Self { - self.expanded = expanded; - self - } + /// Sets whether the parameter area is shown. Has no visual effect when + /// no params view was provided via [`params`](EffectCard::params). + pub fn expanded(mut self, expanded: bool) -> Self { + self.expanded = expanded; + self + } - /// Sets whether the remove button is shown. - pub fn removable(mut self, removable: bool) -> Self { - self.removable = removable; - self - } + /// Sets whether the remove button is shown. + pub fn removable(mut self, removable: bool) -> Self { + self.removable = removable; + self + } - /// Sets whether the drag handle is shown and the card can start a drag. - pub fn reorderable(mut self, reorderable: bool) -> Self { - self.reorderable = reorderable; - self - } + /// Sets whether the drag handle is shown and the card can start a drag. + pub fn reorderable(mut self, reorderable: bool) -> Self { + self.reorderable = reorderable; + self + } - /// Sets the optional numeric badge (e.g. animated-parameter count). - /// `None` (or `Some(0)`) hides the badge. - pub fn badge_count(mut self, badge_count: Option) -> Self { - self.badge_count = badge_count; - self - } + /// Sets the optional numeric badge (e.g. animated-parameter count). + /// `None` (or `Some(0)`) hides the badge. + pub fn badge_count(mut self, badge_count: Option) -> Self { + self.badge_count = badge_count; + self + } - /// Sets the parameter-area content, usually produced by the app's - /// [`ParamsRenderer`](crate::effect_stack::ParamsRenderer). Only laid - /// out when the card is expanded. - pub fn params(mut self, params: AnyView) -> Self { - self.params = Some(params); - self - } + /// Sets the parameter-area content, usually produced by the app's + /// [`ParamsRenderer`](crate::effect_stack::ParamsRenderer). Only laid + /// out when the card is expanded. + pub fn params(mut self, params: AnyView) -> Self { + self.params = Some(params); + self + } - /// Sets whether this card renders as the semi-transparent drag ghost - /// (i.e. it is the card currently being dragged). - pub fn drag_ghost(mut self, drag_ghost: bool) -> Self { - self.drag_ghost = drag_ghost; - self - } + /// Sets whether this card renders as the semi-transparent drag ghost + /// (i.e. it is the card currently being dragged). + pub fn drag_ghost(mut self, drag_ghost: bool) -> Self { + self.drag_ghost = drag_ghost; + self + } } impl RenderOnce for EffectCard { - fn render(self, _window: &mut Window, cx: &mut crate::App) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let fixed = self.kind != EffectCardKind::Effect; - let title_color = if self.enabled { colors.text } else { colors.disabled }; + fn render(self, _window: &mut Window, cx: &mut crate::App) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let fixed = self.kind != EffectCardKind::Effect; + let title_color = if self.enabled { + colors.text + } else { + colors.disabled + }; - // Header row: the title block (flexing to fill the row), an optional - // numeric badge, and the expand chevron for effects. The drag handle, - // enable toggle, and remove button are rendered by - // `EffectStackView`'s card wrapper, which wires them to - // `EffectStackEvent`. - let mut header = div() - .flex() - .flex_row() - .items_center() - .gap_1() - .min_w_0() - .px_2() - .py_1() - .child( - div() - .flex_1() - .min_w_0() - .text_ellipsis() - .text_color(title_color) - .child(self.title), - ); + // Header row: the title block (flexing to fill the row), an optional + // numeric badge, and the expand chevron for effects. The drag handle, + // enable toggle, and remove button are rendered by + // `EffectStackView`'s card wrapper, which wires them to + // `EffectStackEvent`. + let mut header = div() + .flex() + .flex_row() + .items_center() + .gap_1() + .min_w_0() + .px_2() + .py_1() + .child( + div() + .flex_1() + .min_w_0() + .text_ellipsis() + .text_color(title_color) + .child(self.title), + ); - if let Some(count) = self.badge_count.filter(|&count| count > 0) { - header = header.child( - div() - .flex() - .items_center() - .rounded_full() - .bg(colors.selected) - .px_1() - .py_0p5() - .text_xs() - .text_color(colors.selected_text) - .child(if count > 99 { - SharedString::from("99+") - } else { - SharedString::from(count.to_string()) - }), - ); - } + if let Some(count) = self.badge_count.filter(|&count| count > 0) { + header = header.child( + div() + .flex() + .items_center() + .rounded_full() + .bg(colors.selected) + .px_1() + .py_0p5() + .text_xs() + .text_color(colors.selected_text) + .child(if count > 99 { + SharedString::from("99+") + } else { + SharedString::from(count.to_string()) + }), + ); + } - // Effects show an expand chevron; source/output cards are fixed and - // have no expandable parameter area. - if !fixed { - header = header.child( - div() - .text_xs() - .text_color(colors.disabled) - .child(if self.expanded { "⌄" } else { "⌃" }), - ); - } + // Effects show an expand chevron; source/output cards are fixed and + // have no expandable parameter area. + if !fixed { + header = header.child( + div() + .text_xs() + .text_color(colors.disabled) + .child(if self.expanded { "⌄" } else { "⌃" }), + ); + } - let mut card = div().flex().flex_col().flex_1().min_w_0(); - // Indent compensation for the handle/remove buttons drawn by the - // stack view beside this card, and the semi-transparent drag ghost. - if self.reorderable { - card = card.pl_1(); - } - if self.removable { - card = card.pr_1(); - } - if self.drag_ghost { - card = card.opacity(0.5); - } - card = card.child(header); + let mut card = div().flex().flex_col().flex_1().min_w_0(); + // Indent compensation for the handle/remove buttons drawn by the + // stack view beside this card, and the semi-transparent drag ghost. + if self.reorderable { + card = card.pl_1(); + } + if self.removable { + card = card.pr_1(); + } + if self.drag_ghost { + card = card.opacity(0.5); + } + card = card.child(header); - if let Some(subtitle) = self.subtitle { - card = card.child( - div() - .w_full() - .text_xs() - .text_color(colors.disabled) - .text_ellipsis() - .child(subtitle), - ); - } + if let Some(subtitle) = self.subtitle { + card = card.child( + div() + .w_full() + .text_xs() + .text_color(colors.disabled) + .text_ellipsis() + .child(subtitle), + ); + } - // The parameter-area view is laid out by `EffectStackView` inside - // the expanded card body rather than here; reading the slot keeps - // this purely-visual component's contract exercised. - if self.expanded && self.params.is_some() { - // Rendered by EffectStackView::render. - } + // The parameter-area view is laid out by `EffectStackView` inside + // the expanded card body rather than here; reading the slot keeps + // this purely-visual component's contract exercised. + if self.expanded && self.params.is_some() { + // Rendered by EffectStackView::render. + } - card - } + card + } } /// The horizontal line shown between cards to indicate where a dragged card @@ -262,52 +266,53 @@ impl RenderOnce for EffectCard { /// render in a "not allowed" style. #[derive(Clone, Copy, Debug, Default, IntoElement)] pub struct InsertIndicator { - valid: bool, - thickness: Option, + valid: bool, + thickness: Option, } impl InsertIndicator { - /// Creates an indicator for a valid drop position. - pub fn valid() -> Self { - Self { - valid: true, - thickness: None, - } - } + /// Creates an indicator for a valid drop position. + pub fn valid() -> Self { + Self { + valid: true, + thickness: None, + } + } - /// Creates an indicator for a rejected drop position (renders in a - /// "not allowed" style, e.g. red/dashed). - pub fn invalid() -> Self { - Self { - valid: false, - thickness: None, - } - } + /// Creates an indicator for a rejected drop position (renders in a + /// "not allowed" style, e.g. red/dashed). + pub fn invalid() -> Self { + Self { + valid: false, + thickness: None, + } + } - /// Whether this indicator marks an accepted drop position. - pub fn is_valid(&self) -> bool { - self.valid - } + /// Whether this indicator marks an accepted drop position. + pub fn is_valid(&self) -> bool { + self.valid + } - /// Overrides the line thickness. `None` uses the theme default. - pub fn thickness(mut self, thickness: Pixels) -> Self { - self.thickness = Some(thickness); - self - } + /// Overrides the line thickness. `None` uses the theme default. + pub fn thickness(mut self, thickness: Pixels) -> Self { + self.thickness = Some(thickness); + self + } } impl RenderOnce for InsertIndicator { - fn render(self, _window: &mut Window, cx: &mut crate::App) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let thickness = self.thickness.unwrap_or(px(2.0)); - let line = div() - .w_full() - .h(thickness) - .bg(if self.valid { colors.selected } else { colors.disabled }); - if self.valid { - line - } else { - line.border_dashed() - } - } + fn render(self, _window: &mut Window, cx: &mut crate::App) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let thickness = self.thickness.unwrap_or(px(2.0)); + let line = div().w_full().h(thickness).bg(if self.valid { + colors.selected + } else { + colors.disabled + }); + if self.valid { + line + } else { + line.border_dashed() + } + } } diff --git a/crates/gpui/src/effect_stack/data.rs b/crates/gpui/src/effect_stack/data.rs index b8336ca139..c584a95e52 100644 --- a/crates/gpui/src/effect_stack/data.rs +++ b/crates/gpui/src/effect_stack/data.rs @@ -28,15 +28,15 @@ use crate::SharedString; pub struct EffectId(pub u64); impl fmt::Display for EffectId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "effect-{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "effect-{}", self.0) + } } impl From for EffectId { - fn from(raw: u64) -> Self { - Self(raw) - } + fn from(raw: u64) -> Self { + Self(raw) + } } /// Which role a card plays in the linear chain. @@ -49,14 +49,14 @@ impl From for EffectId { /// what [`EffectData::is_removable`] / [`EffectData::is_reorderable`] say. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum EffectCardKind { - /// The media/source card (e.g. the clip's footage). Fixed at the top of - /// the stack; not removable, not reorderable. - Source, - /// A regular effect card. Reorderable and removable by default. - Effect, - /// The output card. Fixed at the bottom of the stack; not removable, - /// not reorderable. - Output, + /// The media/source card (e.g. the clip's footage). Fixed at the top of + /// the stack; not removable, not reorderable. + Source, + /// A regular effect card. Reorderable and removable by default. + Effect, + /// The output card. Fixed at the bottom of the stack; not removable, + /// not reorderable. + Output, } /// Read-only view of one card in the effect stack. @@ -70,66 +70,66 @@ pub enum EffectCardKind { /// applies them to its model, and the next render observes the new values /// here. pub trait EffectData: 'static { - /// Stable identity of this card; see [`EffectId`] for invariants. - fn id(&self) -> EffectId; + /// Stable identity of this card; see [`EffectId`] for invariants. + fn id(&self) -> EffectId; - /// The role of this card in the chain. See [`EffectCardKind`]. - fn kind(&self) -> EffectCardKind; + /// The role of this card in the chain. See [`EffectCardKind`]. + fn kind(&self) -> EffectCardKind; - /// Primary label shown in the card header (e.g. `"Transform"`, - /// `"OCIO LUT"`). - fn title(&self) -> SharedString; + /// Primary label shown in the card header (e.g. `"Transform"`, + /// `"OCIO LUT"`). + fn title(&self) -> SharedString; - /// Optional secondary line shown under the title in a muted style, e.g. - /// the LUT filename or a one-line parameter summary. - /// - /// Defaults to `None` (no subtitle row is laid out). - fn subtitle(&self) -> Option { - None - } + /// Optional secondary line shown under the title in a muted style, e.g. + /// the LUT filename or a one-line parameter summary. + /// + /// Defaults to `None` (no subtitle row is laid out). + fn subtitle(&self) -> Option { + None + } - /// Whether the effect is currently enabled (not bypassed). - /// - /// Disabled cards are rendered dimmed and their parameter area is - /// inert. Source and output cards should return `true`; the view does - /// not render an enable toggle for them. - fn is_enabled(&self) -> bool; + /// Whether the effect is currently enabled (not bypassed). + /// + /// Disabled cards are rendered dimmed and their parameter area is + /// inert. Source and output cards should return `true`; the view does + /// not render an enable toggle for them. + fn is_enabled(&self) -> bool; - /// Whether the card's parameter area is currently expanded. - /// - /// Expansion state lives in the app's model (so it can persist and sync - /// with [`crate::node_graph`]); toggling it is requested via - /// [`EffectStackEvent::ExpansionToggled`](crate::effect_stack::EffectStackEvent::ExpansionToggled). - fn is_expanded(&self) -> bool; + /// Whether the card's parameter area is currently expanded. + /// + /// Expansion state lives in the app's model (so it can persist and sync + /// with [`crate::node_graph`]); toggling it is requested via + /// [`EffectStackEvent::ExpansionToggled`](crate::effect_stack::EffectStackEvent::ExpansionToggled). + fn is_expanded(&self) -> bool; - /// Whether the card may be removed from the stack. - /// - /// Defaults to `true` for [`EffectCardKind::Effect`] and `false` for - /// source/output cards. When `false`, the remove affordance is hidden - /// and [`EffectStackEvent::RemoveRequested`](crate::effect_stack::EffectStackEvent::RemoveRequested) - /// is never emitted for this card. - fn is_removable(&self) -> bool { - self.kind() == EffectCardKind::Effect - } + /// Whether the card may be removed from the stack. + /// + /// Defaults to `true` for [`EffectCardKind::Effect`] and `false` for + /// source/output cards. When `false`, the remove affordance is hidden + /// and [`EffectStackEvent::RemoveRequested`](crate::effect_stack::EffectStackEvent::RemoveRequested) + /// is never emitted for this card. + fn is_removable(&self) -> bool { + self.kind() == EffectCardKind::Effect + } - /// Whether the card may be reordered by dragging. - /// - /// Defaults to `true` for [`EffectCardKind::Effect`] and `false` for - /// source/output cards. When `false`, the drag handle is hidden and the - /// card never participates in a reorder (neither as the dragged card - /// nor as a displaced neighbor position). - fn is_reorderable(&self) -> bool { - self.kind() == EffectCardKind::Effect - } + /// Whether the card may be reordered by dragging. + /// + /// Defaults to `true` for [`EffectCardKind::Effect`] and `false` for + /// source/output cards. When `false`, the drag handle is hidden and the + /// card never participates in a reorder (neither as the dragged card + /// nor as a displaced neighbor position). + fn is_reorderable(&self) -> bool { + self.kind() == EffectCardKind::Effect + } - /// Optional numeric badge shown in the card header, e.g. the number of - /// animated parameters on the effect. - /// - /// `Some(0)` and `None` both render no badge; prefer `None`. Large - /// values are clamped visually (e.g. `99+`). - fn badge_count(&self) -> Option { - None - } + /// Optional numeric badge shown in the card header, e.g. the number of + /// animated parameters on the effect. + /// + /// `Some(0)` and `None` both render no badge; prefer `None`. Large + /// values are clamped visually (e.g. `99+`). + fn badge_count(&self) -> Option { + None + } } /// Ordered collection of effect cards backing an [`EffectStackView`](crate::effect_stack::EffectStackView). @@ -146,37 +146,37 @@ pub trait EffectData: 'static { /// and [`can_reorder`](EffectStackDataSource::can_reorder) refer to /// positions in this list. pub trait EffectStackDataSource: 'static { - /// All cards in the stack, top-to-bottom (signal order). - /// - /// May be empty (or contain only source/output) — see - /// [`target_label`](EffectStackDataSource::target_label) for the - /// empty-selection state, which is distinct from a stack with no - /// effects. - fn effects(&self) -> Vec>; + /// All cards in the stack, top-to-bottom (signal order). + /// + /// May be empty (or contain only source/output) — see + /// [`target_label`](EffectStackDataSource::target_label) for the + /// empty-selection state, which is distinct from a stack with no + /// effects. + fn effects(&self) -> Vec>; - /// Label describing what this stack edits, e.g. the clip name shown in - /// the panel header. - /// - /// Return `None` when there is no valid selection (no clip under the - /// playhead, multi-selection, etc.). The view then renders an empty - /// state instead of cards and suppresses all card interactions. - fn target_label(&self) -> Option; + /// Label describing what this stack edits, e.g. the clip name shown in + /// the panel header. + /// + /// Return `None` when there is no valid selection (no clip under the + /// playhead, multi-selection, etc.). The view then renders an empty + /// state instead of cards and suppresses all card interactions. + fn target_label(&self) -> Option; - /// Whether dropping the given effect at `new_index` (an index into the - /// list returned by [`effects`](EffectStackDataSource::effects)) would - /// be a valid reorder. - /// - /// Called continuously during a drag to drive the insertion indicator; - /// invalid positions render as "not allowed". The default - /// implementation allows any position between the source and output - /// cards. Apps override this to reject reorders that would produce - /// invalid signal chains (e.g. a node that requires two inputs). - /// - /// Note this is advisory UI feedback only — the app re-validates when - /// the actual - /// [`ReorderRequested`](crate::effect_stack::EffectStackEvent::ReorderRequested) - /// event arrives. - fn can_reorder(&self, _id: EffectId, _new_index: usize) -> bool { - true - } + /// Whether dropping the given effect at `new_index` (an index into the + /// list returned by [`effects`](EffectStackDataSource::effects)) would + /// be a valid reorder. + /// + /// Called continuously during a drag to drive the insertion indicator; + /// invalid positions render as "not allowed". The default + /// implementation allows any position between the source and output + /// cards. Apps override this to reject reorders that would produce + /// invalid signal chains (e.g. a node that requires two inputs). + /// + /// Note this is advisory UI feedback only — the app re-validates when + /// the actual + /// [`ReorderRequested`](crate::effect_stack::EffectStackEvent::ReorderRequested) + /// event arrives. + fn can_reorder(&self, _id: EffectId, _new_index: usize) -> bool { + true + } } diff --git a/crates/gpui/src/effect_stack/stack_view.rs b/crates/gpui/src/effect_stack/stack_view.rs index b82b3d3646..68ca6d6ff2 100644 --- a/crates/gpui/src/effect_stack/stack_view.rs +++ b/crates/gpui/src/effect_stack/stack_view.rs @@ -8,10 +8,10 @@ use std::rc::Rc; use crate::{ - colors::DefaultColors, div, AnyView, App, AppContext, ClickEvent, Context, DragMoveEvent, - ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, - MouseButton, ParentElement, Pixels, Point, Render, SharedString, StatefulInteractiveElement, - Styled, Window, + AnyView, App, AppContext, ClickEvent, Context, DragMoveEvent, ElementId, Entity, EventEmitter, + FocusHandle, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, + Point, Render, SharedString, StatefulInteractiveElement, Styled, Window, colors::DefaultColors, + div, }; use super::card::{EffectCard, InsertIndicator}; @@ -25,8 +25,7 @@ use super::data::{EffectCardKind, EffectId, EffectStackDataSource}; /// is placed in the card's content slot; its size drives the expanded /// height of the card. Return any empty view (e.g. [`crate::div()`]'s /// default) to render a blank parameter area. -pub type ParamsRenderer = - Rc AnyView>; +pub type ParamsRenderer = Rc AnyView>; /// Edit requests emitted by [`EffectStackView`]. /// @@ -39,65 +38,65 @@ pub type ParamsRenderer = /// unchanged model. #[derive(Clone, Debug)] pub enum EffectStackEvent { - /// The user dragged a card to a new position. - /// - /// `new_index` is an index into the list returned by - /// [`EffectStackDataSource::effects`] **after** removal of the dragged - /// card (i.e. an insertion position). The app maps this to rewiring the - /// node-graph path; see the [module docs](crate::effect_stack). The - /// view has already filtered positions rejected by - /// [`EffectStackDataSource::can_reorder`], but the app must re-validate. - ReorderRequested { - /// The dragged effect. - effect: EffectId, - /// Insertion index in the post-removal card list. - new_index: usize, - }, - /// The user clicked the enable/disable toggle on a card. - EnableToggled { - /// The toggled effect. - effect: EffectId, - /// The desired new enabled state. - enabled: bool, - }, - /// The user clicked a card header to expand or collapse its parameter - /// area. - ExpansionToggled { - /// The toggled effect. - effect: EffectId, - /// The desired new expansion state. - expanded: bool, - }, - /// The user clicked the remove button on a removable card. - RemoveRequested(EffectId), - /// The user invoked an "add effect" affordance at a stack position. - /// - /// The app typically responds by opening its effect browser; once the - /// user picks an effect, the app inserts the corresponding node at - /// `index` and notifies. - AddRequested { - /// Insertion index into the current card list. - index: usize, - }, - /// The user secondary-clicked a card. The app owns the menu itself — - /// the view only reports where and on which card it happened. - ContextMenuRequested { - /// The effect that was clicked. - effect: EffectId, - /// Mouse position in window coordinates, suitable for positioning a - /// context menu. - position: Point, - }, - /// A parameter of an effect changed inside its card's parameter area. - /// - /// Emitted when the app's parameter UI calls - /// [`EffectStackView::notify_parameter_changed`]. The view uses it to - /// refresh card metadata (e.g. the animated-parameter badge); the app - /// may additionally subscribe to e.g. schedule a preview re-render. - ParameterChanged { - /// The effect whose parameters changed. - effect: EffectId, - }, + /// The user dragged a card to a new position. + /// + /// `new_index` is an index into the list returned by + /// [`EffectStackDataSource::effects`] **after** removal of the dragged + /// card (i.e. an insertion position). The app maps this to rewiring the + /// node-graph path; see the [module docs](crate::effect_stack). The + /// view has already filtered positions rejected by + /// [`EffectStackDataSource::can_reorder`], but the app must re-validate. + ReorderRequested { + /// The dragged effect. + effect: EffectId, + /// Insertion index in the post-removal card list. + new_index: usize, + }, + /// The user clicked the enable/disable toggle on a card. + EnableToggled { + /// The toggled effect. + effect: EffectId, + /// The desired new enabled state. + enabled: bool, + }, + /// The user clicked a card header to expand or collapse its parameter + /// area. + ExpansionToggled { + /// The toggled effect. + effect: EffectId, + /// The desired new expansion state. + expanded: bool, + }, + /// The user clicked the remove button on a removable card. + RemoveRequested(EffectId), + /// The user invoked an "add effect" affordance at a stack position. + /// + /// The app typically responds by opening its effect browser; once the + /// user picks an effect, the app inserts the corresponding node at + /// `index` and notifies. + AddRequested { + /// Insertion index into the current card list. + index: usize, + }, + /// The user secondary-clicked a card. The app owns the menu itself — + /// the view only reports where and on which card it happened. + ContextMenuRequested { + /// The effect that was clicked. + effect: EffectId, + /// Mouse position in window coordinates, suitable for positioning a + /// context menu. + position: Point, + }, + /// A parameter of an effect changed inside its card's parameter area. + /// + /// Emitted when the app's parameter UI calls + /// [`EffectStackView::notify_parameter_changed`]. The view uses it to + /// refresh card metadata (e.g. the animated-parameter badge); the app + /// may additionally subscribe to e.g. schedule a preview re-render. + ParameterChanged { + /// The effect whose parameters changed. + effect: EffectId, + }, } /// Transient drag state for an in-progress card reorder. @@ -108,11 +107,11 @@ pub enum EffectStackEvent { /// on drop or cancel; never survives into the app's model. #[derive(Clone, Copy, Debug, Default)] pub struct DragState { - /// The card currently being dragged, if any. - pub dragged: Option, - /// Current insertion index (into the post-removal list) while dragging, - /// if the pointer is over a valid drop position. - pub insertion_index: Option, + /// The card currently being dragged, if any. + pub dragged: Option, + /// Current insertion index (into the post-removal list) while dragging, + /// if the pointer is over a valid drop position. + pub insertion_index: Option, } /// The linear effect-stack inspector view. @@ -149,456 +148,477 @@ pub struct DragState { /// a virtualized list would fight against. Revisit if profiles show /// otherwise. pub struct EffectStackView { - data: Entity, - params_renderer: Option, - focus_handle: FocusHandle, - drag_state: DragState, + data: Entity, + params_renderer: Option, + focus_handle: FocusHandle, + drag_state: DragState, } impl EffectStackView { - /// Creates a new view over the given data-source entity. - /// - /// The view does not subscribe to the entity itself; the app is - /// expected to call [`cx.notify()`](Context::notify) on the data source - /// (or on this view) after applying edits, per the "edits are requests" - /// contract. - pub fn new(data: Entity, cx: &mut Context) -> Self { - Self { - data, - params_renderer: None, - focus_handle: cx.focus_handle(), - drag_state: DragState::default(), - } - } + /// Creates a new view over the given data-source entity. + /// + /// The view does not subscribe to the entity itself; the app is + /// expected to call [`cx.notify()`](Context::notify) on the data source + /// (or on this view) after applying edits, per the "edits are requests" + /// contract. + pub fn new(data: Entity, cx: &mut Context) -> Self { + Self { + data, + params_renderer: None, + focus_handle: cx.focus_handle(), + drag_state: DragState::default(), + } + } - /// Registers the app callback that renders an effect's parameter - /// controls inside its expanded card. See [`ParamsRenderer`]. - /// - /// Builder style; call once at setup: - /// - /// ```ignore - /// let stack = cx.new(|cx| { - /// EffectStackView::new(data, cx).params_renderer(|id, window, cx| { - /// my_effect_params_view(*id).into() - /// }) - /// }); - /// ``` - /// - /// Cards of [`EffectCardKind::Source`](crate::effect_stack::EffectCardKind::Source) - /// and [`Output`](crate::effect_stack::EffectCardKind::Output) never - /// invoke the renderer — they have no parameter area. - pub fn params_renderer( - mut self, - renderer: impl Fn(&EffectId, &mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - self.params_renderer = Some(Rc::new(renderer)); - self - } + /// Registers the app callback that renders an effect's parameter + /// controls inside its expanded card. See [`ParamsRenderer`]. + /// + /// Builder style; call once at setup: + /// + /// ```ignore + /// let stack = cx.new(|cx| { + /// EffectStackView::new(data, cx).params_renderer(|id, window, cx| { + /// my_effect_params_view(*id).into() + /// }) + /// }); + /// ``` + /// + /// Cards of [`EffectCardKind::Source`](crate::effect_stack::EffectCardKind::Source) + /// and [`Output`](crate::effect_stack::EffectCardKind::Output) never + /// invoke the renderer — they have no parameter area. + pub fn params_renderer( + mut self, + renderer: impl Fn(&EffectId, &mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + self.params_renderer = Some(Rc::new(renderer)); + self + } - /// The data-source entity this view reads from. - pub fn data(&self) -> &Entity { - &self.data - } + /// The data-source entity this view reads from. + pub fn data(&self) -> &Entity { + &self.data + } - /// Current transient drag state (visual only). - pub fn drag_state(&self) -> DragState { - self.drag_state - } + /// Current transient drag state (visual only). + pub fn drag_state(&self) -> DragState { + self.drag_state + } - /// Helper for the app's parameter UIs: reports that a parameter of - /// `effect` changed, causing the view to refresh card metadata and to - /// emit [`EffectStackEvent::ParameterChanged`]. - /// - /// Call this from within the app's parameter view after applying a - /// parameter edit to the engine. This is a notification of an edit the - /// app already performed — unlike the other events, it does not require - /// a follow-up model change. - pub fn notify_parameter_changed(&mut self, effect: EffectId, cx: &mut Context) { - cx.emit(EffectStackEvent::ParameterChanged { effect }); - cx.notify(); - } + /// Helper for the app's parameter UIs: reports that a parameter of + /// `effect` changed, causing the view to refresh card metadata and to + /// emit [`EffectStackEvent::ParameterChanged`]. + /// + /// Call this from within the app's parameter view after applying a + /// parameter edit to the engine. This is a notification of an edit the + /// app already performed — unlike the other events, it does not require + /// a follow-up model change. + pub fn notify_parameter_changed(&mut self, effect: EffectId, cx: &mut Context) { + cx.emit(EffectStackEvent::ParameterChanged { effect }); + cx.notify(); + } - /// Updates the transient drag state while a reorder drag moves over the - /// card `card_id`. - /// - /// The root-level drag-move listener runs first (capture phase, - /// registration order) and clears the insertion index, so a card's - /// listener only needs to set it while the pointer is inside that card's - /// own bounds — a pointer is inside at most one card, so the indicator - /// tracks exactly the card under the pointer. When the pointer is - /// elsewhere, this returns without touching the (already cleared) state. - fn update_drag( - &mut self, - card_id: EffectId, - event: &DragMoveEvent, - cx: &mut Context, - ) { - let effects = self.data.read(cx).effects(); - let dragged = *event.drag(cx); - self.drag_state.dragged = Some(dragged); - if !event.bounds.contains(&event.event.position) { - return; - } - let Some(i0) = effects.iter().position(|e| e.id() == dragged) else { - self.drag_state.insertion_index = None; - cx.notify(); - return; - }; - let Some(j) = effects.iter().position(|e| e.id() == card_id) else { - self.drag_state.insertion_index = None; - cx.notify(); - return; - }; - // The pointer is inside this card, so the indicator sits on the - // nearer of the two edges of the card; the index is expressed in - // the post-removal card list. - let insert_before = event.event.position.y < event.bounds.center().y; - let pos_in_removed = j - usize::from(j > i0); - let new_index = if insert_before { - pos_in_removed - } else { - pos_in_removed + 1 - }; - self.drag_state.insertion_index = if self.data.read(cx).can_reorder(dragged, new_index) { - Some(new_index) - } else { - None - }; - cx.notify(); - } + /// Updates the transient drag state while a reorder drag moves over the + /// card `card_id`. + /// + /// The root-level drag-move listener runs first (capture phase, + /// registration order) and clears the insertion index, so a card's + /// listener only needs to set it while the pointer is inside that card's + /// own bounds — a pointer is inside at most one card, so the indicator + /// tracks exactly the card under the pointer. When the pointer is + /// elsewhere, this returns without touching the (already cleared) state. + fn update_drag( + &mut self, + card_id: EffectId, + event: &DragMoveEvent, + cx: &mut Context, + ) { + let effects = self.data.read(cx).effects(); + let dragged = *event.drag(cx); + self.drag_state.dragged = Some(dragged); + if !event.bounds.contains(&event.event.position) { + return; + } + let Some(i0) = effects.iter().position(|e| e.id() == dragged) else { + self.drag_state.insertion_index = None; + cx.notify(); + return; + }; + let Some(j) = effects.iter().position(|e| e.id() == card_id) else { + self.drag_state.insertion_index = None; + cx.notify(); + return; + }; + // The pointer is inside this card, so the indicator sits on the + // nearer of the two edges of the card; the index is expressed in + // the post-removal card list. + let insert_before = event.event.position.y < event.bounds.center().y; + let pos_in_removed = j - usize::from(j > i0); + let new_index = if insert_before { + pos_in_removed + } else { + pos_in_removed + 1 + }; + self.drag_state.insertion_index = if self.data.read(cx).can_reorder(dragged, new_index) { + Some(new_index) + } else { + None + }; + cx.notify(); + } - /// Clears the transient drag state (mouse released outside the stack, or - /// any gesture that should abort an in-progress drag). - fn cancel_drag(&mut self, cx: &mut Context) { - self.drag_state = DragState::default(); - cx.notify(); - } + /// Clears the transient drag state (mouse released outside the stack, or + /// any gesture that should abort an in-progress drag). + fn cancel_drag(&mut self, cx: &mut Context) { + self.drag_state = DragState::default(); + cx.notify(); + } - /// Toggles a card's expansion by emitting - /// [`EffectStackEvent::ExpansionToggled`]. - fn toggle_expanded(&mut self, id: EffectId, cx: &mut Context) { - self.drag_state = DragState::default(); - let expanded = self - .data - .read(cx) - .effects() - .iter() - .find(|e| e.id() == id) - .map(|e| !e.is_expanded()) - .unwrap_or(false); - cx.emit(EffectStackEvent::ExpansionToggled { effect: id, expanded }); - cx.notify(); - } + /// Toggles a card's expansion by emitting + /// [`EffectStackEvent::ExpansionToggled`]. + fn toggle_expanded(&mut self, id: EffectId, cx: &mut Context) { + self.drag_state = DragState::default(); + let expanded = self + .data + .read(cx) + .effects() + .iter() + .find(|e| e.id() == id) + .map(|e| !e.is_expanded()) + .unwrap_or(false); + cx.emit(EffectStackEvent::ExpansionToggled { + effect: id, + expanded, + }); + cx.notify(); + } - /// Toggles a card's enabled state by emitting - /// [`EffectStackEvent::EnableToggled`]. - fn toggle_enabled(&mut self, id: EffectId, cx: &mut Context) { - self.drag_state = DragState::default(); - let enabled = self - .data - .read(cx) - .effects() - .iter() - .find(|e| e.id() == id) - .map(|e| !e.is_enabled()) - .unwrap_or(false); - cx.emit(EffectStackEvent::EnableToggled { effect: id, enabled }); - cx.notify(); - } + /// Toggles a card's enabled state by emitting + /// [`EffectStackEvent::EnableToggled`]. + fn toggle_enabled(&mut self, id: EffectId, cx: &mut Context) { + self.drag_state = DragState::default(); + let enabled = self + .data + .read(cx) + .effects() + .iter() + .find(|e| e.id() == id) + .map(|e| !e.is_enabled()) + .unwrap_or(false); + cx.emit(EffectStackEvent::EnableToggled { + effect: id, + enabled, + }); + cx.notify(); + } - /// Requests removal of a card by emitting - /// [`EffectStackEvent::RemoveRequested`]. - fn remove(&mut self, id: EffectId, cx: &mut Context) { - self.drag_state = DragState::default(); - cx.emit(EffectStackEvent::RemoveRequested(id)); - cx.notify(); - } + /// Requests removal of a card by emitting + /// [`EffectStackEvent::RemoveRequested`]. + fn remove(&mut self, id: EffectId, cx: &mut Context) { + self.drag_state = DragState::default(); + cx.emit(EffectStackEvent::RemoveRequested(id)); + cx.notify(); + } - /// Reports a secondary click on a card by emitting - /// [`EffectStackEvent::ContextMenuRequested`]. - fn context_menu(&mut self, id: EffectId, position: Point, cx: &mut Context) { - self.drag_state = DragState::default(); - cx.emit(EffectStackEvent::ContextMenuRequested { effect: id, position }); - cx.notify(); - } + /// Reports a secondary click on a card by emitting + /// [`EffectStackEvent::ContextMenuRequested`]. + fn context_menu(&mut self, id: EffectId, position: Point, cx: &mut Context) { + self.drag_state = DragState::default(); + cx.emit(EffectStackEvent::ContextMenuRequested { + effect: id, + position, + }); + cx.notify(); + } - /// Requests insertion of a new effect at the end of the stack by - /// emitting [`EffectStackEvent::AddRequested`]. - fn add(&mut self, cx: &mut Context) { - let index = self.data.read(cx).effects().len(); - cx.emit(EffectStackEvent::AddRequested { index }); - cx.notify(); - } + /// Requests insertion of a new effect at the end of the stack by + /// emitting [`EffectStackEvent::AddRequested`]. + fn add(&mut self, cx: &mut Context) { + let index = self.data.read(cx).effects().len(); + cx.emit(EffectStackEvent::AddRequested { index }); + cx.notify(); + } } impl EventEmitter for EffectStackView {} impl Focusable for EffectStackView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for EffectStackView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let focus_handle = self.focus_handle.clone(); - let params_renderer = self.params_renderer.clone(); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let focus_handle = self.focus_handle.clone(); + let params_renderer = self.params_renderer.clone(); - // A drag that ended without a drop (released outside a drop target, or - // cancelled) leaves transient drag state behind; clear it on the next - // render so no card stays half-transparent and no indicator lingers. - if self.drag_state.dragged.is_some() && !cx.has_active_drag() { - self.drag_state = DragState::default(); - } + // A drag that ended without a drop (released outside a drop target, or + // cancelled) leaves transient drag state behind; clear it on the next + // render so no card stays half-transparent and no indicator lingers. + if self.drag_state.dragged.is_some() && !cx.has_active_drag() { + self.drag_state = DragState::default(); + } - let (label, effects) = { - let data = self.data.read(cx); - (data.target_label(), data.effects()) - }; - let insertion_index = self.drag_state.insertion_index; - let dragged_id = self.drag_state.dragged; - let i0 = dragged_id.and_then(|d| effects.iter().position(|e| e.id() == d)); - let i0_guard = i0.unwrap_or(usize::MAX); + let (label, effects) = { + let data = self.data.read(cx); + (data.target_label(), data.effects()) + }; + let insertion_index = self.drag_state.insertion_index; + let dragged_id = self.drag_state.dragged; + let i0 = dragged_id.and_then(|d| effects.iter().position(|e| e.id() == d)); + let i0_guard = i0.unwrap_or(usize::MAX); - // Constructs the ghost view shown under the pointer while a card is - // being dragged. - let ghost_ctor = { - let data = self.data.clone(); - move |_id: &EffectId, _origin: Point, _window: &mut Window, cx: &mut App| { - let title = data - .read(cx) - .effects() - .iter() - .find(|e| e.id() == *_id) - .map(|e| e.title()) - .unwrap_or_else(|| SharedString::from("Effect")); - cx.new(|_cx| DragGhost { title }) - } - }; + // Constructs the ghost view shown under the pointer while a card is + // being dragged. + let ghost_ctor = { + let data = self.data.clone(); + move |_id: &EffectId, _origin: Point, _window: &mut Window, cx: &mut App| { + let title = data + .read(cx) + .effects() + .iter() + .find(|e| e.id() == *_id) + .map(|e| e.title()) + .unwrap_or_else(|| SharedString::from("Effect")); + cx.new(|_cx| DragGhost { title }) + } + }; - let mut root = div() - .id("effect-stack") - .flex() - .flex_col() - .w_full() - .h_full() - .track_focus(&focus_handle) - .on_drag_move::(cx.listener(|this, _event, _window, cx| { - // Runs first (capture phase, registration order): clear the - // indicator by default; the per-card listener re-sets it - // while the pointer is inside that card. - this.drag_state.insertion_index = None; - cx.notify(); - })) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event, _window, cx| this.cancel_drag(cx)), - ); + let mut root = div() + .id("effect-stack") + .flex() + .flex_col() + .w_full() + .h_full() + .track_focus(&focus_handle) + .on_drag_move::(cx.listener(|this, _event, _window, cx| { + // Runs first (capture phase, registration order): clear the + // indicator by default; the per-card listener re-sets it + // while the pointer is inside that card. + this.drag_state.insertion_index = None; + cx.notify(); + })) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| this.cancel_drag(cx)), + ); - let Some(label) = label else { - return root.child( - div() - .id("empty-state") - .flex() - .flex_1() - .items_center() - .justify_center() - .text_sm() - .text_color(colors.disabled) - .child(crate::i18n::tr("effect_stack.empty", "No selection")), - ); - }; + let Some(label) = label else { + return root.child( + div() + .id("empty-state") + .flex() + .flex_1() + .items_center() + .justify_center() + .text_sm() + .text_color(colors.disabled) + .child(crate::i18n::tr("effect_stack.empty", "No selection")), + ); + }; - root = root.child( - div() - .id("effect-stack-header") - .px_3() - .py_2() - .text_sm() - .text_color(colors.text) - .child(label), - ); + root = root.child( + div() + .id("effect-stack-header") + .px_3() + .py_2() + .text_sm() + .text_color(colors.text) + .child(label), + ); - let mut column = div().id("effect-stack-cards").flex().flex_col().w_full(); + let mut column = div().id("effect-stack-cards").flex().flex_col().w_full(); - for (index, effect) in effects.iter().enumerate() { - let id = effect.id(); - let fixed = effect.kind() != EffectCardKind::Effect; - let enabled = effect.is_enabled(); - let expanded = effect.is_expanded(); - let removable = effect.is_removable(); - let reorderable = effect.is_reorderable(); + for (index, effect) in effects.iter().enumerate() { + let id = effect.id(); + let fixed = effect.kind() != EffectCardKind::Effect; + let enabled = effect.is_enabled(); + let expanded = effect.is_expanded(); + let removable = effect.is_removable(); + let reorderable = effect.is_reorderable(); - // Insertion indicator for a drop position just before this card. - if let Some(p) = insertion_index { - let k = p + usize::from(p >= i0_guard); - if k == index { - column = column.child(InsertIndicator::valid()); - } - } + // Insertion indicator for a drop position just before this card. + if let Some(p) = insertion_index { + let k = p + usize::from(p >= i0_guard); + if k == index { + column = column.child(InsertIndicator::valid()); + } + } - // Header row: drag handle, enable toggle, the card itself - // (flexing to fill), and the remove button. - let mut header_row = div() - .id(ElementId::named_usize("effect-header", id.0 as usize)) - .flex() - .flex_row() - .items_center() - .gap_1() - .px_2() - .py_1() - .on_aux_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { - cx.stop_propagation(); - this.context_menu(id, event.position(), cx); - })); + // Header row: drag handle, enable toggle, the card itself + // (flexing to fill), and the remove button. + let mut header_row = div() + .id(ElementId::named_usize("effect-header", id.0 as usize)) + .flex() + .flex_row() + .items_center() + .gap_1() + .px_2() + .py_1() + .on_aux_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + cx.stop_propagation(); + this.context_menu(id, event.position(), cx); + })); - if !fixed { - header_row = header_row.cursor_pointer().on_click( - cx.listener(move |this, _event, _window, cx| { - cx.stop_propagation(); - this.toggle_expanded(id, cx); - }), - ); - } + if !fixed { + header_row = header_row.cursor_pointer().on_click(cx.listener( + move |this, _event, _window, cx| { + cx.stop_propagation(); + this.toggle_expanded(id, cx); + }, + )); + } - if reorderable { - header_row = header_row.child( - div() - .id(ElementId::named_usize("effect-handle", id.0 as usize)) - .cursor_grab() - .text_color(colors.disabled) - .child("⠿") - .on_drag(id, ghost_ctor.clone()), - ); - } + if reorderable { + header_row = header_row.child( + div() + .id(ElementId::named_usize("effect-handle", id.0 as usize)) + .cursor_grab() + .text_color(colors.disabled) + .child("⠿") + .on_drag(id, ghost_ctor.clone()), + ); + } - if !fixed { - header_row = header_row.child( - div() - .id(ElementId::named_usize("effect-toggle", id.0 as usize)) - .cursor_pointer() - .text_color(if enabled { colors.text } else { colors.disabled }) - .child("⏻") - .on_click(cx.listener(move |this, _event, _window, cx| { - cx.stop_propagation(); - this.toggle_enabled(id, cx); - })), - ); - } + if !fixed { + header_row = header_row.child( + div() + .id(ElementId::named_usize("effect-toggle", id.0 as usize)) + .cursor_pointer() + .text_color(if enabled { + colors.text + } else { + colors.disabled + }) + .child("⏻") + .on_click(cx.listener(move |this, _event, _window, cx| { + cx.stop_propagation(); + this.toggle_enabled(id, cx); + })), + ); + } - let card = EffectCard::new(id) - .kind(effect.kind()) - .title(effect.title()) - .subtitle(effect.subtitle()) - .enabled(enabled) - .expanded(expanded) - .removable(removable) - .reorderable(reorderable) - .badge_count(effect.badge_count()) - .drag_ghost(dragged_id == Some(id)); - header_row = header_row.child(card); + let card = EffectCard::new(id) + .kind(effect.kind()) + .title(effect.title()) + .subtitle(effect.subtitle()) + .enabled(enabled) + .expanded(expanded) + .removable(removable) + .reorderable(reorderable) + .badge_count(effect.badge_count()) + .drag_ghost(dragged_id == Some(id)); + header_row = header_row.child(card); - if removable { - header_row = header_row.child( - div() - .id(ElementId::named_usize("effect-remove", id.0 as usize)) - .cursor_pointer() - .text_color(colors.disabled) - .child("✕") - .on_click(cx.listener(move |this, _event, _window, cx| { - cx.stop_propagation(); - this.remove(id, cx); - })), - ); - } + if removable { + header_row = header_row.child( + div() + .id(ElementId::named_usize("effect-remove", id.0 as usize)) + .cursor_pointer() + .text_color(colors.disabled) + .child("✕") + .on_click(cx.listener(move |this, _event, _window, cx| { + cx.stop_propagation(); + this.remove(id, cx); + })), + ); + } - let mut wrapper = div() - .id(ElementId::named_usize("effect", id.0 as usize)) - .flex() - .flex_col() - .w_full() - .rounded_md() - .border_1() - .border_color(if fixed { colors.border } else { colors.separator }) - .bg(if fixed { colors.container } else { colors.background }) - .overflow_hidden(); - wrapper = wrapper.child(header_row); + let mut wrapper = div() + .id(ElementId::named_usize("effect", id.0 as usize)) + .flex() + .flex_col() + .w_full() + .rounded_md() + .border_1() + .border_color(if fixed { + colors.border + } else { + colors.separator + }) + .bg(if fixed { + colors.container + } else { + colors.background + }) + .overflow_hidden(); + wrapper = wrapper.child(header_row); - if expanded && !fixed { - if let Some(renderer) = ¶ms_renderer { - wrapper = wrapper.child( - div() - .id(ElementId::named_usize("effect-params", id.0 as usize)) - .border_t_1() - .border_color(colors.separator) - .child(renderer(&id, window, cx)), - ); - } - } + if expanded && !fixed { + if let Some(renderer) = ¶ms_renderer { + wrapper = wrapper.child( + div() + .id(ElementId::named_usize("effect-params", id.0 as usize)) + .border_t_1() + .border_color(colors.separator) + .child(renderer(&id, window, cx)), + ); + } + } - if reorderable { - wrapper = wrapper - .on_drag_move::(cx.listener(move |this, event, _window, cx| { - this.update_drag(id, event, cx); - })) - .on_drop::( - cx.listener(move |this, &dragged: &EffectId, _window, cx| { - let index = this.drag_state.insertion_index; - this.drag_state = DragState::default(); - if let Some(index) = index { - cx.emit(EffectStackEvent::ReorderRequested { - effect: dragged, - new_index: index, - }); - } - cx.notify(); - }), - ) - .can_drop(|payload, _window, _cx| payload.is::()); - } + if reorderable { + wrapper = wrapper + .on_drag_move::(cx.listener(move |this, event, _window, cx| { + this.update_drag(id, event, cx); + })) + .on_drop::(cx.listener( + move |this, &dragged: &EffectId, _window, cx| { + let index = this.drag_state.insertion_index; + this.drag_state = DragState::default(); + if let Some(index) = index { + cx.emit(EffectStackEvent::ReorderRequested { + effect: dragged, + new_index: index, + }); + } + cx.notify(); + }, + )) + .can_drop(|payload, _window, _cx| payload.is::()); + } - column = column.child(wrapper); - } + column = column.child(wrapper); + } - // Insertion indicator for a drop position after the last card. - if let Some(p) = insertion_index { - let k = p + usize::from(p >= i0_guard); - if k == effects.len() { - column = column.child(InsertIndicator::valid()); - } - } + // Insertion indicator for a drop position after the last card. + if let Some(p) = insertion_index { + let k = p + usize::from(p >= i0_guard); + if k == effects.len() { + column = column.child(InsertIndicator::valid()); + } + } - let add_button = div() - .id("effect-add") - .cursor_pointer() - .px_3() - .py_2() - .text_sm() - .text_color(colors.text) - .child(crate::i18n::tr("effect_stack.add", "+ Add Effect")) - .on_click(cx.listener(move |this, _event, _window, cx| this.add(cx))); + let add_button = div() + .id("effect-add") + .cursor_pointer() + .px_3() + .py_2() + .text_sm() + .text_color(colors.text) + .child(crate::i18n::tr("effect_stack.add", "+ Add Effect")) + .on_click(cx.listener(move |this, _event, _window, cx| this.add(cx))); - root.child(column).child(add_button) - } + root.child(column).child(add_button) + } } /// The floating view shown under the pointer while a card is being dragged. struct DragGhost { - title: SharedString, + title: SharedString, } impl Render for DragGhost { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - div() - .px_2() - .py_1() - .rounded_md() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .shadow_md() - .child(self.title.clone()) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .px_2() + .py_1() + .rounded_md() + .bg(colors.background) + .border_1() + .border_color(colors.border) + .shadow_md() + .child(self.title.clone()) + } } diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index e05d6e97f4..b213a08a58 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -32,16 +32,16 @@ //! your own custom layout algorithm or rendering a code editor. use crate::{ - App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, FocusHandle, - InspectorElementId, LayoutId, Pixels, Point, SharedString, Size, Style, Window, - util::FluentBuilder, window::with_element_arena, + App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, FocusHandle, + InspectorElementId, LayoutId, Pixels, Point, SharedString, Size, Style, Window, + util::FluentBuilder, window::with_element_arena, }; use derive_more::{Deref, DerefMut}; use std::{ - any::{Any, type_name}, - fmt::{self, Debug, Display}, - mem, panic, - sync::Arc, + any::{Any, type_name}, + fmt::{self, Debug, Display}, + mem, panic, + sync::Arc, }; /// Implemented by types that participate in laying out and painting the contents of a window. @@ -49,95 +49,95 @@ use std::{ /// 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::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; + /// 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; + /// 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>>; + /// 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); + /// 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; + /// 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, - ); + /// 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 - } + /// 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) {} + /// 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) {} - /// Convert this element into a dynamically-typed [`AnyElement`]. - fn into_any(self) -> AnyElement { - AnyElement::new(self) - } + /// 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; + /// 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 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() - } + /// Convert self into a dynamically-typed [`AnyElement`]. + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } } impl FluentBuilder for T {} @@ -145,14 +145,14 @@ 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; + /// 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 - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } } /// You can derive [`IntoElement`] on any type that implements this trait. @@ -161,145 +161,145 @@ impl Render for Empty { /// 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; + /// 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); + /// 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 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 - } + /// 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 + } } /// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro /// for [`RenderOnce`] #[doc(hidden)] pub struct Component { - component: Option, - #[cfg(debug_assertions)] - source: &'static core::panic::Location<'static>, + component: Option, + #[cfg(debug_assertions)] + source: &'static core::panic::Location<'static>, } impl Component { - /// Create a new component from the given RenderOnce type. - #[track_caller] - pub fn new(component: C) -> Self { - Component { - component: Some(component), - #[cfg(debug_assertions)] - source: core::panic::Location::caller(), - } - } + /// Create a new component from the given RenderOnce type. + #[track_caller] + pub fn new(component: C) -> Self { + Component { + component: Some(component), + #[cfg(debug_assertions)] + source: core::panic::Location::caller(), + } + } } fn prepaint_component( - (element, name): &mut (AnyElement, &'static str), - window: &mut Window, - cx: &mut App, + (element, name): &mut (AnyElement, &'static str), + window: &mut Window, + cx: &mut App, ) { - window.with_id(ElementId::Name(SharedString::new_static(name)), |window| { - element.prepaint(window, cx); - }) + window.with_id(ElementId::Name(SharedString::new_static(name)), |window| { + element.prepaint(window, cx); + }) } fn paint_component( - (element, name): &mut (AnyElement, &'static str), - window: &mut Window, - cx: &mut App, + (element, name): &mut (AnyElement, &'static str), + window: &mut Window, + cx: &mut App, ) { - window.with_id(ElementId::Name(SharedString::new_static(name)), |window| { - element.paint(window, cx); - }) + window.with_id(ElementId::Name(SharedString::new_static(name)), |window| { + element.paint(window, cx); + }) } impl Element for Component { - type RequestLayoutState = (AnyElement, &'static str); - type PrepaintState = (); + type RequestLayoutState = (AnyElement, &'static str); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - #[cfg(debug_assertions)] - return Some(self.source); + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + #[cfg(debug_assertions)] + return Some(self.source); - #[cfg(not(debug_assertions))] - return None; - } + #[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) { - window.with_id(ElementId::Name(type_name::().into()), |window| { - let mut element = self - .component - .take() - .unwrap() - .render(window, cx) - .into_any_element(); + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + window.with_id(ElementId::Name(type_name::().into()), |window| { + let mut element = self + .component + .take() + .unwrap() + .render(window, cx) + .into_any_element(); - let layout_id = element.request_layout(window, cx); - (layout_id, (element, type_name::())) - }) - } + let layout_id = element.request_layout(window, cx); + (layout_id, (element, type_name::())) + }) + } - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _: Bounds, - state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) { - prepaint_component(state, window, cx); - } + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + prepaint_component(state, window, cx); + } - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _: Bounds, - state: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - paint_component(state, window, cx); - } + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + state: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + paint_component(state, window, cx); + } } impl IntoElement for Component { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } /// A globally unique identifier for an element, used to track state across frames. @@ -307,540 +307,540 @@ impl IntoElement for Component { 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(()) - } + 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()) - } + 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 inner_element(&mut self) -> &mut dyn Any; - fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId; + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId; - fn prepaint(&mut self, window: &mut Window, cx: &mut App); + fn prepaint(&mut self, window: &mut Window, cx: &mut App); - fn paint(&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; + 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, + /// 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, + #[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, - } - } + 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)) - }); + 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 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, - ); + 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(); - } + 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"), - } - } + 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); - } + 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); - } - } - } + 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); + } + } + } - let node_id = window.next_frame.dispatch_tree.push_node(); - let 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(); + let node_id = window.next_frame.dispatch_tree.push_node(); + let 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 { - window.a11y.nodes.pop(); - } + if pushed_a11y_node { + window.a11y.nodes.pop(); + } - if global_id.is_some() { - window.element_id_stack.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"), - } - } + 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); - } + 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, - ); + 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(); - } + if global_id.is_some() { + window.element_id_stack.pop(); + } - self.phase = ElementDrawPhase::Painted; - (request_layout, prepaint) - } - _ => panic!("must call prepaint before paint"), - } - } + 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); - } + 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"), - }; + 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 - } + window.layout_bounds(layout_id).size + } } impl ElementObject for Drawable where - E: Element, - E::RequestLayoutState: 'static, + E: Element, + E::RequestLayoutState: 'static, { - fn inner_element(&mut self) -> &mut dyn Any { - &mut self.element - } + 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 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 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 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) - } + #[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) - } + 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::() - } + /// 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) - } + /// 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(); + /// 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); + 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); - } + if !focus_assigned && let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); + } - None - } + None + } - /// Paints the element stored in this `AnyElement`. - pub fn paint(&mut self, window: &mut Window, cx: &mut App) { - self.0.paint(window, cx); - } + /// 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) - } + /// 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)) - } + /// 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)) - } + /// 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 = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - 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 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 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); - } + 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; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } - fn into_any_element(self) -> AnyElement { - self - } + fn into_any_element(self) -> AnyElement { + self + } } /// The empty element, which renders nothing. pub struct Empty; impl IntoElement for Empty { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Element for Empty { - type RequestLayoutState = (); - type PrepaintState = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - 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 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 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, - ) { - } + 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/src/elements/anchored.rs b/crates/gpui/src/elements/anchored.rs index 29260061bb..ee78367766 100644 --- a/crates/gpui/src/elements/anchored.rs +++ b/crates/gpui/src/elements/anchored.rs @@ -1,398 +1,398 @@ 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, + 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]>, + 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>, + 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, - } + 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 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 - } + /// 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 - } + /// 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 - } + /// 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 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 - } + /// 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) - } + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } } impl Element for Anchored { - type RequestLayoutState = AnchoredState; - type PrepaintState = (); + type RequestLayoutState = AnchoredState; + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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::>(); + 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 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); + let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx); - (layout_id, AnchoredState { child_layout_ids }) - } + (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; - } + 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 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 (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(), - }; + let limits = Bounds { + origin: Point::default(), + size: window.viewport_size(), + }; - if self.fit_mode == AnchoredFitMode::SwitchAnchor { - let mut anchor = self.anchor; + 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.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; - } - } - } + 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); + 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 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; - } + // 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()); + 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); - } - }) - } + 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); - } - } + 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; + type Element = Self; - fn into_element(self) -> Self::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, + /// 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, + /// 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(); + 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) - } - } - } + 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, - }; + use crate::{ + Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point, + prelude::*, px, size, + }; - struct AnchoredTestView { - position: Point, - } + 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), - ), - ) - } - } + 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.)), - }); + #[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(); + 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"); + 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.))); - } + 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.)), - }); + #[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(); + 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(); + 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(); + 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"); + 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.))); - } + 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.)), - }); + #[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(); + 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"); + 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.))); - } + 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/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 8a42c8bd49..24f0166cfd 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -2,7 +2,7 @@ use scheduler::Instant; use std::{rc::Rc, time::Duration}; use crate::{ - AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window, + AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window, }; pub use easing::*; @@ -11,251 +11,251 @@ 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, - /// A function that takes a delta between 0 and 1 and returns a new delta - /// between 0 and 1 based on the given easing function. - pub easing: Rc f32>, + /// The amount of time for which this animation should run + pub duration: Duration, + /// Whether to repeat this animation when it finishes + pub oneshot: bool, + /// A function that takes a delta between 0 and 1 and returns a new delta + /// between 0 and 1 based on the given easing function. + pub easing: Rc f32>, } 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, - easing: Rc::new(linear), - } - } + /// 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, + easing: Rc::new(linear), + } + } - /// 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. + pub fn repeat(mut self) -> Self { + self.oneshot = false; + self + } - /// Set the easing function to use for this animation. - /// The easing function will take a time delta between 0 and 1 and return a new delta - /// between 0 and 1 - pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { - self.easing = Rc::new(easing); - self - } + /// Set the easing function to use for this animation. + /// The easing function will take a time delta between 0 and 1 and return a new delta + /// between 0 and 1 + pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { + self.easing = Rc::new(easing); + self + } } /// An extension trait for adding the animation wrapper to both Elements and Components 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 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(), - } - } + /// 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(), + } + } } 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>, + id: ElementId, + element: Option, + animations: SmallVec<[Animation; 1]>, + animator: Box E + 'static>, } 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 - } + /// 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; + type Element = AnimationElement; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } struct AnimationState { - start: Instant, - animation_ix: usize, + start: Instant, + animation_ix: usize, } impl Element for AnimationElement { - type RequestLayoutState = AnyElement; - type PrepaintState = (); + type RequestLayoutState = AnyElement; + type PrepaintState = (); - fn id(&self) -> Option { - Some(self.id.clone()) - } + fn id(&self) -> Option { + Some(self.id.clone()) + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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, - ) -> (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, - }); - let animation_ix = state.animation_ix; + 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, + }); + let animation_ix = state.animation_ix; - let mut delta = state.start.elapsed().as_secs_f32() - / self.animations[animation_ix].duration.as_secs_f32(); + let mut delta = state.start.elapsed().as_secs_f32() + / self.animations[animation_ix].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; - } - } - let delta = (self.animations[animation_ix].easing)(delta); + 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; + } + } + let delta = (self.animations[animation_ix].easing)(delta); - debug_assert!( - (0.0..=1.0).contains(&delta), - "delta should always be between 0 and 1" - ); + debug_assert!( + (0.0..=1.0).contains(&delta), + "delta should always be between 0 and 1" + ); - let element = self.element.take().expect("should only be called once"); - let mut element = (self.animator)(element, animation_ix, delta).into_any_element(); + 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 { - window.request_animation_frame(); - } + if !done { + window.request_animation_frame(); + } - ((element.request_layout(window, cx), element), state) - }) - } + ((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 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); - } + 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; + use std::f32::consts::PI; - /// The linear easing function, or delta itself - pub fn linear(delta: f32) -> f32 { - delta - } + /// 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 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 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) - } + /// 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) - } - } - } + /// 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; + /// 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; + 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; + // Map the breath to our desired alpha range + let normalized_alpha = (breath + 1.0) / 2.0; - min + (normalized_alpha * range) - } - } + min + (normalized_alpha * range) + } + } } diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index d57d2f6041..81fda97f4a 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -1,95 +1,95 @@ use refineable::Refineable as _; use crate::{ - App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels, - Style, StyleRefinement, Styled, Window, + 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), + 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(), - } + 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, + prepaint: Option, &mut Window, &mut App) -> T>>, + paint: Option, T, &mut Window, &mut App)>>, + style: StyleRefinement, } impl IntoElement for Canvas { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Element for Canvas { - type RequestLayoutState = Style; - type PrepaintState = Option; + type RequestLayoutState = Style; + type PrepaintState = Option; - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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) - }); - } + 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 - } + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } } diff --git a/crates/gpui/src/elements/deferred.rs b/crates/gpui/src/elements/deferred.rs index 25245fa4b6..cd76978066 100644 --- a/crates/gpui/src/elements/deferred.rs +++ b/crates/gpui/src/elements/deferred.rs @@ -1,96 +1,96 @@ use crate::{ - AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId, - Pixels, Window, + 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, - } + 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, + 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 - } + /// 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 = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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, - ) { - } + 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; + type Element = Self; - fn into_element(self) -> Self::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 - } + /// 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 + } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index a0970015b5..7fd672df49 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -17,14 +17,14 @@ use crate::PinchEvent; use crate::{ - Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, - Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, - HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, - KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, - MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, Overflow, - ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, - StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, - size, + Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, + Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, + HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, + KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, + MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, Overflow, + ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, + size, }; use collections::HashMap; use gpui_util::ResultExt; @@ -32,15 +32,15 @@ use refineable::Refineable; use smallvec::SmallVec; use stacksafe::{StackSafe, stacksafe}; use std::{ - any::{Any, TypeId}, - cell::RefCell, - cmp::Ordering, - fmt::Debug, - marker::PhantomData, - mem, - rc::Rc, - sync::Arc, - time::Duration, + any::{Any, TypeId}, + cell::RefCell, + cmp::Ordering, + fmt::Debug, + marker::PhantomData, + mem, + rc::Rc, + sync::Arc, + time::Duration, }; use super::ImageCacheProvider; @@ -51,1490 +51,1490 @@ 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 identifier for this group. + pub group: SharedString, - /// The specific style refinement that this group would apply - /// to its children. - pub style: Box, + /// 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 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, + /// 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.") - } + /// 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() - } + /// 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(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() - } + /// 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 - } + /// 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 - } - } + #[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 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 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 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 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 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 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 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 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 && !hitbox.contains(&window.mouse_position()) { - (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 && !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 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 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 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 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 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 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 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 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. - 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 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. + 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 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 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 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 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 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 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); - }), - )); - } + /// 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)); - } + /// 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 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) - })); - } + /// 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(( - Arc::new(value), - Box::new(move |value, offset, window, cx| { - constructor(value.downcast_ref().unwrap(), offset, window, cx).into() - }), - )); - } + /// 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(( + Arc::new(value), + Box::new(move |value, offset, window, cx| { + constructor(value.downcast_ref().unwrap(), offset, window, cx).into() + }), + )); + } - /// 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. - /// 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)); - } + /// 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. + /// 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)); + } - /// 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 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, - }); - } + /// 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, + }); + } - /// 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; - } + /// 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); - } + /// 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; - } + /// 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() - } + 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; + /// 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 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()); + /// 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 } - } + 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 - } + /// 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 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 - } + /// 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 - } + /// 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 - } + /// 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 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 - } + /// 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 - } + /// 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(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 - } + #[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::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 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 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 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 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 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 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 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 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 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 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 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 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 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. - 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 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. + 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 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 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 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 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 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 - } + /// 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 + 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 - } + /// 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 - } + /// 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 - } + /// 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 - } + /// 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 - } + /// 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 - } + /// 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, 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 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 - } + /// 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 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 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 label for this element. + fn aria_label(mut self, label: impl Into) -> Self { + self.interactivity().aria_label = Some(label.into()); + 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 selected state for this element. + fn aria_selected(mut self, selected: bool) -> Self { + self.interactivity().aria_selected = Some(selected); + 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 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 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 numeric value for this element. + fn aria_numeric_value(mut self, value: f64) -> Self { + self.interactivity().aria_numeric_value = Some(value); + 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 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 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 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 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 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 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 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 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 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 - } + /// 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 - } + /// 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 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 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 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 - } + /// Set the overflow y to scroll. + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + 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 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 - } + /// 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 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 - } + /// 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 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 - } + /// 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 - } + /// 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 + } - /// 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. - /// 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 - } + /// 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. + /// 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 + } - /// 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 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 - } + /// 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 + } } pub(crate) type MouseDownListener = - Box; + Box; pub(crate) type MouseUpListener = - Box; + Box; pub(crate) type MousePressureListener = - Box; + Box; pub(crate) type MouseMoveListener = - Box; + Box; pub(crate) type ScrollWheelListener = - Box; + Box; pub(crate) type PinchListener = - Box; + Box; pub(crate) type ClickListener = Rc; pub(crate) type DragListener = - Box, &mut Window, &mut App) -> AnyView + 'static>; + Box, &mut Window, &mut App) -> AnyView + 'static>; type DropListener = Box; type CanDropPredicate = Box bool + 'static>; pub(crate) struct TooltipBuilder { - build: Rc AnyView + 'static>, - hoverable: bool, + build: Rc AnyView + 'static>, + hoverable: bool, } pub(crate) type KeyDownListener = - Box; + Box; pub(crate) type KeyUpListener = - Box; + Box; pub(crate) type ModifiersChangedListener = - Box; + Box; pub(crate) type ActionListener = - Box; + 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, - } + 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]>>>, + 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 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 - } + /// 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 - } + /// 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. @@ -1544,1720 +1544,1719 @@ impl Div { /// 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]>, + 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, + /// 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 - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } } impl InteractiveElement for Div { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } } impl ParentElement for Div { - fn extend(&mut self, elements: impl IntoIterator) { - self.children - .extend(elements.into_iter().map(StackSafe::new)) - } + fn extend(&mut self, elements: impl IntoIterator) { + self.children + .extend(elements.into_iter().map(StackSafe::new)) + } } impl Element for Div { - type RequestLayoutState = DivFrameState; - type PrepaintState = Option; + type RequestLayoutState = DivFrameState; + type PrepaintState = Option; - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - self.interactivity.source_location() - } + 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 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 write_a11y_info(&self, node: &mut accesskit::Node) { + self.interactivity.write_a11y_info(node); + } - #[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)); + #[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) - }) - }, - ) - }); + 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 }) - } + (layout_id, DivFrameState { child_layout_ids }) + } - #[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)); + #[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 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()); + 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 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(); - } + 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; - } + 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); - } - } - }); + 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); - } - }); + if let Some(listener) = self.prepaint_listener.as_ref() { + listener(children_bounds, window, cx); + } + }); - hitbox - }, - ) - } + hitbox + }, + ) + } - #[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)); + #[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; - } + 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); - } - }, - ) - }); - } + for child in &mut self.children { + child.paint(window, cx); + } + }, + ) + }); + } } impl IntoElement for Div { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } /// 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) 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) 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<(Arc, DragListener)>, - pub(crate) hover_listener: Option>, - pub(crate) tooltip_builder: 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, + /// 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) 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) 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<(Arc, DragListener)>, + pub(crate) hover_listener: Option>, + pub(crate) tooltip_builder: 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) override_role: Option, - pub(crate) aria_label: Option, - pub(crate) aria_selected: Option, - pub(crate) aria_expanded: Option, - pub(crate) aria_toggled: Option, - pub(crate) aria_numeric_value: Option, - pub(crate) aria_min_numeric_value: Option, - pub(crate) aria_max_numeric_value: Option, - pub(crate) aria_orientation: Option, - pub(crate) aria_level: Option, - pub(crate) aria_position_in_set: Option, - pub(crate) aria_size_of_set: Option, - pub(crate) aria_row_index: Option, - pub(crate) aria_column_index: Option, - pub(crate) aria_row_count: Option, - pub(crate) aria_column_count: Option, + pub(crate) a11y_action_listeners: + Vec<(accesskit::Action, crate::window::a11y::A11yActionListener)>, + pub(crate) override_role: Option, + pub(crate) aria_label: Option, + pub(crate) aria_selected: Option, + pub(crate) aria_expanded: Option, + pub(crate) aria_toggled: Option, + pub(crate) aria_numeric_value: Option, + pub(crate) aria_min_numeric_value: Option, + pub(crate) aria_max_numeric_value: Option, + pub(crate) aria_orientation: Option, + pub(crate) aria_level: Option, + pub(crate) aria_position_in_set: Option, + pub(crate) aria_size_of_set: Option, + pub(crate) aria_row_index: Option, + pub(crate) aria_column_index: Option, + pub(crate) aria_row_count: Option, + pub(crate) aria_column_count: Option, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) source_location: Option<&'static core::panic::Location<'static>>, + #[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, + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_selector: Option, } impl Interactivity { - /// Assigns the current scroll offset of an element. No-op if the element's - /// style does not have overflow enabled. - /// - /// Should only be called during the `request_layout` phase. - pub fn set_scroll_offset( - &self, - global_id: Option<&GlobalElementId>, - window: &mut Window, - point: Point, - ) { - 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 overflow = &self.base_style.overflow; - if (overflow.x == Some(Overflow::Scroll) || overflow.y == Some(Overflow::Scroll)) - && let Some(element_state) = element_state.as_mut() - { - let scroll_offset = element_state.scroll_offset.get_or_insert_with(Rc::default); - *scroll_offset.borrow_mut() = point; - } - ((), element_state) - }, - ); - } + /// Assigns the current scroll offset of an element. No-op if the element's + /// style does not have overflow enabled. + /// + /// Should only be called during the `request_layout` phase. + pub fn set_scroll_offset( + &self, + global_id: Option<&GlobalElementId>, + window: &mut Window, + point: Point, + ) { + 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 overflow = &self.base_style.overflow; + if (overflow.x == Some(Overflow::Scroll) || overflow.y == Some(Overflow::Scroll)) + && let Some(element_state) = element_state.as_mut() + { + let scroll_offset = element_state.scroll_offset.get_or_insert_with(Rc::default); + *scroll_offset.borrow_mut() = point; + } + ((), element_state) + }, + ); + } } 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() { - self.scroll_offset = Some(scroll_handle.0.borrow().offset.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(), - ); - } - - 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.focus_ids.insert(node_id, focus_handle.id); - if focus_handle.is_focused(window) && window.a11y.nodes.has_node(node_id) { - window.a11y.nodes.set_focus(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| { - 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.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.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(); - let padding = style.padding.to_pixels(bounds.size.into(), rem_size); - 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; - } - if let Some(focus_handle) = &self.tracked_focus_handle { - window.next_frame.tab_stops.insert(focus_handle); - } - - 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| { - window.with_tab_group(tab_group, |window| { - 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, - 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.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 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((drag_value, drag_listener)) = drag_listener.take() - && mouse_down.button == MouseButton::Left - { - *clicked_state.borrow_mut() = ElementClickedState::default(); - let cursor_offset = event.position - hitbox.origin; - let drag = - (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx); - cx.active_drag = Some(AnyDrag { - view: drag, - value: drag_value, - cursor_offset, - cursor_style: drag_cursor_style, - }); - pending_mouse_down.take(); - window.refresh(); - cx.stop_propagation(); - } - } - }); - - if is_focused { - // 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 click_event = ClickEvent::Keyboard(KeyboardClickEvent { - button, - bounds: hitbox.bounds, - }); - - for listener in &click_listeners { - listener(&click_event, window, cx); - } - } - } - } - }); - } - - 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 hitbox = hitbox.clone(); - 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(); - - window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - let is_hovered = has_mouse_down.borrow().is_none() - && !cx.has_active_drag() - && hitbox.is_hovered(window); - let mut was_hovered = was_hovered.borrow_mut(); - - if is_hovered != *was_hovered { - *was_hovered = is_hovered; - drop(was_hovered); - - hover_listener(&is_hovered, 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, - 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 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 delta = event.delta.pixel_delta(line_height); - - let mut delta_x = Pixels::ZERO; - if overflow.x == Overflow::Scroll { - if !delta.x.is_zero() { - delta_x = delta.x; - } else if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll { - delta_x = delta.y; - } - } - let mut delta_y = Pixels::ZERO; - if overflow.y == Overflow::Scroll { - if !delta.y.is_zero() { - delta_y = delta.y; - } else if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll { - delta_y = delta.x; - } - } - 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) { - group_hitbox_id.is_hovered(window) - } else if let Some(element_state) = element_state.as_ref() { - 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 { - hitbox.is_hovered(window) - } else if let Some(element_state) = element_state.as_ref() { - 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(label) = &self.aria_label { - node.set_label(label.to_string()); - } - if let Some(selected) = self.aria_selected { - node.set_selected(selected); - } - 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(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); - } - } + /// 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() { + self.scroll_offset = Some(scroll_handle.0.borrow().offset.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(), + ); + } + + 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.focus_ids.insert(node_id, focus_handle.id); + if focus_handle.is_focused(window) && window.a11y.nodes.has_node(node_id) { + window.a11y.nodes.set_focus(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| { + 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.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.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(); + let padding = style.padding.to_pixels(bounds.size.into(), rem_size); + 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; + } + if let Some(focus_handle) = &self.tracked_focus_handle { + window.next_frame.tab_stops.insert(focus_handle); + } + + 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| { + window.with_tab_group(tab_group, |window| { + 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, + 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.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 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((drag_value, drag_listener)) = drag_listener.take() + && mouse_down.button == MouseButton::Left + { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = + (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + cursor_style: drag_cursor_style, + }); + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); + } + } + }); + + if is_focused { + // 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 click_event = ClickEvent::Keyboard(KeyboardClickEvent { + button, + bounds: hitbox.bounds, + }); + + for listener in &click_listeners { + listener(&click_event, window, cx); + } + } + } + } + }); + } + + 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 hitbox = hitbox.clone(); + 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(); + + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = has_mouse_down.borrow().is_none() + && !cx.has_active_drag() + && hitbox.is_hovered(window); + let mut was_hovered = was_hovered.borrow_mut(); + + if is_hovered != *was_hovered { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(&is_hovered, 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, + 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 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 delta = event.delta.pixel_delta(line_height); + + let mut delta_x = Pixels::ZERO; + if overflow.x == Overflow::Scroll { + if !delta.x.is_zero() { + delta_x = delta.x; + } else if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll { + delta_x = delta.y; + } + } + let mut delta_y = Pixels::ZERO; + if overflow.y == Overflow::Scroll { + if !delta.y.is_zero() { + delta_y = delta.y; + } else if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll { + delta_y = delta.x; + } + } + 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) { + group_hitbox_id.is_hovered(window) + } else if let Some(element_state) = element_state.as_ref() { + 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 { + hitbox.is_hovered(window) + } else if let Some(element_state) = element_state.as_ref() { + 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(label) = &self.aria_label { + node.set_label(label.to_string()); + } + if let Some(selected) = self.aria_selected { + node.set_selected(selected); + } + 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(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>>>, - pub(crate) scroll_offset: Option>>>, - pub(crate) active_tooltip: Option>>>, + 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>>>, + pub(crate) scroll_offset: 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's group has been clicked, false otherwise + pub group: bool, - /// True if this element has been clicked, false otherwise - pub element: bool, + /// True if this element has been clicked, false otherwise + pub element: bool, } impl ElementClickedState { - fn is_clicked(&self) -> bool { - self.group || self.element - } + 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's group is hovered, false otherwise + pub group: bool, - /// True if this element is hovered, false otherwise - pub element: 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<()>, - }, + /// 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, + 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(), - } + 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, + 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(); - } + 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, + 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)) + 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>, - window: &mut Window, + active_tooltip: &Rc>>, + tooltip_id: Option, + build_tooltip: Rc Option<(AnyView, bool)>>, + check_is_hovered: Rc bool>, + check_is_hovered_during_prepaint: Rc bool>, + window: &mut Window, ) { - 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, - phase, - window, - cx, - ) - } - }); + 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, + phase, + 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 |_: &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); - } - } - }); + 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. @@ -3272,184 +3271,184 @@ pub(crate) fn register_tooltip_mouse_handlers( /// 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>, - phase: DispatchPhase, - window: &mut Window, - cx: &mut App, + active_tooltip: &Rc>>, + build_tooltip: &Rc Option<(AnyView, bool)>>, + check_is_hovered: &Rc bool>, + check_is_hovered_during_prepaint: &Rc bool>, + phase: DispatchPhase, + 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, - } + // Separates logic for what mutation should occur from applying it, to avoid overlapping + // RefCell borrows. + enum Action { + None, + CancelShow, + ScheduleShow, + } - 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 - } - } - // These are handled in check_visible_and_update. - Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => { - Action::None - } - }; + 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 + } + } + // These are handled in check_visible_and_update. + Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => { + 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(TOOLTIP_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, - }); - } - } + 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(TOOLTIP_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, + }); + } + } } /// 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, + 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), - } + // 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, - }; + 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, - }); - } - } + 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() + active_tooltip.borrow().is_some() } #[derive(Default)] @@ -3458,195 +3457,195 @@ 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 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 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(); - } + 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, + pub(crate) element: E, } impl Styled for Stateful where - E: Styled, + E: Styled, { - fn style(&mut self) -> &mut StyleRefinement { - self.element.style() - } + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } } impl StatefulInteractiveElement for Stateful where - E: Element, - Self: InteractiveElement, + E: Element, + Self: InteractiveElement, { } impl InteractiveElement for Stateful where - E: InteractiveElement, + E: InteractiveElement, { - fn interactivity(&mut self) -> &mut Interactivity { - self.element.interactivity() - } + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } } impl Element for Stateful where - E: Element, + E: Element, { - type RequestLayoutState = E::RequestLayoutState; - type PrepaintState = E::PrepaintState; + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; - fn id(&self) -> Option { - self.element.id() - } + fn id(&self) -> Option { + self.element.id() + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - self.element.source_location() - } + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + self.element.source_location() + } - fn a11y_role(&self) -> Option { - self.element.a11y_role() - } + 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 write_a11y_info(&self, node: &mut accesskit::Node) { + self.element.write_a11y_info(node); + } - 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 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 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, - ); - } + 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, + E: Element, { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl ParentElement for Stateful where - E: ParentElement, + E: ParentElement, { - fn extend(&mut self, elements: impl IntoIterator) { - self.element.extend(elements) - } + 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>>, + 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(); + /// 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); - }); - } + 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>>, - bounds: Bounds, - max_offset: Point, - child_bounds: Vec>, - scroll_to_bottom: bool, - overflow: Point, - active_item: Option, + offset: 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, + index: usize, + strategy: ScrollStrategy, } #[derive(Default, Debug, Clone, Copy)] enum ScrollStrategy { - #[default] - FirstVisible, - Top, + #[default] + FirstVisible, + Top, } /// A handle to the scrollable aspects of an element. @@ -3656,432 +3655,432 @@ enum ScrollStrategy { pub struct ScrollHandle(Rc>); impl Default for ScrollHandle { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl ScrollHandle { - /// Construct a new scroll handle. - pub fn new() -> Self { - Self(Rc::default()) - } + /// 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 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 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; + /// 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)), - } - } + 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; + /// 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)), - } - } + 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 - } + /// 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() - } + /// 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 + 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, - }); - } + /// 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(); + /// 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 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(); + 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(); - } - } + 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; - } + 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; - } + /// 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; - } + /// 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(); + /// 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.)) - } - } + 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(); + /// 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.)) - } - } + 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() - } + /// 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::{AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext}; - use std::rc::Weak; + use super::*; + use crate::{AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext}; + use std::rc::Weak; - struct TestTooltipView; + 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") - } - } + 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>>>>>; + type CapturedActiveTooltip = Rc>>>>>; - struct TooltipCaptureElement { - child: AnyElement, - captured_active_tooltip: CapturedActiveTooltip, - } + struct TooltipCaptureElement { + child: AnyElement, + captured_active_tooltip: CapturedActiveTooltip, + } - impl IntoElement for TooltipCaptureElement { - type Element = Self; + impl IntoElement for TooltipCaptureElement { + type Element = Self; - fn into_element(self) -> Self::Element { - self - } - } + fn into_element(self) -> Self::Element { + self + } + } - impl Element for TooltipCaptureElement { - type RequestLayoutState = (); - type PrepaintState = (); + impl Element for TooltipCaptureElement { + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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) - }, - ) - }); - } - } + 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, - } + struct TooltipOwner { + captured_active_tooltip: CapturedActiveTooltip, + } - 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()), - ) - .into_any_element(), - captured_active_tooltip: self.captured_active_tooltip.clone(), - } - } - } + 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()), + ) + .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(), - }); - } + #[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(); + handle.scroll_to_active_item(); - assert_eq!(handle.offset().x, px(-25.)); - } + 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(), - }); - } + #[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(); + handle.scroll_to_active_item(); - assert_eq!(handle.offset().y, px(-25.)); - } + assert_eq!(handle.offset().y, px(-25.)); + } - fn setup_tooltip_owner_test() -> ( - 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, - } - }); - let any_window = window.into(); + fn setup_tooltip_owner_test() -> ( + 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, + } + }); + let any_window = window.into(); - test_app - .update_window(any_window, |_, window, cx| { - window.draw(cx).clear(); - }) - .unwrap(); + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(); + }) + .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.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(); - }) - .unwrap(); + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(); + }) + .unwrap(); - (test_app, any_window, captured_active_tooltip) - } + (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(); + #[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(); - 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 { .. }) - )); + 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); + 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()); - } + assert!(weak_active_tooltip.upgrade().is_none()); + } - #[test] - fn tooltip_is_released_when_its_owner_disappears() { - let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(); + #[test] + fn tooltip_is_released_when_its_owner_disappears() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(); - let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); - let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); - test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY); - test_app.run_until_parked(); + test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY); + test_app.run_until_parked(); - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::Visible { .. }) - )); + 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); + 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()); - } + assert!(weak_active_tooltip.upgrade().is_none()); + } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index ee1436134a..6617f643ca 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -1,8 +1,8 @@ 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, + 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}; @@ -13,152 +13,152 @@ 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(), - } + 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>>, + 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::, - } - } + 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) - } + /// 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::*; + 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)) - } + 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]>, + 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) - } + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } } impl Styled for ImageCacheElement { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } } impl IntoElement for ImageCacheElement { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Element for ImageCacheElement { - type RequestLayoutState = SmallVec<[LayoutId; 4]>; - type PrepaintState = (); + type RequestLayoutState = SmallVec<[LayoutId; 4]>; + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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); - } - }) - } + 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. @@ -166,188 +166,188 @@ 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>), + /// 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() - } + 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()), - } - } + /// 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>>; + /// 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; + /// 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() - } + 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() - } + 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 - } + /// 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); + /// 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(); - } + 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 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(); + let entity = window.current_view(); + window + .spawn(cx, { + async move |cx| { + _ = task.await; + cx.on_next_frame(move |_, cx| { + cx.notify(entity); + }); + } + }) + .detach(); - None - } + 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)); - } - } - } + /// 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)); - } - } + /// 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 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() - } + /// 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) - } + 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() } + RetainAllImageCacheProvider { id: id.into() } } /// A provider struct for creating a retain-all image cache inline pub struct RetainAllImageCacheProvider { - id: ElementId, + 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() - } + 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/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 232d3b8f1d..67887ab742 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -1,27 +1,27 @@ 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, px, + 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, px, }; use anyhow::Result; use futures::Future; use gpui_util::ResultExt; use image::{ - AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba, - codecs::{gif::GifDecoder, webp::WebPDecoder}, + AnimationDecoder, DynamicImage, Frame, 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, + fs, + io::{self, Cursor}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::Duration, }; use thiserror::Error; @@ -39,563 +39,563 @@ 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>>>), + /// 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() + url::Url::from_str(uri).is_ok() } impl From for ImageSource { - fn from(value: SharedUri) -> Self { - Self::Resource(Resource::Uri(value)) - } + 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())) - } - } + 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())) - } - } + 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() - } + 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()) - } + 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()) - } + fn from(value: Arc) -> Self { + Self::Resource(value.into()) + } } impl From for ImageSource { - fn from(value: PathBuf) -> Self { - Self::Resource(value.into()) - } + fn from(value: PathBuf) -> Self { + Self::Resource(value.into()) + } } impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::Render(value) - } + fn from(value: Arc) -> Self { + Self::Render(value) + } } impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::Image(value) - } + fn from(value: Arc) -> Self { + Self::Image(value) + } } impl From for ImageSource where - F: Fn(&mut Window, &mut App) -> Option, ImageCacheError>> + 'static, + F: Fn(&mut Window, &mut App) -> Option, ImageCacheError>> + 'static, { - fn from(value: F) -> Self { - Self::Custom(Arc::new(value)) - } + 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>>, + 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, - } - } + 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; + /// 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 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 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 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 - } + /// 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 - } + 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 - } + 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, + 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, - } + 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", - ] - } + /// 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 - } - } + /// 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; + type Target = Img; - fn deref(&self) -> &Self::Target { - &self.element - } + fn deref(&self) -> &Self::Target { + &self.element + } } impl DerefMut for Stateful { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.element - } + 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<()>)>, + 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, + frame_index: usize, + replacement: Option, } impl Element for Img { - type RequestLayoutState = ImgLayoutState; - type PrepaintState = Option; + type RequestLayoutState = ImgLayoutState; + type PrepaintState = Option; - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - self.interactivity.source_location() - } + 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, - }; + 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, - }) - }); + 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 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; + 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); + 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 { - 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 let Some(state) = &mut state { + state.frame_index = state.frame_index.min(max_frame_index); + if frame_count > 1 { + 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; - } + 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); - style.aspect_ratio = Some(image_size.width / image_size.height); + let image_size = data.render_size(frame_index); + 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.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 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() - { - 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)); - } - } - } - } + if global_id.is_some() + && data.frame_count() > 1 + && window.is_window_active() + { + 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) - }, - ); + window.request_layout(style, replacement_id, cx) + }, + ); - layout_state.frame_index = frame_index; + layout_state.frame_index = frame_index; - ((layout_id, layout_state), state) - }) - } + ((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); - } + 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 - }, - ) - } + 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()) - .clamp_radii_for_quad_size(new_bounds.size); - window - .paint_image( - 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); - } - }, - ) - } + 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()) + .clamp_radii_for_quad_size(new_bounds.size); + window + .paint_image( + 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 - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } } impl InteractiveElement for Img { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } } impl IntoElement for Img { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::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 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), - } - } + 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), - } - } + /// 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), + } + } } #[derive(Clone)] enum ImageDecoder {} impl Asset for ImageDecoder { - type Source = Arc; - type Output = Result, ImageCacheError>; + 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) } - } + 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 @@ -603,258 +603,258 @@ impl Asset for ImageDecoder { pub enum ImageAssetLoader {} impl Asset for ImageAssetLoader { - type Source = Resource; - type Output = Result, ImageCacheError>; + 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 _; + 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 _; - let response = client - .get(uri.as_ref(), true) - .await - .with_context(|| format!("loading image asset from {uri:?}"))?; - if !response.status.is_success() { - let mut error_body = String::from_utf8_lossy(&response.body).into_owned(); - let first_line = error_body.lines().next().unwrap_or("").trim_end(); - error_body.truncate(first_line.len()); - return Err(ImageCacheError::BadStatus { - uri, - status: response.status, - body: error_body, - }); - } - response.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(), - )); - } - } - }; + let response = client + .get(uri.as_ref(), true) + .await + .with_context(|| format!("loading image asset from {uri:?}"))?; + if !response.status.is_success() { + let mut error_body = String::from_utf8_lossy(&response.body).into_owned(); + let first_line = error_body.lines().next().unwrap_or("").trim_end(); + error_body.truncate(first_line.len()); + return Err(ImageCacheError::BadStatus { + uri, + status: response.status, + body: error_body, + }); + } + response.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(); + 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}" - ); - } - } - } + 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:?})" - )))); - } + 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))?; + 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(); + 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}" - ); - } - } - } + 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:?})" - )))); - } + if frames.is_empty() { + return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!( + "WebP could not be decoded: all frames failed ({source:?})" + )))); + } - frames - } else { - let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8(); + frames + } else { + let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8(); - // Convert from RGBA to BGRA. - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } - SmallVec::from_elem(Frame::new(data), 1) - } - } - _ => { - let mut data = - image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); + SmallVec::from_elem(Frame::new(data), 1) + } + } + _ => { + let mut data = + image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); - // Convert from RGBA to BGRA. - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } - SmallVec::from_elem(Frame::new(data), 1) - } - }; + SmallVec::from_elem(Frame::new(data), 1) + } + }; - Ok(Arc::new(RenderImage::new(data))) - } else { - svg_renderer - .render_single_frame(&bytes, 1.0) - .map_err(Into::into) - } - } - } + 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::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), + /// 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::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)) - } + 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)) - } + 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)) - } + 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)) - } + 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}; + use super::*; + use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size}; + use image::{Frame, ImageBuffer, Rgba}; - const TEST_IMG_ID: &str = "test-img"; + 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(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()), + ))) + } - /// 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) - }); - }); - }, - ) - } + /// 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 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 stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) { - let window = cx.add_empty_window(); + #[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() - }); - } + // 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/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 5a729dcc5f..7574b500ca 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,10 +8,10 @@ //! 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, + 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 _; @@ -22,31 +22,31 @@ type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'sta /// Construct a new list element pub fn list( - state: ListState, - render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, + 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(), - } + 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, + 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 - } + /// 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. @@ -54,25 +54,25 @@ impl List { 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") - } + 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, + 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. @@ -83,219 +83,219 @@ struct StateInner { /// 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), + /// 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, + /// 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, + /// 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, + /// 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, - }, + #[default] + Normal, + Tail { + is_following: bool, + }, } impl FollowState { - fn is_following(&self) -> bool { - matches!(self, FollowState::Tail { is_following: true }) - } + 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 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 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, - }; - } - } + 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, + /// 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 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, + /// 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 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, + /// 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 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, + /// 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 => {} - } - } + 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, + /// 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, + max_item_width: Pixels, + scroll_top: ListOffset, + item_layouts: VecDeque, } struct ItemLayout { - index: usize, - element: AnyElement, - size: Size, + index: usize, + element: AnyElement, + size: Size, } /// Frame state used by the [List] element after layout. pub struct ListPrepaintState { - hitbox: Hitbox, - layout: LayoutItemsResponse, + hitbox: Hitbox, + layout: LayoutItemsResponse, } #[derive(Clone)] enum ListItem { - Unmeasured { - size_hint: Option>, - focus_handle: Option, - }, - Measured { - size: Size, - focus_handle: Option, - }, + 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(&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 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 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)) - } - } - } + 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, + 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)] @@ -305,2168 +305,2168 @@ struct Count(usize); 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 - } + /// 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 - } + /// 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 + } - /// 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.scrollbar_drag_start_height = None; - state.items.summary().count - }; + /// 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.scrollbar_drag_start_height = None; + state.items.summary().count + }; - self.splice(0..old_count, element_count); - } + self.splice(0..old_count, element_count); + } - /// 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); - } + /// 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); - } + /// 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(); + 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); + 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 - }; + 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()) - } - }; - } - } + 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(); - } + // 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 - } + /// 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) - } + /// 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)) - } + /// 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(); + /// 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 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; + 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; - } - } - } + 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)) - } + /// 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() - } + /// 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; - } + /// 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(); + let current_offset = self.logical_scroll_top(); + let state = &mut *self.0.borrow_mut(); - if distance < px(0.) { - state.follow_state.stop_following(); - } + 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 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 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); + } - state.logical_scroll_top = Some(ListOffset { - item_ix: cursor.start().count, - offset_in_item: new_pixel_offset - cursor.start().height, - }); - } + state.logical_scroll_top = Some(ListOffset { + item_ix: cursor.start().count, + offset_in_item: new_pixel_offset - cursor.start().height, + }); + } - /// 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.logical_scroll_top = Some(ListOffset { - item_ix: item_count, - offset_in_item: px(0.), - }); - } + /// 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.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(); + /// 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.), - }); - } - } - } - } + 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.), + }); + } + } + } + } - /// 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 } - ) - } + /// 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.); - } + /// 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(); - } + if scroll_top.item_ix < item_count { + state.follow_state.stop_following(); + } - state.logical_scroll_top = Some(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(); + /// 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(); + 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); + 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; + 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; - } - } + if start_ix >= scroll_top.item_ix { + scroll_top.item_ix = start_ix; + scroll_top.offset_in_item = goal_top - start_item_top; + } + } - state.logical_scroll_top = Some(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(); + /// 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 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 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; + 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 - } + 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); - } + /// 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(); - } + /// 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() - } + /// 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); - } + /// 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 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(); + /// 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()); - } + 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 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; + 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) - } + 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() - } + /// 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. - pub fn item_is_above_viewport(&self, ix: usize) -> Option { - let viewport_bounds = self.viewport_bounds(); - if viewport_bounds.size.height == px(0.0) { - return None; - } + /// Returns whether the item is entirely above the viewport, or `None` if + /// the list has not measured enough layout to know. + pub fn item_is_above_viewport(&self, ix: usize) -> Option { + let viewport_bounds = self.viewport_bounds(); + if viewport_bounds.size.height == px(0.0) { + return None; + } - 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 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()) - } + 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. - pub fn item_is_below_viewport(&self, ix: usize) -> Option { - let viewport_bounds = self.viewport_bounds(); - if viewport_bounds.size.height == px(0.0) { - return None; - } + /// Returns whether the item is entirely below the viewport, or `None` if + /// the list has not measured enough layout to know. + pub fn item_is_below_viewport(&self, ix: usize) -> Option { + let viewport_bounds = self.viewport_bounds(); + if viewport_bounds.size.height == px(0.0) { + return None; + } - 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 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()) - } + let item_bounds = self.bounds_for_item(ix)?; + Some(item_bounds.top() >= viewport_bounds.bottom()) + } } impl StateInner { - 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 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 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; - } + 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); + 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.logical_scroll_top = None; - } else { - let (start, ..) = - self.items - .find::((), &Height(new_scroll_top), Bias::Right); - let item_ix = start.count; - let offset_in_item = new_scroll_top - start.height; - self.logical_scroll_top = Some(ListOffset { - item_ix, - offset_in_item, - }); - } + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let (start, ..) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } - if delta.y > px(0.) { - self.follow_state.stop_following(); - } + 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, - ); - } + 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); - } + 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 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 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; - } - } + 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 cursor = self.items.cursor::(()); + let available_item_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::MinContent, + ); - let mut measured_items = Vec::default(); + 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) - }); + 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(), - }); - } + measured_items.push(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } - self.items = SumTree::from_iter(measured_items, ()); - } + 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(); + 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); - } + 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 mut rendered_focused_item = false; - let available_item_space = size( - available_width.map_or(AvailableSpace::MinContent, |width| { - AvailableSpace::Definite(width) - }), - AvailableSpace::MinContent, - ); + let available_item_space = size( + available_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); - let mut cursor = old_items.cursor::(()); + 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; - } + // 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(); + // 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 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 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; - } - } - } + 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; + 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); + // 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; - } - } + // 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, - }; + 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; - } - }; - } + 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) - }; + // 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; - } - } + 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; + 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 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(); - } - } + // 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, - } - } + 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); - } - _ => {} - } + 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, - ); + 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(); + // 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 }), |window| { - item.element.prepaint_at(item_origin, window, cx); - }); + // 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 }), |window| { + item.element.prepaint_at(item_origin, window, cx); + }); - if let Some(autoscroll_bounds) = window.take_autoscroll() - && autoscroll - { - if autoscroll_bounds.top() < bounds.top() { - return Err(ListOffset { - item_ix: item.index, - offset_in_item: autoscroll_bounds.top() - item_origin.y, - }); - } 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; + if let Some(autoscroll_bounds) = window.take_autoscroll() + && autoscroll + { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } 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; + // 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 }; + // 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; - } + 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 - }, - }); - } - } + 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(); - } + item_origin.y += item.size.height; + } + } else { + layout_response.item_layouts.clear(); + } - Ok(layout_response) - }) - } + Ok(layout_response) + }) + } - // Scrollbar support + // 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; + 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); + 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.logical_scroll_top = Some(ListOffset { - item_ix: item_count, - offset_in_item: px(0.), - }); - return; - } + // 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.logical_scroll_top = Some(ListOffset { + item_ix: item_count, + offset_in_item: px(0.), + }); + return; + } - self.follow_state.stop_following(); + self.follow_state.stop_following(); - if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { - self.logical_scroll_top = None; - } else { - let (start, _, _) = - self.items - .find::((), &Height(new_scroll_top), Bias::Right); + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let (start, _, _) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); - let item_ix = start.count; - let offset_in_item = new_scroll_top - start.height; - self.logical_scroll_top = Some(ListOffset { - item_ix, - offset_in_item, - }); - } - } + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } + } } 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(), - } - } + 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, + /// 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; + type RequestLayoutState = (); + type PrepaintState = ListPrepaintState; - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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(); + 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 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 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; + 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, ()) - } + 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; + 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 mut style = Style::default(); + style.refine(&self.style); - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + 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(), - }), - (), - ); + // 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(); - } + 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() - } - }; + 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 } - } + 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(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - for item in &mut prepaint.layout.item_layouts { - item.element.paint(window, cx); - } - }); + 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(); + window.with_content_mask(Some(ContentMask { bounds }), |window| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(window, cx); + } + }); - 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, - ) - } - }); - } + 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, + ) + } + }); + } } impl IntoElement for List { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Styled for List { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } } impl sum_tree::Item for ListItem { - type Summary = ListItemSummary; + 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, - }, - } - } + 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 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; - } + 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 zero(_cx: ()) -> Self { + Default::default() + } - fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { - self.0 += summary.count; - } + 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 zero(_cx: ()) -> Self { + Default::default() + } - fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { - self.0 += summary.height; - } + 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() - } + 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() - } + 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, Context, Element, FollowMode, IntoElement, ListState, Render, - Styled, TestAppContext, Window, div, list, point, px, size, - }; - - #[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.)); - } - - 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_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_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_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 \ + use gpui::{ScrollDelta, ScrollWheelEvent}; + use std::cell::Cell; + use std::rc::Rc; + + use crate::{ + self as gpui, AppContext, Context, Element, FollowMode, IntoElement, ListState, Render, + Styled, TestAppContext, Window, div, list, point, px, size, + }; + + #[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.)); + } + + 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_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_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_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(); + #[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(); + // 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() - } - } + 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()))); + 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.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 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" - ); - } + // 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(); + #[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(); + 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() - } - } + 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()))); + 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.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.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.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(); + 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() - }); + 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 \ + 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/src/elements/surface.rs b/crates/gpui/src/elements/surface.rs index 8e51963c2e..508a3f9dbf 100644 --- a/crates/gpui/src/elements/surface.rs +++ b/crates/gpui/src/elements/surface.rs @@ -1,6 +1,6 @@ use crate::{ - App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, - ObjectFit, Pixels, Style, StyleRefinement, Styled, Window, + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + ObjectFit, Pixels, Style, StyleRefinement, Styled, Window, }; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use crate::{DevicePixels, Size}; @@ -12,154 +12,154 @@ use std::sync::Arc; /// A source of a surface's content. pub enum SurfaceSource { - /// A macOS image buffer from CoreVideo - #[cfg(target_os = "macos")] - Surface(CVPixelBuffer), - /// A GPU texture handle (type-erased to avoid depending on wgpu) - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - Texture { - /// The GPU texture, type-erased (expected to be `Arc`) - texture: Arc, - /// Dimensions of the texture in device pixels - size: Size, - }, + /// A macOS image buffer from CoreVideo + #[cfg(target_os = "macos")] + Surface(CVPixelBuffer), + /// A GPU texture handle (type-erased to avoid depending on wgpu) + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + Texture { + /// The GPU texture, type-erased (expected to be `Arc`) + texture: Arc, + /// Dimensions of the texture in device pixels + size: Size, + }, } impl Clone for SurfaceSource { - fn clone(&self) -> Self { - match *self { - #[cfg(target_os = "macos")] - SurfaceSource::Surface(ref buf) => SurfaceSource::Surface(buf.clone()), - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - SurfaceSource::Texture { ref texture, size } => SurfaceSource::Texture { - texture: Arc::clone(texture), - size, - }, - } - } + fn clone(&self) -> Self { + match *self { + #[cfg(target_os = "macos")] + SurfaceSource::Surface(ref buf) => SurfaceSource::Surface(buf.clone()), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + SurfaceSource::Texture { ref texture, size } => SurfaceSource::Texture { + texture: Arc::clone(texture), + size, + }, + } + } } impl std::fmt::Debug for SurfaceSource { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - #[cfg(target_os = "macos")] - SurfaceSource::Surface(ref buf) => _f.debug_tuple("Surface").field(buf).finish(), - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - SurfaceSource::Texture { size, .. } => _f - .debug_struct("Texture") - .field("size", &size) - .finish_non_exhaustive(), - } - } + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + #[cfg(target_os = "macos")] + SurfaceSource::Surface(ref buf) => _f.debug_tuple("Surface").field(buf).finish(), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + SurfaceSource::Texture { size, .. } => _f + .debug_struct("Texture") + .field("size", &size) + .finish_non_exhaustive(), + } + } } #[cfg(target_os = "macos")] impl From for SurfaceSource { - fn from(value: CVPixelBuffer) -> Self { - SurfaceSource::Surface(value) - } + fn from(value: CVPixelBuffer) -> Self { + SurfaceSource::Surface(value) + } } /// A surface element. pub struct Surface { - source: SurfaceSource, - object_fit: ObjectFit, - style: StyleRefinement, + source: SurfaceSource, + object_fit: ObjectFit, + style: StyleRefinement, } /// Create a new surface element. pub fn surface(source: impl Into) -> Surface { - Surface { - source: source.into(), - object_fit: ObjectFit::Contain, - style: Default::default(), - } + 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 - } + /// 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 = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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>, - _bounds: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - _window: &mut Window, - _: &mut App, - ) { - match self.source { - #[cfg(target_os = "macos")] - SurfaceSource::Surface(ref 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()); - } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - SurfaceSource::Texture { - ref texture, - ref size, - } => { - let new_bounds = self.object_fit.get_bounds(_bounds, *size); - _window.paint_surface(new_bounds, Arc::clone(texture), *size); - } - } - } + fn paint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + _window: &mut Window, + _: &mut App, + ) { + match self.source { + #[cfg(target_os = "macos")] + SurfaceSource::Surface(ref 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()); + } + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + SurfaceSource::Texture { + ref texture, + ref size, + } => { + let new_bounds = self.object_fit.get_bounds(_bounds, *size); + _window.paint_surface(new_bounds, Arc::clone(texture), *size); + } + } + } } impl IntoElement for Surface { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Styled for Surface { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } } diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index a29b106c0e..88213ff680 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,276 +1,276 @@ use std::{fs, 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, + 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, + interactivity: Interactivity, + transformation: Option, + path: Option, + external_path: Option, } /// Create a new SVG element. #[track_caller] pub fn svg() -> Svg { - Svg { - interactivity: Interactivity::new(), - transformation: None, - path: None, - external_path: None, - } + Svg { + interactivity: Interactivity::new(), + transformation: None, + path: None, + external_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 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 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 + } - /// 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 - } + /// 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; + type RequestLayoutState = (); + type PrepaintState = Option; - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - self.interactivity.source_location() - } + 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 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 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| { - if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { - let transformation = self - .transformation - .as_ref() - .map(|transformation| { - transformation.into_matrix(bounds.center(), window.scale_factor()) - }) - .unwrap_or_default(); + 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| { + if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), window.scale_factor()) + }) + .unwrap_or_default(); - window - .paint_svg(bounds, path.clone(), None, 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(), None, 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; + }; - let transformation = self - .transformation - .as_ref() - .map(|transformation| { - transformation.into_matrix(bounds.center(), window.scale_factor()) - }) - .unwrap_or_default(); + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), window.scale_factor()) + }) + .unwrap_or_default(); - window - .paint_svg( - bounds, - path.clone(), - Some(&bytes), - transformation, - color, - cx, - ) - .log_err(); - } - }, - ) - } + window + .paint_svg( + bounds, + path.clone(), + Some(&bytes), + transformation, + color, + cx, + ) + .log_err(); + } + }, + ) + } } impl IntoElement for Svg { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl Styled for Svg { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } } impl InteractiveElement for Svg { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } + 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, + 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), - } - } + 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 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 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, - } - } + /// 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 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 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 - } + /// 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)) - } + 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>; + 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) - } - } + 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/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 5a78bac223..8be5bacc97 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,21 +1,21 @@ 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, TextTransform, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine, - WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, + ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, + HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, + TextRun, TextStyle, TextTransform, 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, + borrow::Cow, + cell::{Cell, RefCell}, + mem, + ops::{Deref, DerefMut, Range}, + rc::Rc, + sync::Arc, }; use unicode_segmentation::UnicodeSegmentation; @@ -66,64 +66,64 @@ use unicode_segmentation::UnicodeSegmentation; /// contents", or an old node being destroyed and a new node being created. #[derive(Debug, Clone)] pub struct Text { - id: Option, - text: SharedString, + 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 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 } - } + /// 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() - } + /// 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 - } + /// 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 - } + /// 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 - } + 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 - } + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.text + } } /// Trivial hash function for the location information produced by the [`text`] @@ -131,18 +131,18 @@ impl DerefMut for Text { /// 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; + 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 + 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. @@ -159,229 +159,229 @@ pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> /// 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()) - }}; + (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 - } + type Element = Self; + #[inline] + fn into_element(self) -> Self::Element { + self + } } impl Element for Text { - type RequestLayoutState = TextLayout; - type PrepaintState = (); + type RequestLayoutState = TextLayout; + type PrepaintState = (); - fn id(&self) -> Option { - self.id.clone() - } + fn id(&self) -> Option { + self.id.clone() + } - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } + 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 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 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 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 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, - ); - } + 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 = (); + type RequestLayoutState = TextLayout; + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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) - } + 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; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } impl IntoElement for String { - type Element = SharedString; + type Element = SharedString; - fn into_element(self) -> Self::Element { - self.into() - } + fn into_element(self) -> Self::Element { + self.into() + } } impl IntoElement for Cow<'static, str> { - type Element = SharedString; + type Element = SharedString; - fn into_element(self) -> Self::Element { - self.into() - } + fn into_element(self) -> Self::Element { + self.into() + } } impl Element for SharedString { - type RequestLayoutState = TextLayout; - type PrepaintState = (); + type RequestLayoutState = TextLayout; + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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 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) - } + 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; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } /// Renders text with runs of different styles. @@ -390,224 +390,224 @@ impl IntoElement for SharedString { /// 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, + 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(), - } - } + /// 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 - } + /// 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_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 - } + /// 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 - } + 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 - } + /// 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; - } - } + 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 - } + /// 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 = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn id(&self) -> Option { - None - } + fn id(&self) -> Option { + None + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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) - }) - }); + 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); - } + 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, ()) - } + 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 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) - } + 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; + type Element = Self; - fn into_element(self) -> Self::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. @@ -615,881 +615,879 @@ impl IntoElement for StyledText { pub struct TextLayout(Rc>>); struct TextLayoutInner { - len: usize, - lines: SmallVec<[WrappedLine; 1]>, - line_height: Pixels, - wrap_width: Option, - size: Option>, - bounds: Option>, + len: usize, + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, + wrap_width: Option, + size: Option>, + bounds: Option>, } fn apply_text_transform_preserving_byte_len( - text: SharedString, - transform: Option, + text: SharedString, + transform: Option, ) -> SharedString { - let Some(transform) = transform else { - return text; - }; - if matches!(transform, TextTransform::None) { - return text; - } + let Some(transform) = transform else { + return text; + }; + if matches!(transform, TextTransform::None) { + return text; + } - let mut output = String::with_capacity(text.len()); - match transform { - TextTransform::Uppercase => { - for character in text.as_ref().chars() { - push_case_mapped_character(&mut output, character, CaseMapKind::Upper); - } - } - TextTransform::Lowercase => { - for character in text.as_ref().chars() { - push_case_mapped_character(&mut output, character, CaseMapKind::Lower); - } - } - TextTransform::Capitalize => { - for piece in text.as_ref().split_word_bounds() { - let mut seen_first_letter = false; - for character in piece.chars() { - if !seen_first_letter && character.is_alphabetic() { - push_case_mapped_character(&mut output, character, CaseMapKind::Upper); - seen_first_letter = true; - } else { - output.push(character); - } - } - } - } - TextTransform::None => return text, - } + let mut output = String::with_capacity(text.len()); + match transform { + TextTransform::Uppercase => { + for character in text.as_ref().chars() { + push_case_mapped_character(&mut output, character, CaseMapKind::Upper); + } + } + TextTransform::Lowercase => { + for character in text.as_ref().chars() { + push_case_mapped_character(&mut output, character, CaseMapKind::Lower); + } + } + TextTransform::Capitalize => { + for piece in text.as_ref().split_word_bounds() { + let mut seen_first_letter = false; + for character in piece.chars() { + if !seen_first_letter && character.is_alphabetic() { + push_case_mapped_character(&mut output, character, CaseMapKind::Upper); + seen_first_letter = true; + } else { + output.push(character); + } + } + } + } + TextTransform::None => return text, + } - SharedString::from(output) + SharedString::from(output) } #[derive(Copy, Clone)] enum CaseMapKind { - Upper, - Lower, + Upper, + Lower, } fn push_case_mapped_character(output: &mut String, character: char, kind: CaseMapKind) { - let mapped = match kind { - CaseMapKind::Upper => character.to_uppercase().collect::(), - CaseMapKind::Lower => character.to_lowercase().collect::(), - }; + let mapped = match kind { + CaseMapKind::Upper => character.to_uppercase().collect::(), + CaseMapKind::Lower => character.to_lowercase().collect::(), + }; - if mapped.len() == character.len_utf8() && mapped.chars().count() == 1 { - output.push_str(&mapped); - } else { - output.push(character); - } + if mapped.len() == character.len_utf8() && mapped.chars().count() == 1 { + output.push_str(&mapped); + } else { + output.push(character); + } } #[cfg(test)] mod text_transform_tests { - use super::apply_text_transform_preserving_byte_len; - use crate::{SharedString, TextTransform}; + use super::apply_text_transform_preserving_byte_len; + use crate::{SharedString, TextTransform}; - #[test] - fn text_transforms_preserve_bytes_and_spacing() { - let input = SharedString::from("hello WORLD\tfoo-bar 123baz déjà vu"); - let uppercase = - apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase)); - let lowercase = - apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase)); - let capitalize = apply_text_transform_preserving_byte_len( - input.clone(), - Some(TextTransform::Capitalize), - ); + #[test] + fn text_transforms_preserve_bytes_and_spacing() { + let input = SharedString::from("hello WORLD\tfoo-bar 123baz déjà vu"); + let uppercase = + apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase)); + let lowercase = + apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase)); + let capitalize = apply_text_transform_preserving_byte_len( + input.clone(), + Some(TextTransform::Capitalize), + ); - assert_eq!(uppercase.as_ref(), "HELLO WORLD\tFOO-BAR 123BAZ DÉJÀ VU"); - assert_eq!(lowercase.as_ref(), "hello world\tfoo-bar 123baz déjà vu"); - assert_eq!(capitalize.as_ref(), "Hello WORLD\tFoo-Bar 123Baz Déjà Vu"); - assert_eq!(input.len(), uppercase.len()); - assert_eq!(input.len(), lowercase.len()); - assert_eq!(input.len(), capitalize.len()); - } + assert_eq!(uppercase.as_ref(), "HELLO WORLD\tFOO-BAR 123BAZ DÉJÀ VU"); + assert_eq!(lowercase.as_ref(), "hello world\tfoo-bar 123baz déjà vu"); + assert_eq!(capitalize.as_ref(), "Hello WORLD\tFoo-Bar 123Baz Déjà Vu"); + assert_eq!(input.len(), uppercase.len()); + assert_eq!(input.len(), lowercase.len()); + assert_eq!(input.len(), capitalize.len()); + } - #[test] - fn text_transforms_skip_expanding_unicode_mappings() { - let input = SharedString::from("straße İSTANBUL"); - let uppercase = - apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase)); - let lowercase = - apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase)); + #[test] + fn text_transforms_skip_expanding_unicode_mappings() { + let input = SharedString::from("straße İSTANBUL"); + let uppercase = + apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase)); + let lowercase = + apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase)); - assert_eq!(uppercase.as_ref(), "STRAßE İSTANBUL"); - assert_eq!(lowercase.as_ref(), "straße İstanbul"); - assert_eq!(input.len(), uppercase.len()); - assert_eq!(input.len(), lowercase.len()); - } + assert_eq!(uppercase.as_ref(), "STRAßE İSTANBUL"); + assert_eq!(lowercase.as_ref(), "straße İstanbul"); + assert_eq!(input.len(), uppercase.len()); + assert_eq!(input.len(), lowercase.len()); + } - #[test] - fn capitalize_preserves_letters_after_digit_prefix() { - let input = SharedString::from("123BAZ"); - let output = apply_text_transform_preserving_byte_len( - input.clone(), - Some(TextTransform::Capitalize), - ); - assert_eq!(output.as_ref(), "123BAZ"); - assert_eq!(input.len(), output.len()); - } + #[test] + fn capitalize_preserves_letters_after_digit_prefix() { + let input = SharedString::from("123BAZ"); + let output = apply_text_transform_preserving_byte_len( + input.clone(), + Some(TextTransform::Capitalize), + ); + assert_eq!(output.as_ref(), "123BAZ"); + assert_eq!(input.len(), output.len()); + } - #[test] - fn capitalize_does_not_fold_remaining_letters() { - let input = SharedString::from("foo2BAR"); - let output = - apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize)); - assert_eq!(output.as_ref(), "Foo2BAR"); - } + #[test] + fn capitalize_does_not_fold_remaining_letters() { + let input = SharedString::from("foo2BAR"); + let output = + apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize)); + assert_eq!(output.as_ref(), "Foo2BAR"); + } - #[test] - fn capitalize_handles_apostrophe_contractions() { - let input = SharedString::from("don't panic"); - let output = - apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize)); - assert_eq!(output.as_ref(), "Don't Panic"); - } + #[test] + fn capitalize_handles_apostrophe_contractions() { + let input = SharedString::from("don't panic"); + let output = + apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize)); + assert_eq!(output.as_ref(), "Don't Panic"); + } } /// Metadata about how text should be truncated. Generated during text layout via `TextLayout::evaluate_overflow`. pub struct TextLayoutTruncation { - /// The width that the text can occupy before it is truncated. - pub width: Option, - /// The text to affix to the displayed text if truncating (e.g. an ellipsis `...`). - pub affix: SharedString, - /// What side of the text will be truncated if it does not fit. - pub source: TruncateFrom, + /// The width that the text can occupy before it is truncated. + pub width: Option, + /// The text to affix to the displayed text if truncating (e.g. an ellipsis `...`). + pub affix: SharedString, + /// What side of the text will be truncated if it does not fit. + pub source: TruncateFrom, } impl TextLayoutTruncation { - /// Creates a truncation by using the overflow as the affix, given the provided width. - fn overflow_width(text_overflow: TextOverflow, width: Option) -> Self { - match text_overflow { - TextOverflow::Truncate(s) => TextLayoutTruncation { - width, - affix: s, - source: TruncateFrom::End, - }, - TextOverflow::TruncateStart(s) => TextLayoutTruncation { - width, - affix: s, - source: TruncateFrom::Start, - }, - } - } + /// Creates a truncation by using the overflow as the affix, given the provided width. + fn overflow_width(text_overflow: TextOverflow, width: Option) -> Self { + match text_overflow { + TextOverflow::Truncate(s) => TextLayoutTruncation { + width, + affix: s, + source: TruncateFrom::End, + }, + TextOverflow::TruncateStart(s) => TextLayoutTruncation { + width, + affix: s, + source: TruncateFrom::Start, + }, + } + } } impl TextLayout { - /// Evaluates the width to wrap the text at. - pub fn evaluate_wrap_width( - white_space: &WhiteSpace, - known_dimensions: Size>, - available_space: Size, - ) -> Option { - use crate::AvailableSpace::*; - match white_space { - // Text does not wrap, no max width - WhiteSpace::Nowrap => None, - // If the text wraps, return the already calculated width. - WhiteSpace::Normal => known_dimensions.width.or(match available_space.width { - // Otherwise if the available space is a concrete value, then that is the width to wrap to. - Definite(x) => Some(x), - // If the wrapping is content-based, then there is no wrapping of text. - MaxContent | MinContent => None, - }), - } - } + /// Evaluates the width to wrap the text at. + pub fn evaluate_wrap_width( + white_space: &WhiteSpace, + known_dimensions: Size>, + available_space: Size, + ) -> Option { + use crate::AvailableSpace::*; + match white_space { + // Text does not wrap, no max width + WhiteSpace::Nowrap => None, + // If the text wraps, return the already calculated width. + WhiteSpace::Normal => known_dimensions.width.or(match available_space.width { + // Otherwise if the available space is a concrete value, then that is the width to wrap to. + Definite(x) => Some(x), + // If the wrapping is content-based, then there is no wrapping of text. + MaxContent | MinContent => None, + }), + } + } - /// Evaluates how truncation should be applied if the text overflows the available space. - pub fn evaluate_overflow( - text_style: &TextStyle, - known_dimensions: Size>, - available_space: Size, - ) -> TextLayoutTruncation { - match text_style.text_overflow.clone() { - Some(text_overflow) => { - // Calculate the desired width, prioritizing the calculated dimensions, - // falling back on calculating a width from the available space and - // number of lines to clamp to via text style. - 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, - }); + /// Evaluates how truncation should be applied if the text overflows the available space. + pub fn evaluate_overflow( + text_style: &TextStyle, + known_dimensions: Size>, + available_space: Size, + ) -> TextLayoutTruncation { + match text_style.text_overflow.clone() { + Some(text_overflow) => { + // Calculate the desired width, prioritizing the calculated dimensions, + // falling back on calculating a width from the available space and + // number of lines to clamp to via text style. + 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, + }); - TextLayoutTruncation::overflow_width(text_overflow, width) - } - None => TextLayoutTruncation { - width: None, - affix: SharedString::default(), - source: TruncateFrom::End, - }, - } - } + TextLayoutTruncation::overflow_width(text_overflow, width) + } + None => TextLayoutTruncation { + width: None, + affix: SharedString::default(), + source: TruncateFrom::End, + }, + } + } - /// Conditionally applies truncation to some text and outputs how the text should be displayed. - pub fn apply_truncation<'runs>( - text: SharedString, - text_style: &TextStyle, - font_size: Pixels, - wrap_width: Option, - truncation: &TextLayoutTruncation, - runs: &'runs [TextRun], - cx: &mut App, - ) -> (SharedString, Cow<'runs, [TextRun]>) { - let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); - line_wrapper.set_letter_spacing(text_style.letter_spacing); - if truncation.width.is_some() { - if let Some(max_lines) = text_style.line_clamp - && let Some(wrap_width) = wrap_width - { - line_wrapper.truncate_wrapped_line( - text, - wrap_width, - max_lines, - &truncation.affix, - &runs, - truncation.source, - ) - } else { - line_wrapper.truncate_line( - text, - truncation.width.unwrap_or(Pixels::MAX), - &truncation.affix, - &runs, - truncation.source, - ) - } - } else { - (text, std::borrow::Cow::Borrowed(runs)) - } - } + /// Conditionally applies truncation to some text and outputs how the text should be displayed. + pub fn apply_truncation<'runs>( + text: SharedString, + text_style: &TextStyle, + font_size: Pixels, + wrap_width: Option, + truncation: &TextLayoutTruncation, + runs: &'runs [TextRun], + cx: &mut App, + ) -> (SharedString, Cow<'runs, [TextRun]>) { + let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); + line_wrapper.set_letter_spacing(text_style.letter_spacing); + if truncation.width.is_some() { + if let Some(max_lines) = text_style.line_clamp + && let Some(wrap_width) = wrap_width + { + line_wrapper.truncate_wrapped_line( + text, + wrap_width, + max_lines, + &truncation.affix, + &runs, + truncation.source, + ) + } else { + line_wrapper.truncate_line( + text, + truncation.width.unwrap_or(Pixels::MAX), + &truncation.affix, + &runs, + truncation.source, + ) + } + } else { + (text, std::borrow::Cow::Borrowed(runs)) + } + } - fn layout( - &self, - text: SharedString, - runs: Option>, - window: &mut Window, - _: &mut App, - ) -> LayoutId { - let text_style = window.text_style(); - let text = apply_text_transform_preserving_byte_len(text, text_style.text_transform); - 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()), - ); + fn layout( + &self, + text: SharedString, + runs: Option>, + window: &mut Window, + _: &mut App, + ) -> LayoutId { + let text_style = window.text_style(); + let text = apply_text_transform_preserving_byte_len(text, text_style.text_transform); + 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(); + 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 = Self::evaluate_wrap_width( - &text_style.white_space, - known_dimensions, - available_space, - ); + move |known_dimensions, available_space, window, cx| { + let wrap_width = Self::evaluate_wrap_width( + &text_style.white_space, + known_dimensions, + available_space, + ); - let truncation = - Self::evaluate_overflow(&text_style, known_dimensions, available_space); + let truncation = + Self::evaluate_overflow(&text_style, known_dimensions, available_space); - // 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) - 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) - && truncation.width.is_none() - { - return size; - } + // 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) + 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) + && truncation.width.is_none() + { + return size; + } - let (text, runs) = Self::apply_truncation( - text.clone(), - &text_style, - font_size, - wrap_width, - &truncation, - &runs, - cx, - ); - let len = text.len(); + let (text, runs) = Self::apply_truncation( + text.clone(), + &text_style, + font_size, + wrap_width, + &truncation, + &runs, + cx, + ); + 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, - size: Some(Size::default()), - bounds: None, - }); - return Size::default(); - }; + 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, + 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(); - } + 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, - size: Some(size), - bounds: None, - }); + element_state.0.borrow_mut().replace(TextLayoutInner { + lines, + len, + line_height, + wrap_width, + size: Some(size), + bounds: None, + }); - size - } - }) - } + 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 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(); + 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; - } - } + 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"); + /// 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); - } + 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), - } - } - } + 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)) - } + 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; + /// 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; + 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)?); - } - } + 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 - } + 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 bounds = element_state - .bounds - .expect("prepaint has not been performed"); - let line_height = element_state.line_height; + /// 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 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; + 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 { - return Some(line.layout.clone()); - } - } + 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 { + return Some(line.layout.clone()); + } + } - None - } + None + } - /// The bounds of this layout. - pub fn bounds(&self) -> Bounds { - self.0.borrow().as_ref().unwrap().bounds.unwrap() - } + /// 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 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 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. + 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(); + /// 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; + 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 - } + 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>, + 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, + 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>>, + 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(), - } - } + /// 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_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 - } + /// 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 - } + /// 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; + type RequestLayoutState = (); + type PrepaintState = Hitbox; - fn id(&self) -> Option { - Some(self.element_id.clone()) - } + fn id(&self) -> Option { + Some(self.element_id.clone()) + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } - fn a11y_role(&self) -> Option { - Some(accesskit::Role::Label) - } + 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 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 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()); + 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(); - } - } + 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) - }, - ) - } + 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) - } + 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, - ) - } + 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(); - } - }); - } - } + 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); - } - } - } - }); + 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)) - } - }); + 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() - } - }); + // 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() - } - }); + 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, - window, - ); - } + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + check_is_hovered_during_prepaint, + window, + ); + } - self.text - .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); + self.text + .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); - ((), interactive_state) - }, - ); - } + ((), interactive_state) + }, + ); + } } impl IntoElement for InteractiveText { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_into_element_for() { - use crate::{ParentElement as _, SharedString, div}; - use std::borrow::Cow; + #[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")); - } + 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" }) - } + #[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") } - } + // 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 - ); - } + 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/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 0a3314573f..33e6131913 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,10 +5,10 @@ //! 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, + 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, usize}; @@ -20,58 +20,58 @@ use super::ListHorizontalSizingBehavior; /// 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, + id: impl Into, + item_count: usize, + f: impl 'static + Fn(Range, &mut Window, &mut App) -> Vec, ) -> UniformList where - R: IntoElement, + R: IntoElement, { - let id = id.into(); - let mut base_style = StyleRefinement::default(); - base_style.overflow.y = Some(Overflow::Scroll); + 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() - }; + 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(), - } + 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, + 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]>, + items: SmallVec<[AnyElement; 32]>, + decorations: SmallVec<[AnyElement; 2]>, } /// A handle for controlling the scroll position of a uniform list. @@ -82,784 +82,784 @@ 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, + /// 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, + /// 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, + 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, + /// 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, - }))) - } + /// 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 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 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 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, - }); - } + /// 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 - } + /// 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) - } + /// 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 - } - } + /// 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) - } + /// 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); - } + /// 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 - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } } impl Element for UniformList { - type RequestLayoutState = UniformListFrameState; - type PrepaintState = Option; + type RequestLayoutState = UniformListFrameState; + type PrepaintState = Option; - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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 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) - }), - }, - ); + 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(), - }, - ) - } + ( + 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()); + 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 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 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 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() - }); + 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 - }; + 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; + 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; + 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; - } + 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; - } + 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; + 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; + // 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; - } - } + 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 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 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 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 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 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 }; - 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 content_mask = ContentMask { bounds }; + 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 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); - } - }); - } + 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 - }, - ) - } + 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); - } - }, - ) - } + 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; + type Element = Self; - fn into_element(self) -> Self::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; + /// 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, - ) - }) - } + 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 - } + /// 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 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 - } + /// 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 - } + /// 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(); - } + 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::MinContent, |width| { - AvailableSpace::Definite(width) - }), - AvailableSpace::MinContent, - ); - item_to_measure.layout_as_root(available_space, window, cx) - } + 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::MinContent, |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 - } + /// 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 - } + /// 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 - } + fn interactivity(&mut self) -> &mut crate::Interactivity { + &mut self.interactivity + } } #[cfg(test)] mod test { - use crate::TestAppContext; + 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; + #[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]); + actions!(example, [SelectNext, SelectPrev]); - struct TestView { - index: usize, - length: usize, - scroll_handle: UniformListScrollHandle, - focus_handle: FocusHandle, - visible_range: Range, - } + 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(); - } + 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(); - } - } + 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)), - ) - } - } + 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, - } - }); + 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 + // 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); - }) - } + // 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); - }) - } + // 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); - }); + // 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); - }); + // 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); - }) - } + // 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); - }) - } - } + // 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/src/executor.rs b/crates/gpui/src/executor.rs index 42c65fe9b9..8ec5aff423 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -12,487 +12,487 @@ pub use scheduler::{FallibleTask, LocalExecutor as SchedulerLocalExecutor, Prior /// for spawning background tasks. #[derive(Clone)] pub struct BackgroundExecutor { - inner: scheduler::BackgroundExecutor, - dispatcher: Arc, + 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, - not_send: PhantomData>, + inner: scheduler::LocalExecutor, + dispatcher: Arc, + 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); + /// 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, + 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(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(); - } + #[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())) - }; + /// 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())); + #[cfg(not(any(test, feature = "test-support")))] + let scheduler: Arc = Arc::new(PlatformScheduler::new(dispatcher.clone())); - Self { - inner: scheduler::BackgroundExecutor::new(scheduler), - dispatcher, - } - } + 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() - } + /// 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() + } - /// 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. + #[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) - } - } + /// 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) + } + } - /// Scoped lets you start a number of tasks and waits - /// for all of them to complete before returning. - 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; - } - } + /// Scoped lets you start a number of tasks and waits + /// for all of them to complete before returning. + 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; + } + } - /// Scoped lets you start a number of tasks and waits - /// for all of them to complete before returning. - 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; - } - } + /// Scoped lets you start a number of tasks and waits + /// for all of them to complete before returning. + 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() - } + /// 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)) - } + /// 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, 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, 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 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, 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(); + /// 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"); - } - } + 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); - } + /// 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(); - } + /// 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() - } + /// 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() - } + /// 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); - } + /// 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() - } + /// 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 - } + #[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, - not_send: PhantomData, - }; - }; + /// 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, + not_send: PhantomData, + }; + }; - #[cfg(not(any(test, feature = "test-support")))] - let inner = { - let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); - platform_scheduler.foreground_executor() - }; + #[cfg(not(any(test, feature = "test-support")))] + let inner = { + let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); + platform_scheduler.foreground_executor() + }; - #[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(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); + } + }) + }; - Self { - inner, - dispatcher, - not_send: PhantomData, - } - } + Self { + inner, + dispatcher, + 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. + #[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) - } + /// 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) + } - /// Used by the test harness to run an async test in a synchronous fashion. - #[cfg(any(test, feature = "test-support"))] - #[track_caller] - pub fn block_test(&self, future: impl Future) -> R { - use std::cell::Cell; + /// Used by the test harness to run an async test in a synchronous fashion. + #[cfg(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 scheduler = self.inner.scheduler(); - let output = Cell::new(None); - let future = async { - output.set(Some(future.await)); - }; - let mut future = std::pin::pin!(future); + 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); + // 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") - } + output.take().expect("block_test future did not complete") + } - /// Block the current thread until the given future resolves. - /// Consider using `block_with_timeout` instead. - pub fn block_on(&self, future: impl Future) -> R { - self.inner.block_on(future) - } + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. + 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. - pub fn block_with_timeout>( - &self, - duration: Duration, - future: Fut, - ) -> Result + use> { - self.inner.block_with_timeout(duration, future) - } + /// Block the current thread until the given future resolves or the timeout elapses. + 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 dispatcher(&self) -> &Arc { + &self.dispatcher + } - #[doc(hidden)] - pub fn scheduler_executor(&self) -> SchedulerLocalExecutor { - self.inner.clone() - } + #[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`]. pub struct Scope<'a> { - executor: BackgroundExecutor, - priority: Priority, - futures: Vec + Send + 'static>>>, - tx: Option>, - rx: mpsc::Receiver<()>, - lifetime: PhantomData<&'a ()>, + executor: BackgroundExecutor, + priority: Priority, + futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, + lifetime: PhantomData<&'a ()>, } 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, - } - } + 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() - } + /// 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(); + /// 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); - } + // 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); + } } impl Drop for Scope<'_> { - fn drop(&mut self) { - self.tx.take().unwrap(); + 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); - } + // 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; + 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); + /// 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 = crate::http_client::FakeHttpClient::with_404_response(); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor); + let asset_source = Arc::new(()); + let http_client = crate::http_client::FakeHttpClient::with_404_response(); - let app = App::new_app(platform, asset_source, http_client); - (dispatcher, background_executor, app) - } + 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(); + #[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)); + 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(); + 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(); + // 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" - ); - } + // Task should have run + assert!( + *task_ran.borrow(), + "Task should run normally when app is alive" + ); + } } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 6b8c19a69d..5d5808aeaf 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -11,10 +11,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::borrow::Cow; use std::ops::Range; use std::{ - cmp::{self, PartialOrd}, - fmt::{self, Display}, - hash::Hash, - ops::{Add, Div, Mul, MulAssign, Neg, Sub}, + cmp::{self, PartialOrd}, + fmt::{self, Display}, + hash::Hash, + ops::{Add, Div, Mul, MulAssign, Neg, Sub}, }; use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; @@ -23,32 +23,32 @@ 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, + /// 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, - } - } + /// 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; + /// The unit associated with this type + type Unit; - /// Returns the unit along the given axis. - fn along(&self, axis: Axis) -> Self::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; + /// 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. @@ -64,29 +64,29 @@ pub trait Along { /// 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, + 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, + /// 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. @@ -109,294 +109,294 @@ pub struct Point { /// assert_eq!(p.y, 20); /// ``` pub const fn point(x: T, y: T) -> Point { - Point { x, y } + 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 } - } + /// 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()), - } - } + /// 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; + type Unit = T; - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.x.clone(), - Axis::Vertical => self.y.clone(), - } - } + 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()), - }, - } - } + 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), - } - } + /// 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() - } + /// 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() + } - /// Returns true if the difference between self and `other` is less than `epsilon` in both coordinates. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Pixels, Point}; - /// let p1 = Point { x: Pixels::from(1.0), y: Pixels::from(1.0) }; - /// let p2 = Point { x: Pixels::from(3.0), y: Pixels::from(-2.0) }; - /// assert_eq!(p1.is_nearly_eq(&p2, Pixels::from(4.0)), true); - /// ``` - pub fn is_nearly_eq(&self, other: &Self, epsilon: Pixels) -> bool { - let diff = *self - *other; - diff.x.abs() < epsilon && diff.y.abs() < epsilon - } + /// Returns true if the difference between self and `other` is less than `epsilon` in both coordinates. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Pixels, Point}; + /// let p1 = Point { x: Pixels::from(1.0), y: Pixels::from(1.0) }; + /// let p2 = Point { x: Pixels::from(3.0), y: Pixels::from(-2.0) }; + /// assert_eq!(p1.is_nearly_eq(&p2, Pixels::from(4.0)), true); + /// ``` + pub fn is_nearly_eq(&self, other: &Self, epsilon: Pixels) -> bool { + let diff = *self - *other; + diff.x.abs() < epsilon && diff.y.abs() < epsilon + } } impl Point where - T: Sub + Clone + Debug + Default + PartialEq, + 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(), - ) - } + /// 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, + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug, { - type Output = Point; + type Output = Point; - fn mul(self, rhs: Rhs) -> Self::Output { - Point { - x: self.x * rhs.clone(), - y: self.y * rhs, - } - } + 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, + 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; - } + 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, + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, { - type Output = Self; + type Output = Self; - fn div(self, rhs: S) -> Self::Output { - Self { - x: self.x / rhs.clone(), - y: self.y / rhs, - } - } + 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, + 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 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() - }, - } - } + /// 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) - } + /// 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(), - } - } + 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) - } + 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. @@ -404,22 +404,22 @@ impl Display for Point { /// 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, + 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, + /// 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) - } + /// 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. @@ -439,280 +439,280 @@ impl Size { /// ``` pub const fn size(width: T, height: T) -> Size where - T: Clone + Debug + Default + PartialEq, + T: Clone + Debug + Default + PartialEq, { - Size { width, height } + Size { width, height } } impl Size where - T: Clone + Debug + Default + PartialEq, + 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()), - } - } + /// 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, + 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(), - } - } + /// 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), - } - } + /// 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, + T: Clone + Debug + Default + PartialEq, { - type Unit = T; + type Unit = T; - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.width.clone(), - Axis::Vertical => self.height.clone(), - } - } + 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()), - }, - } - } + /// 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, + 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 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() - }, - } - } + /// 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, + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug + Default + PartialEq, { - type Output = Size; + type Output = Size; - fn mul(self, rhs: Rhs) -> Self::Output { - Size { - width: self.width * rhs.clone(), - height: self.height * rhs, - } - } + 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, + 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; - } + 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, + T: Clone + Debug + Default + PartialEq, { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) - } + 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) - } + 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, - } - } + 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(), - } - } + 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(), - } - } + 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(), - } - } + /// 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, - } - } + /// 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. @@ -736,889 +736,889 @@ impl Size { #[refineable(Debug)] #[repr(C)] pub struct Bounds { - /// The origin point of this area. - pub origin: Point, - /// The size of the rectangle. - pub size: Size, + /// 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, + origin: Point, + size: Size, ) -> Bounds { - Bounds { origin, size } + 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()); + /// 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, - }) - } + 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()); + /// 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.)), - }) - } + 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, + 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 } - } + /// 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, + 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 } - } + /// 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, + 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(), - }, - }; + /// 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 } - } + Bounds { origin, size } + } } impl Bounds where - T: Sub + Half + Clone + Debug + Default + PartialEq, + 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) - } + /// 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, + 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 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 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 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(), - } - } + /// 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, + 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(); + /// 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 - } + 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, + 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(), - } - } + /// 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, + 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() - } + /// 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, + 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), - } - } + /// 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, - ), - } - } + /// 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, + 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) - } + /// 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 + 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) - } + /// 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) - } + /// 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, + 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(), - } - } + /// 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, + T: Mul + Clone + Debug + Default + PartialEq, + Point: Mul>, + Rhs: Clone + Debug + Default + PartialEq, { - type Output = Bounds; + type Output = Bounds; - fn mul(self, rhs: Rhs) -> Self::Output { - Bounds { - origin: self.origin * rhs.clone(), - size: self.size * rhs, - } - } + 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, + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, { - fn mul_assign(&mut self, rhs: S) { - self.origin *= rhs.clone(); - self.size *= rhs; - } + 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, + Size: Div>, + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, { - type Output = Self; + type Output = Self; - fn div(self, rhs: S) -> Self { - Self { - origin: self.origin / rhs.clone(), - size: self.size / rhs, - } - } + 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, + T: Add + Clone + Debug + Default + PartialEq, { - type Output = Self; + type Output = Self; - fn add(self, rhs: Point) -> Self { - Self { - origin: self.origin + rhs, - size: self.size, - } - } + 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, + T: Sub + Clone + Debug + Default + PartialEq, { - type Output = Self; + type Output = Self; - fn sub(self, rhs: Point) -> Self { - Self { - origin: self.origin - rhs, - size: self.size, - } - } + 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, - } - } + fn from(size: Size) -> Self { + Self { + x: size.width, + y: size.height, + } + } } impl Bounds where - T: Add + Clone + Debug + Default + PartialEq, + 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 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 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 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 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 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 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(), - } - } + /// 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, + 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(), - } - } + /// 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, + 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 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()) - } + /// 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 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_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), - } - } + /// 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, + 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)) - } + /// 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. @@ -1627,116 +1627,116 @@ where /// /// 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() - } + /// 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 - ) - } + 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), - ) - } + /// 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), - ) - } + /// 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), - } - } + /// 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), - } - } + /// 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), - } - } + /// 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. @@ -1763,505 +1763,505 @@ impl Bounds { #[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, + /// 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, + T: Mul + Clone + Debug + Default + PartialEq, { - type Output = Self; + 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, - } - } + 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, + 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; - } + 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, - } - } + /// 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), - } - } + /// 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) - } + /// 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 `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(), - } - } + /// 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(), - } - } + /// 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), - } - } + /// 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(), - } - } + /// 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), - } - } + /// 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), - } - } + /// 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) - } + /// 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() - } + 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, - } - } + 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, + /// 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 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 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 - ) - } + /// 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. @@ -2271,383 +2271,383 @@ impl Anchor { #[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, + /// 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, + 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, - } - } + /// 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(), - } - } + /// 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), - } - } + /// 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), - } - } + /// 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) - } + /// 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), - } - } + /// 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), - } - } + /// 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, + T: Mul + Clone + Debug + Default + PartialEq, { - type Output = Self; + 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, - } - } + 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, + 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; - } + 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(), - } - } + 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, - } - } + 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, + 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) + Radians(value) } /// A type representing a percentage value. #[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - Debug, + 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) + 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) - } + 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. @@ -2673,300 +2673,300 @@ impl From for Radians { /// assert_eq!(scaled_length, ScaledPixels::from(20.0)); /// ``` #[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - JsonSchema, + 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; + type Output = f32; - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } + 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); - } + 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; - } + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } } impl std::ops::Rem for Pixels { - type Output = Self; + type Output = Self; - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } } impl Mul for Pixels { - type Output = Self; + type Output = Self; - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } } impl Mul for f32 { - type Output = Pixels; + type Output = Pixels; - fn mul(self, rhs: Pixels) -> Self::Output { - rhs * self - } + fn mul(self, rhs: Pixels) -> Self::Output { + rhs * self + } } impl Mul for Pixels { - type Output = Self; + type Output = Self; - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } } impl Mul for usize { - type Output = Pixels; + type Output = Pixels; - fn mul(self, rhs: Pixels) -> Pixels { - rhs * self - } + fn mul(self, rhs: Pixels) -> Pixels { + rhs * self + } } impl MulAssign for Pixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } + 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) - } + 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) - } + 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) - } + 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) - } + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |a, b| a + *b) + } } impl TryFrom<&'_ str> for Pixels { - type Error = anyhow::Error; + 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) - } + 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); + /// 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 - } + /// 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()) - } + /// 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()) - } + /// 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()) - } + /// 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) - } + /// 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)) - } + /// 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 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 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 - } + /// 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)) - } + 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) - } + 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); - } + 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) - } + fn from(pixels: f64) -> Self { + Pixels(pixels as f32) + } } impl From for Pixels { - fn from(pixels: f32) -> Self { - Pixels(pixels) - } + fn from(pixels: f32) -> Self { + Pixels(pixels) + } } impl From for f32 { - fn from(pixels: Pixels) -> Self { - pixels.0 - } + fn from(pixels: Pixels) -> Self { + pixels.0 + } } impl From<&Pixels> for f32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 - } + fn from(pixels: &Pixels) -> Self { + pixels.0 + } } impl From for f64 { - fn from(pixels: Pixels) -> Self { - pixels.0 as f64 - } + fn from(pixels: Pixels) -> Self { + pixels.0 as f64 + } } impl From for u32 { - fn from(pixels: Pixels) -> Self { - pixels.0 as u32 - } + fn from(pixels: Pixels) -> Self { + pixels.0 as u32 + } } impl From<&Pixels> for u32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 as u32 - } + fn from(pixels: &Pixels) -> Self { + pixels.0 as u32 + } } impl From for Pixels { - fn from(pixels: u32) -> Self { - Pixels(pixels as f32) - } + fn from(pixels: u32) -> Self { + Pixels(pixels as f32) + } } impl From for usize { - fn from(pixels: Pixels) -> Self { - pixels.0 as usize - } + fn from(pixels: Pixels) -> Self { + pixels.0 as usize + } } impl From for Pixels { - fn from(pixels: usize) -> Self { - Pixels(pixels as f32) - } + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } } /// Represents physical pixels on the display. @@ -2977,105 +2977,105 @@ impl From for Pixels { /// 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, + 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 - } + /// 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) - } + 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 - } + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 + } } impl From for DevicePixels { - fn from(device_pixels: i32) -> Self { - DevicePixels(device_pixels) - } + fn from(device_pixels: i32) -> Self { + DevicePixels(device_pixels) + } } impl From for DevicePixels { - fn from(device_pixels: u32) -> Self { - DevicePixels(device_pixels as i32) - } + 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 - } + 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 - } + 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) - } + 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 - } + 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) - } + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } } /// Represents scaled pixels that take into account the device's scale factor. @@ -3090,153 +3090,153 @@ impl From for DevicePixels { pub struct ScaledPixels(pub f32); impl ScaledPixels { - /// Returns the raw `f32` value of this `ScaledPixels`. - pub fn as_f32(self) -> f32 { - self.0 - } + /// 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()) - } + /// 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()) - } + /// 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()) - } + /// 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)) - } + 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) - } + 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) - } + 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) - } + 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) - } + 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 - } + fn from(scaled_pixels: ScaledPixels) -> Self { + scaled_pixels.0 as f64 + } } impl From for u32 { - fn from(pixels: ScaledPixels) -> Self { - pixels.0 as u32 - } + fn from(pixels: ScaledPixels) -> Self { + pixels.0 as u32 + } } impl From for ScaledPixels { - fn from(pixels: f32) -> Self { - Self(pixels) - } + fn from(pixels: f32) -> Self { + Self(pixels) + } } impl Div for ScaledPixels { - type Output = f32; + type Output = f32; - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } + 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); - } + 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; - } + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } } impl std::ops::Rem for ScaledPixels { - type Output = Self; + type Output = Self; - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } } impl Mul for ScaledPixels { - type Output = Self; + type Output = Self; - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } } impl Mul for f32 { - type Output = ScaledPixels; + type Output = ScaledPixels; - fn mul(self, rhs: ScaledPixels) -> Self::Output { - rhs * self - } + fn mul(self, rhs: ScaledPixels) -> Self::Output { + rhs * self + } } impl Mul for ScaledPixels { - type Output = Self; + type Output = Self; - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } } impl Mul for usize { - type Output = ScaledPixels; + type Output = ScaledPixels; - fn mul(self, rhs: ScaledPixels) -> ScaledPixels { - rhs * self - } + fn mul(self, rhs: ScaledPixels) -> ScaledPixels { + rhs * self + } } impl MulAssign for ScaledPixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } + 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]. @@ -3253,42 +3253,42 @@ impl MulAssign for ScaledPixels { pub struct Rems(pub f32); impl Rems { - /// Convert this Rem value to pixels. - pub fn to_pixels(self, rem_size: Pixels) -> Pixels { - self * rem_size - } + /// Convert this Rem value to pixels. + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size + } } impl Mul for Rems { - type Output = Pixels; + type Output = Pixels; - fn mul(self, other: Pixels) -> Pixels { - Pixels(self.0 * other.0) - } + fn mul(self, other: Pixels) -> Pixels { + Pixels(self.0 * other.0) + } } impl Display for Rems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}rem", self.0) - } + 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) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } } impl TryFrom<&'_ str> for Rems { - type Error = anyhow::Error; + 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) - } + 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. @@ -3299,159 +3299,159 @@ impl TryFrom<&'_ str> for Rems { /// 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), + /// 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, - } - } + /// 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) - } + fn from(pixels: Pixels) -> Self { + AbsoluteLength::Pixels(pixels) + } } impl From for AbsoluteLength { - fn from(rems: Rems) -> Self { - AbsoluteLength::Rems(rems) - } + 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 `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, - } - } + /// 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() - } + 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}"), - } - } + 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) - } + 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; + 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}" - )) - } - } + 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 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)$" - }) - } + 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; + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; - impl de::Visitor<'_> for StringVisitor { - type Value = AbsoluteLength; + impl de::Visitor<'_> for StringVisitor { + type Value = AbsoluteLength; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_ABSOLUTE_LENGTH}") - } + 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) - } - } + fn visit_str(self, value: &str) -> Result { + AbsoluteLength::try_from(value).map_err(E::custom) + } + } - deserializer.deserialize_str(StringVisitor) - } + deserializer.deserialize_str(StringVisitor) + } } impl Serialize for AbsoluteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } + 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. @@ -3461,236 +3461,236 @@ impl Serialize for AbsoluteLength { /// 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), + /// 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, - }, - } - } + /// 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) - } + 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), - } - } + 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; + 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}" - )) - } - } + 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 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|%)$" - }) - } + 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; + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; - impl de::Visitor<'_> for StringVisitor { - type Value = DefiniteLength; + impl de::Visitor<'_> for StringVisitor { + type Value = DefiniteLength; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_DEFINITE_LENGTH}") - } + 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) - } - } + fn visit_str(self, value: &str) -> Result { + DefiniteLength::try_from(value).map_err(E::custom) + } + } - deserializer.deserialize_str(StringVisitor) - } + deserializer.deserialize_str(StringVisitor) + } } impl Serialize for DefiniteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } + 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()) - } + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels.into()) + } } impl From for DefiniteLength { - fn from(rems: Rems) -> Self { - Self::Absolute(rems.into()) - } + fn from(rems: Rems) -> Self { + Self::Absolute(rems.into()) + } } impl From for DefiniteLength { - fn from(length: AbsoluteLength) -> Self { - Self::Absolute(length) - } + fn from(length: AbsoluteLength) -> Self { + Self::Absolute(length) + } } impl Default for DefiniteLength { - fn default() -> Self { - Self::Absolute(AbsoluteLength::default()) - } + 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, + /// 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) - } + 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"), - } - } + 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; + 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}" - )) - } - } + 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 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|%))$" - }) - } + 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; + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; - impl de::Visitor<'_> for StringVisitor { - type Value = Length; + impl de::Visitor<'_> for StringVisitor { + type Value = Length; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_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) - } - } + fn visit_str(self, value: &str) -> Result { + Length::try_from(value).map_err(E::custom) + } + } - deserializer.deserialize_str(StringVisitor) - } + deserializer.deserialize_str(StringVisitor) + } } impl Serialize for Length { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } + 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. @@ -3706,12 +3706,12 @@ impl Serialize for Length { /// /// A `DefiniteLength` representing the relative length as a fraction of the parent's size. pub const fn relative(fraction: f32) -> DefiniteLength { - DefiniteLength::Fraction(fraction) + 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) + relative(1.618_034) } /// Constructs a `Rems` value representing a length in rems. @@ -3724,7 +3724,7 @@ pub const fn phi() -> DefiniteLength { /// /// A `Rems` representing the specified number of rems. pub const fn rems(rems: f32) -> Rems { - Rems(rems) + Rems(rems) } /// Constructs a `Pixels` value representing a length in pixels. @@ -3737,7 +3737,7 @@ pub const fn rems(rems: f32) -> Rems { /// /// A `Pixels` representing the specified number of pixels. pub const fn px(pixels: f32) -> Pixels { - Pixels(pixels) + Pixels(pixels) } /// Returns a `Length` representing an automatic length. @@ -3752,74 +3752,74 @@ pub const fn px(pixels: f32) -> Pixels { /// /// A `Length` variant set to `Auto`. pub const fn auto() -> Length { - Length::Auto + Length::Auto } impl From for Length { - fn from(pixels: Pixels) -> Self { - Self::Definite(pixels.into()) - } + fn from(pixels: Pixels) -> Self { + Self::Definite(pixels.into()) + } } impl From for Length { - fn from(rems: Rems) -> Self { - Self::Definite(rems.into()) - } + fn from(rems: Rems) -> Self { + Self::Definite(rems.into()) + } } impl From for Length { - fn from(length: DefiniteLength) -> Self { - Self::Definite(length) - } + fn from(length: DefiniteLength) -> Self { + Self::Definite(length) + } } impl From for Length { - fn from(length: AbsoluteLength) -> Self { - Self::Definite(length.into()) - } + fn from(length: AbsoluteLength) -> Self { + Self::Definite(length.into()) + } } impl Default for Length { - fn default() -> Self { - Self::Definite(DefiniteLength::default()) - } + fn default() -> Self { + Self::Definite(DefiniteLength::default()) + } } impl From<()> for Length { - fn from(_: ()) -> Self { - Self::Definite(DefiniteLength::default()) - } + 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 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, + /// 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, - } - } + 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. @@ -3828,48 +3828,48 @@ impl From for taffy::GridPlacement { /// 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; + /// 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 - } + fn half(&self) -> Self { + self / 2 + } } impl Half for f32 { - fn half(&self) -> Self { - self / 2. - } + fn half(&self) -> Self { + self / 2. + } } impl Half for DevicePixels { - fn half(&self) -> Self { - Self(self.0 / 2) - } + fn half(&self) -> Self { + Self(self.0 / 2) + } } impl Half for ScaledPixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } + fn half(&self) -> Self { + Self(self.0 / 2.) + } } impl Half for Pixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } + fn half(&self) -> Self { + Self(self.0 / 2.) + } } impl Half for Rems { - fn half(&self) -> Self { - Self(self.0 / 2.) - } + fn half(&self) -> Self { + Self(self.0 / 2.) + } } /// A trait for checking if a value is zero. @@ -3879,133 +3879,133 @@ impl Half for Rems { /// 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; + /// 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 - } + fn is_zero(&self) -> bool { + self.0 == 0 + } } impl IsZero for ScaledPixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } + fn is_zero(&self) -> bool { + self.0 == 0. + } } impl IsZero for Pixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } + fn is_zero(&self) -> bool { + self.0 == 0. + } } impl IsZero for Rems { - fn is_zero(&self) -> bool { - self.0 == 0. - } + 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(), - } - } + 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., - } - } + 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, - } - } + 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() - } + fn is_zero(&self) -> bool { + self.x.is_zero() && self.y.is_zero() + } } impl IsZero for Size where - T: IsZero + Clone + Debug + Default + PartialEq, + T: IsZero + Clone + Debug + Default + PartialEq, { - fn is_zero(&self) -> bool { - self.width.is_zero() || self.height.is_zero() - } + 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() - } + fn is_zero(&self) -> bool { + self.size.is_zero() + } } impl IsZero for Corners where - T: IsZero + Clone + Debug + Default + PartialEq, + 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() - } + 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::*; + 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] + 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 1: Intersecting bounds + assert!(bounds1.intersects(&bounds2)); - // Test Case 2: Non-Intersecting bounds - assert!(!bounds1.intersects(&bounds3)); + // Test Case 2: Non-Intersecting bounds + assert!(!bounds1.intersects(&bounds3)); - // Test Case 3: Bounds intersecting with themselves - assert!(bounds1.intersects(&bounds1)); - } + // Test Case 3: Bounds intersecting with themselves + assert!(bounds1.intersects(&bounds1)); + } } diff --git a/crates/gpui/src/global.rs b/crates/gpui/src/global.rs index a16934c129..54d4ea420c 100644 --- a/crates/gpui/src/global.rs +++ b/crates/gpui/src/global.rs @@ -20,56 +20,56 @@ use crate::{App, BorrowAppContext}; /// 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`. + // 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; + /// 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::() - } + 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; + /// 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; + /// 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) - } + #[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) - } + fn set_global(cx: &mut C, global: Self) + where + C: BorrowAppContext, + { + cx.set_global(global) + } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 22be81f73f..8dff667d1d 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -22,11 +22,11 @@ pub mod colors; pub mod dock; /// Linear effect-stack inspector widget (companion to [`node_graph`]). pub mod effect_stack; -/// Minimal localization hook for widget-baked strings. -pub mod i18n; mod element; mod elements; mod executor; +/// Minimal localization hook for widget-baked strings. +pub mod i18n; mod platform_scheduler; pub(crate) use platform_scheduler::PlatformScheduler; mod geometry; @@ -76,17 +76,17 @@ 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; + 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 {} + /// A mechanism for restricting implementations of a trait to only those in GPUI. + /// See: + pub trait Sealed {} } pub use accesskit; @@ -106,7 +106,7 @@ pub use executor::*; pub use geometry::*; pub use global::*; pub use gpui_macros::{ - AppContext, IntoElement, Render, VisualContext, property_test, register_action, test, + AppContext, IntoElement, Render, VisualContext, property_test, register_action, test, }; pub use gpui_shared_string::*; pub use gpui_util::arc_cow::ArcCow; @@ -147,78 +147,78 @@ 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; + /// 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; + /// 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; + /// 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 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; + /// 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; + /// 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; + /// 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; + /// 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; + /// 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; + /// 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; + /// 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]. @@ -226,46 +226,46 @@ pub trait AppContext { 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() - } + /// 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; + /// The result type for window operations. + type Result; - /// Returns the handle of the window associated with this context. - fn window_handle(&self) -> AnyWindowHandle; + /// 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; + /// 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>; + /// 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; + /// 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; + /// 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 @@ -275,55 +275,55 @@ 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; + /// 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, + C: std::borrow::BorrowMut, { - fn set_global(&mut self, global: G) { - self.borrow_mut().set_global(global) - } + 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 - } + #[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) - } + 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, + /// 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/src/http_client.rs b/crates/gpui/src/http_client.rs index 15ee0358ed..b99918fb61 100644 --- a/crates/gpui/src/http_client.rs +++ b/crates/gpui/src/http_client.rs @@ -3,103 +3,103 @@ use http::StatusCode; /// A simple HTTP response. pub struct HttpResponse { - /// The HTTP status code. - pub status: StatusCode, - /// The response body bytes. - pub body: Vec, + /// The HTTP status code. + pub status: StatusCode, + /// The response body bytes. + pub body: Vec, } /// A trait for making HTTP requests. pub trait HttpClient: 'static + Send + Sync { - /// Perform a GET request and return the full response. - fn get( - &self, - url: &str, - follow_redirects: bool, - ) -> BoxFuture<'static, anyhow::Result>; + /// Perform a GET request and return the full response. + fn get( + &self, + url: &str, + follow_redirects: bool, + ) -> BoxFuture<'static, anyhow::Result>; } /// An HTTP client that always returns an error. pub struct NullHttpClient; impl HttpClient for NullHttpClient { - fn get( - &self, - _url: &str, - _follow_redirects: bool, - ) -> BoxFuture<'static, anyhow::Result> { - Box::pin(async { anyhow::bail!("No HttpClient available") }) - } + fn get( + &self, + _url: &str, + _follow_redirects: bool, + ) -> BoxFuture<'static, anyhow::Result> { + Box::pin(async { anyhow::bail!("No HttpClient available") }) + } } /// An HTTP client that blocks all requests. pub struct BlockedHttpClient; impl BlockedHttpClient { - /// Create a new `BlockedHttpClient`. - pub fn new() -> Self { - Self - } + /// Create a new `BlockedHttpClient`. + pub fn new() -> Self { + Self + } } impl Default for BlockedHttpClient { - fn default() -> Self { - Self - } + fn default() -> Self { + Self + } } impl HttpClient for BlockedHttpClient { - fn get( - &self, - _url: &str, - _follow_redirects: bool, - ) -> BoxFuture<'static, anyhow::Result> { - Box::pin(async { - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "BlockedHttpClient disallowed request", - ) - .into()) - }) - } + fn get( + &self, + _url: &str, + _follow_redirects: bool, + ) -> BoxFuture<'static, anyhow::Result> { + Box::pin(async { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "BlockedHttpClient disallowed request", + ) + .into()) + }) + } } /// A fake HTTP client for testing. #[cfg(any(test, feature = "test-support"))] pub struct FakeHttpClient { - status: StatusCode, + status: StatusCode, } #[cfg(any(test, feature = "test-support"))] impl FakeHttpClient { - /// Create a fake client that returns 404 responses. - pub fn with_404_response() -> std::sync::Arc { - std::sync::Arc::new(Self { - status: StatusCode::NOT_FOUND, - }) - } + /// Create a fake client that returns 404 responses. + pub fn with_404_response() -> std::sync::Arc { + std::sync::Arc::new(Self { + status: StatusCode::NOT_FOUND, + }) + } - /// Create a fake client that returns 200 responses. - pub fn with_200_response() -> std::sync::Arc { - std::sync::Arc::new(Self { - status: StatusCode::OK, - }) - } + /// Create a fake client that returns 200 responses. + pub fn with_200_response() -> std::sync::Arc { + std::sync::Arc::new(Self { + status: StatusCode::OK, + }) + } } #[cfg(any(test, feature = "test-support"))] impl HttpClient for FakeHttpClient { - fn get( - &self, - _url: &str, - _follow_redirects: bool, - ) -> BoxFuture<'static, anyhow::Result> { - let status = self.status; - Box::pin(async move { - Ok(HttpResponse { - status, - body: Vec::new(), - }) - }) - } + fn get( + &self, + _url: &str, + _follow_redirects: bool, + ) -> BoxFuture<'static, anyhow::Result> { + let status = self.status; + Box::pin(async move { + Ok(HttpResponse { + status, + body: Vec::new(), + }) + }) + } } diff --git a/crates/gpui/src/i18n.rs b/crates/gpui/src/i18n.rs index e120000a88..8de31f93f2 100644 --- a/crates/gpui/src/i18n.rs +++ b/crates/gpui/src/i18n.rs @@ -31,56 +31,56 @@ pub type StringTable = HashMap; /// The installed table, or `None` (built-in defaults) when unset. fn table() -> &'static RwLock> { - static TABLE: OnceLock>> = OnceLock::new(); - TABLE.get_or_init(|| RwLock::new(None)) + static TABLE: OnceLock>> = OnceLock::new(); + TABLE.get_or_init(|| RwLock::new(None)) } /// Installs `strings` as the string-table override, replacing any previously /// installed table wholesale. pub fn set_table(strings: StringTable) { - *table().write().unwrap() = Some(strings); + *table().write().unwrap() = Some(strings); } /// Removes the override so every string falls back to its built-in default. pub fn clear_table() { - *table().write().unwrap() = None; + *table().write().unwrap() = None; } /// Returns the localized string for `key`, or `default` when the installed /// table has no entry for it. pub fn tr(key: &str, default: impl Into) -> SharedString { - if let Some(value) = table() - .read() - .unwrap() - .as_ref() - .and_then(|strings| strings.get(key)) - { - SharedString::from(value.clone()) - } else { - default.into() - } + if let Some(value) = table() + .read() + .unwrap() + .as_ref() + .and_then(|strings| strings.get(key)) + { + SharedString::from(value.clone()) + } else { + default.into() + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn missing_table_uses_defaults() { - clear_table(); - assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); - assert_eq!(tr("viewer.zoom", "缩放"), "缩放"); - } + #[test] + fn missing_table_uses_defaults() { + clear_table(); + assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); + assert_eq!(tr("viewer.zoom", "缩放"), "缩放"); + } - #[test] - fn installed_table_overrides_defaults() { - let mut table = StringTable::new(); - table.insert("viewer.safe_frames".into(), "Safe Frames".into()); - set_table(table); - assert_eq!(tr("viewer.safe_frames", "安全框"), "Safe Frames"); - // Keys not in the table keep their defaults. - assert_eq!(tr("viewer.zoom", "缩放"), "缩放"); - clear_table(); - assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); - } + #[test] + fn installed_table_overrides_defaults() { + let mut table = StringTable::new(); + table.insert("viewer.safe_frames".into(), "Safe Frames".into()); + set_table(table); + assert_eq!(tr("viewer.safe_frames", "安全框"), "Safe Frames"); + // Keys not in the table keep their defaults. + assert_eq!(tr("viewer.zoom", "缩放"), "缩放"); + clear_table(); + assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); + } } diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 10ca46501d..ac7457d482 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -8,188 +8,188 @@ use std::ops::Range; /// /// 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::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::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::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::unmark_text`] for details + fn unmark_text(&mut self, window: &mut Window, cx: &mut Context); - /// 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_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::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::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::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::accepts_text_input`] for details - fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { - true - } + /// See [`InputHandler::accepts_text_input`] for details + fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { + true + } } /// 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, + 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, - } - } + /// 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 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 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 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_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 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 unmark_text(&mut self, window: &mut Window, cx: &mut App) { + self.view + .update(cx, |view, cx| view.unmark_text(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 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 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 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 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 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)) + } } diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index ad3ba6a4b6..1e0a976713 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -1,18 +1,18 @@ /// 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, + /// 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() - } + fn into(self) -> InspectorElementId { + self.clone() + } } #[cfg(any(feature = "inspector", debug_assertions))] @@ -20,235 +20,235 @@ 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; - use std::any::{Any, TypeId}; + use super::*; + use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window}; + use collections::FxHashMap; + 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>, - } + /// `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 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() - } - } + impl Into for &InspectorElementPath { + fn into(self) -> InspectorElementPath { + self.clone() + } + } - /// Function set on `App` to render the inspector UI. - pub type InspectorRenderer = - Box) -> AnyElement>; + /// 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, - } + /// 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: FxHashMap>, - } + struct InspectedElement { + id: InspectorElementId, + states: FxHashMap>, + } - impl InspectedElement { - fn new(id: InspectorElementId) -> Self { - InspectedElement { - id, - states: FxHashMap::default(), - } - } - } + impl InspectedElement { + fn new(id: InspectorElementId) -> Self { + InspectedElement { + id, + states: FxHashMap::default(), + } + } + } - impl Inspector { - pub(crate) fn new() -> Self { - Self { - active_element: None, - pick_depth: Some(0.0), - } - } + 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 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 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 - } + 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) - } + /// 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); - }; + 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 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); + let result = f(&mut inspector_state, window); - if let Some(inspector_state) = inspector_state { - active_element - .states - .insert(type_id, Box::new(inspector_state)); - } + if let Some(inspector_state) = inspector_state { + active_element + .states + .insert(type_id, Box::new(inspector_state)); + } - result - } + 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); - } + /// 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() - } + /// 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); - } - } + /// 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); - } + self.active_element = Some(active_element); + } - elements - } - } + 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() - } - } - } + 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>, - >, - } + #[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() - }), - ); - } - } + 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; + 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, - } + /// 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") - } - } + 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/src/interactive.rs b/crates/gpui/src/interactive.rs index 0c7f2f9c97..ab735d60fd 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -1,14 +1,14 @@ use crate::{ - Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, - Window, point, seal::Sealed, + Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, + 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; + /// Convert this event into the platform input enum. + fn to_platform_input(self) -> PlatformInput; } /// A key event from the platform. @@ -23,451 +23,451 @@ 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, + /// The keystroke that was generated. + pub keystroke: Keystroke, - /// Whether the key is currently held down. - pub is_held: bool, + /// 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, + /// 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) - } + 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, + /// 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) - } + 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, + /// 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) - } + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ModifiersChanged(self) + } } impl KeyEvent for ModifiersChangedEvent {} impl Deref for ModifiersChangedEvent { - type Target = Modifiers; + type Target = Modifiers; - fn deref(&self) -> &Self::Target { - &self.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)] pub enum TouchPhase { - /// The touch started. - Started, - /// The touch event is moving. - #[default] - Moved, - /// The touch phase has ended - Ended, + /// The touch started. + Started, + /// The touch event is moving. + #[default] + Moved, + /// The touch phase has ended + Ended, } /// A mouse down event from the platform #[derive(Clone, Debug, Default)] pub struct MouseDownEvent { - /// Which mouse button was pressed. - pub button: MouseButton, + /// Which mouse button was pressed. + pub button: MouseButton, - /// The position of the mouse on the window. - pub position: Point, + /// 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 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, + /// The number of times the button has been clicked. + pub click_count: usize, - /// Whether this is the first, focusing click. - pub first_mouse: bool, + /// 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) - } + 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, - } - } + /// 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, + /// Which mouse button was released. + pub button: MouseButton, - /// The position of the mouse on the window. - pub position: Point, + /// 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 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, + /// 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) - } + 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, - } - } + /// 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 pressed. + pub down: MouseDownEvent, - /// The mouse event when the button was released. - pub up: MouseUpEvent, + /// 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, + /// 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, + /// 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) - } + 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 keyboard button that was pressed to trigger the click. + pub button: KeyboardButton, - /// The bounds of the element that was clicked. - pub bounds: Bounds, + /// The bounds of the element that was clicked. + pub bounds: Bounds, } /// A click event, generated when a mouse button or keyboard button is pressed and released. #[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 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), } impl Default for ClickEvent { - fn default() -> Self { - ClickEvent::Keyboard(KeyboardClickEvent::default()) - } + 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, - } - } + /// 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, + } + } - /// 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. - pub fn position(&self) -> Point { - match self { - ClickEvent::Keyboard(event) => event.bounds.bottom_left(), - ClickEvent::Mouse(event) => event.up.position, - } - } + /// 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. + pub fn position(&self) -> Point { + match self { + ClickEvent::Keyboard(event) => event.bounds.bottom_left(), + ClickEvent::Mouse(event) => event.up.position, + } + } - /// Returns the mouse position of the click event - /// - /// `Keyboard`: None - /// `Mouse`: The position of the mouse when the button was released. - pub fn mouse_position(&self) -> Option> { - match self { - ClickEvent::Keyboard(_) => None, - ClickEvent::Mouse(event) => Some(event.up.position), - } - } + /// Returns the mouse position of the click event + /// + /// `Keyboard`: None + /// `Mouse`: The position of the mouse when the button was released. + pub fn mouse_position(&self) -> Option> { + match self { + ClickEvent::Keyboard(_) => None, + ClickEvent::Mouse(event) => Some(event.up.position), + } + } - /// 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 - } - } - } + /// 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 + } + } + } - /// 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 - } - } - } + /// 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 + } + } + } - /// Returns whether the click was a standard click - /// - /// `Keyboard`: Always true - /// `Mouse`: Left button pressed and released - pub fn standard_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => true, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Left && event.up.button == MouseButton::Left - } - } - } + /// Returns whether the click was a standard click + /// + /// `Keyboard`: Always true + /// `Mouse`: Left button pressed and released + pub fn standard_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Left && event.up.button == MouseButton::Left + } + } + } - /// 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 - pub fn first_focus(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => event.down.first_mouse, - } - } + /// 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 + pub fn first_focus(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => event.down.first_mouse, + } + } - /// Returns the click count of the click event - /// - /// `Keyboard`: Always 1 - /// `Mouse`: Count of clicks from MouseUpEvent - pub fn click_count(&self) -> usize { - match self { - ClickEvent::Keyboard(_) => 1, - ClickEvent::Mouse(event) => event.up.click_count, - } - } + /// Returns the click count of the click event + /// + /// `Keyboard`: Always 1 + /// `Mouse`: Count of clicks from MouseUpEvent + pub fn click_count(&self) -> usize { + match self { + ClickEvent::Keyboard(_) => 1, + ClickEvent::Mouse(event) => event.up.click_count, + } + } - /// Returns whether the click event is generated by a keyboard event - pub fn is_keyboard(&self) -> bool { - match self { - ClickEvent::Mouse(_) => false, - ClickEvent::Keyboard(_) => true, - } - } + /// Returns whether the click event is generated by a keyboard event + pub fn is_keyboard(&self) -> bool { + match self { + ClickEvent::Mouse(_) => 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, + /// 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 left mouse button. + #[default] + Left, - /// The right mouse button. - Right, + /// The right mouse button. + Right, - /// The middle mouse button. - Middle, + /// The middle mouse button. + Middle, - /// A navigation button, such as back or forward. - Navigate(NavigationDirection), + /// 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), - ] - } + /// 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 back button. + #[default] + Back, - /// The forward button. - Forward, + /// 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 position of the mouse on the window. + pub position: Point, - /// The mouse button that was pressed, if any. - pub pressed_button: Option, + /// 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, + /// 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) - } + 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) - } + /// 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 position of the mouse on the window. + pub position: Point, - /// The change in scroll wheel position for this event. - pub delta: ScrollDelta, + /// 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 modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, - /// The phase of the touch event. - pub touch_phase: TouchPhase, + /// 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) - } + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ScrollWheel(self) + } } impl MouseEvent for ScrollWheelEvent {} impl Deref for ScrollWheelEvent { - type Target = Modifiers; + type Target = Modifiers; - fn deref(&self) -> &Self::Target { - &self.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), + /// 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()) - } + fn default() -> Self { + Self::Lines(Default::default()) + } } /// A pinch gesture event from the platform, generated when the user performs @@ -475,124 +475,124 @@ impl Default for ScrollDelta { /// #[derive(Clone, Debug, Default)] pub struct PinchEvent { - /// The position of the pinch center on the window. - pub position: Point, + /// 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 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 modifiers that were held down during the pinch gesture. + pub modifiers: Modifiers, - /// The phase of the pinch gesture. - pub phase: TouchPhase, + /// 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) - } + fn to_platform_input(self) -> PlatformInput { + PlatformInput::Pinch(self) + } } impl GestureEvent for PinchEvent {} impl MouseEvent for PinchEvent {} impl Deref for PinchEvent { - type Target = Modifiers; + type Target = Modifiers; - fn deref(&self) -> &Self::Target { - &self.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, - } - } + /// 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), - } - } + /// 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 - }; + /// 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 - }; + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; - ScrollDelta::Pixels(point(x, 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 - }; + (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 - }; + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; - ScrollDelta::Lines(point(x, y)) - } + ScrollDelta::Lines(point(x, y)) + } - _ => other, - } - } + _ => 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, + /// 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) - } + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseExited(self) + } } impl MouseEvent for MouseExitEvent {} impl Deref for MouseExitEvent { - type Target = Modifiers; + type Target = Modifiers; - fn deref(&self) -> &Self::Target { - &self.modifiers - } + fn deref(&self) -> &Self::Target { + &self.modifiers + } } /// A collection of paths from the platform, such as from a file drop. @@ -600,182 +600,182 @@ impl Deref for MouseExitEvent { pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>); impl ExternalPaths { - /// Convert this collection of paths into a slice. - pub fn paths(&self) -> &[PathBuf] { - &self.0 - } + /// Convert this collection of paths into a slice. + pub fn paths(&self) -> &[PathBuf] { + &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 - } + 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 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, } impl Sealed for FileDropEvent {} impl InputEvent for FileDropEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::FileDrop(self) - } + 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), - /// Files were dragged and dropped onto the window. - FileDrop(FileDropEvent), + /// 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), + /// Files were dragged and dropped onto the window. + FileDrop(FileDropEvent), } 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::FileDrop(event) => Some(event), - } - } + 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::FileDrop(event) => Some(event), + } + } - 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::FileDrop(_) => 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::FileDrop(_) => None, + } + } } #[cfg(test)] mod test { - use crate::{ - self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, - KeyBinding, Keystroke, ParentElement, Render, TestAppContext, Window, div, - }; + use crate::{ + self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, + KeyBinding, Keystroke, ParentElement, Render, TestAppContext, Window, div, + }; - struct TestView { - saw_key_down: bool, - saw_action: bool, - focus_handle: FocusHandle, - } + struct TestView { + saw_key_down: bool, + saw_action: bool, + focus_handle: FocusHandle, + } - actions!(test_only, [TestAction]); + 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(), - ), - ) - } - } + 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() - }); + #[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"))]); - }); + 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(); + 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()); + 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(); - } + 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(); + } } diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index fee75d5dad..b773733c98 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -50,17 +50,17 @@ //! KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) use crate::{ - Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, - Keystroke, ModifiersChangedEvent, Window, + 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, + 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 @@ -69,62 +69,62 @@ use std::{ 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, + 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 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, + 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 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 - } + 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]>, + 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, + 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; @@ -132,1004 +132,1004 @@ type ModifiersChangedListener = Rc, + 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 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 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 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()); + 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 - } + 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(); - } + 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); + 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; - } + 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(); - } - } + 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_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_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 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(); - } + 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); - } + 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); - } + 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(); + 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(); - } - } + 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); - } + 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(); - } + while !source_stack.is_empty() { + source_stack.pop(); + self.pop_node(); + } - ReusedSubtree { - old_range, - new_range, - contains_focus, - } - } + 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); - } + 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); - } + 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_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_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 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; - } + 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 - } + 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 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 - } + 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 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() - } + /// 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 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(); + 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) - } + 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) - } + /// 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); + /// 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(); + 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 (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 - } + 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 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)) - } + if !suffix.is_empty() { + to_replay.extend(self.flush_dispatch(suffix, dispatch_path)) + } - to_replay - } + 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) - } + /// 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 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 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(); + 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) - } + 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] - } + 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] - } + 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 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 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() - } + 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::RefCell, ops::Range, rc::Rc}; - - use crate::{ - ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, IntoElement, - KeyBinding, KeyContext, Keymap, Pixels, Point, Render, Subscription, TestAppContext, - UTF16Selection, Unbind, 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), - ) - } - - #[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))); - } - - #[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(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"))]); - }); - - 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()); - }); - } - - #[crate::test] - fn test_input_handler_pending(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"))]); - }); - let (test, cx) = cx.add_window_view(|_, cx| CustomElement::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("ctrl-b ["); - test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) - } + use crate::{ + self as gpui, AppContext, DispatchResult, Element, ElementId, GlobalElementId, + InspectorElementId, Keystroke, LayoutId, Style, + }; + use core::panic; + use smallvec::SmallVec; + use std::{cell::RefCell, ops::Range, rc::Rc}; + + use crate::{ + ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, IntoElement, + KeyBinding, KeyContext, Keymap, Pixels, Point, Render, Subscription, TestAppContext, + UTF16Selection, Unbind, 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), + ) + } + + #[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))); + } + + #[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(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"))]); + }); + + 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()); + }); + } + + #[crate::test] + fn test_input_handler_pending(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"))]); + }); + let (test, cx) = cx.add_window_view(|_, cx| CustomElement::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("ctrl-b ["); + test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) + } } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b10cf39362..5093021855 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -19,10 +19,10 @@ 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: HashMap>, - disabled_binding_indices: Vec, - version: KeymapVersion, + bindings: Vec, + binding_indices_by_action_id: HashMap>, + disabled_binding_indices: Vec, + version: KeymapVersion, } /// Index of a binding within a keymap. @@ -30,831 +30,831 @@ pub struct Keymap { 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), - } + 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()) + 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 - } + /// 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 - } + /// 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); - } + /// 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; - } + 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; - } + /// 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, 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(); + /// 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; - } + 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; - } + 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; - } - } - } + 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) - }) - } + 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 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 user has disabled a binding 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. - 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(); + /// 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 user has disabled a binding 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. + 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; - }; + 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)); - } - } + 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)) - }); + 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(); + let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); + let mut first_binding_index = None; + let mut unbound_bindings: Vec<&KeyBinding> = Vec::new(); - for (_, ix, binding) in matched_bindings { - if is_no_action(&*binding.action) { - // Only break if this is a user-defined NoAction binding - // This allows user keymaps to override base keymap NoAction bindings - if let Some(meta) = binding.meta { - if meta.0 == 0 { - break; - } - } else { - // If no meta is set, assume it's a user binding for safety - break; - } - // For non-user NoAction bindings, continue searching for user overrides - continue; - } + for (_, ix, binding) in matched_bindings { + if is_no_action(&*binding.action) { + // Only break if this is a user-defined NoAction binding + // This allows user keymaps to override base keymap NoAction bindings + if let Some(meta) = binding.meta { + if meta.0 == 0 { + break; + } + } else { + // If no meta is set, assume it's a user binding for safety + break; + } + // For non-user NoAction bindings, continue searching for user overrides + continue; + } - if is_unbind(&*binding.action) { - unbound_bindings.push(binding); - 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; - } + 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); - } + 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); - } + 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()) - } - } + (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::>(); + /// 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.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::>() - } + 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_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 {})); - } + 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_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/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index fc4b32941b..0426db6b2e 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,139 +1,139 @@ use std::rc::Rc; use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, + 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, + 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(), - } - } + 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() - } + /// 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::>()?; + /// 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, - }) - } + 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 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); - } + /// 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; - } + /// 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; - } - } + 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()) - } + Some(self.keystrokes.len() > typed.len()) + } - /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { - self.keystrokes.as_slice() - } + /// 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 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 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 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() - } + /// 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() - } + 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. diff --git a/crates/gpui/src/keymap/collection.rs b/crates/gpui/src/keymap/collection.rs index 2ab27ced30..8c188d8296 100644 --- a/crates/gpui/src/keymap/collection.rs +++ b/crates/gpui/src/keymap/collection.rs @@ -14,109 +14,109 @@ use std::{any::TypeId, collections::HashMap}; /// ``` #[derive(Default)] pub struct ActionBindingCollection { - entries: HashMap, + entries: HashMap, } #[derive(Debug)] struct ActionBindingEntry { - // The action used to build Keybinding - action: Box, - // The keystrokes that trigger the action - // NOTE: could limit allocations by utilizing smallvec - keystrokes: Vec, + // The action used to build Keybinding + action: Box, + // The keystrokes that trigger the action + // NOTE: could limit allocations by utilizing smallvec + keystrokes: Vec, } impl ActionBindingCollection { - /// Adds a keystroke to an action in the collection. - pub fn with(mut self, keystrokes: impl Into) -> Self { - let action_id = TypeId::of::(); - let entry = self.entries.entry(action_id); - let entry = entry.or_insert_with(|| ActionBindingEntry { - action: Box::new(A::default()), - keystrokes: Vec::with_capacity(5), - }); - entry.keystrokes.push(keystrokes.into()); - self - } + /// Adds a keystroke to an action in the collection. + pub fn with(mut self, keystrokes: impl Into) -> Self { + let action_id = TypeId::of::(); + let entry = self.entries.entry(action_id); + let entry = entry.or_insert_with(|| ActionBindingEntry { + action: Box::new(A::default()), + keystrokes: Vec::with_capacity(5), + }); + entry.keystrokes.push(keystrokes.into()); + self + } - /// Creates an iterator of `KeyBinding` which represents all action+keystrokes in this collection bound to the same key-context. - /// This sequence can be provided to `App::bind_keys`, and any elements which use the same `InteractiveElement::key_context` will receive the bound actions. - pub fn as_keybindings(&self, context: Option<&str>) -> impl Iterator { - let context_predicate = - context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - let iter = self.entries.iter(); - let iter = iter.map(move |(_, entry)| { - let action = &entry.action; - let context_predicate = context_predicate.clone(); - entry.keystrokes.iter().map(move |keystrokes| { - let keybinding = KeyBinding::load( - keystrokes.as_str(), - action.boxed_clone(), - context_predicate.clone(), - false, - None, - &DummyKeyboardMapper, - ); - keybinding.expect("failed to load keybinding") - }) - }); - iter.flatten() - } + /// Creates an iterator of `KeyBinding` which represents all action+keystrokes in this collection bound to the same key-context. + /// This sequence can be provided to `App::bind_keys`, and any elements which use the same `InteractiveElement::key_context` will receive the bound actions. + pub fn as_keybindings(&self, context: Option<&str>) -> impl Iterator { + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); + let iter = self.entries.iter(); + let iter = iter.map(move |(_, entry)| { + let action = &entry.action; + let context_predicate = context_predicate.clone(); + entry.keystrokes.iter().map(move |keystrokes| { + let keybinding = KeyBinding::load( + keystrokes.as_str(), + action.boxed_clone(), + context_predicate.clone(), + false, + None, + &DummyKeyboardMapper, + ); + keybinding.expect("failed to load keybinding") + }) + }); + iter.flatten() + } } #[cfg(test)] mod tests { - use super::*; - use crate::TestAppContext; + use super::*; + use crate::TestAppContext; - actions!([TestAction]); + actions!([TestAction]); - #[gpui::test] - fn test_default_empty(_cx: &mut TestAppContext) { - assert!(ActionBindingCollection::default().entries.is_empty()); - } + #[gpui::test] + fn test_default_empty(_cx: &mut TestAppContext) { + assert!(ActionBindingCollection::default().entries.is_empty()); + } - #[gpui::test] - fn test_single_binding(_cx: &mut TestAppContext) { - let collection = ActionBindingCollection::default(); - let collection = collection.with::("enter"); + #[gpui::test] + fn test_single_binding(_cx: &mut TestAppContext) { + let collection = ActionBindingCollection::default(); + let collection = collection.with::("enter"); - let entry = collection.entries.get(&TypeId::of::()); - let entry = entry.map(|entry| &entry.keystrokes); - assert_eq!(entry, Some(&vec![SharedString::new_static("enter")])); + let entry = collection.entries.get(&TypeId::of::()); + let entry = entry.map(|entry| &entry.keystrokes); + assert_eq!(entry, Some(&vec![SharedString::new_static("enter")])); - let out_bindings = collection.as_keybindings(None).collect::>(); - assert_eq!(out_bindings.len(), 1); - } + let out_bindings = collection.as_keybindings(None).collect::>(); + assert_eq!(out_bindings.len(), 1); + } - #[gpui::test] - fn test_single_action_many_keys(_cx: &mut TestAppContext) { - let collection = ActionBindingCollection::default(); - let collection = collection.with::("enter"); - let collection = collection.with::("escape"); + #[gpui::test] + fn test_single_action_many_keys(_cx: &mut TestAppContext) { + let collection = ActionBindingCollection::default(); + let collection = collection.with::("enter"); + let collection = collection.with::("escape"); - let entry = collection.entries.get(&TypeId::of::()); - let entry = entry.map(|entry| &entry.keystrokes); - assert_eq!( - entry, - Some(&vec![ - SharedString::new_static("enter"), - SharedString::new_static("escape") - ]) - ); + let entry = collection.entries.get(&TypeId::of::()); + let entry = entry.map(|entry| &entry.keystrokes); + assert_eq!( + entry, + Some(&vec![ + SharedString::new_static("enter"), + SharedString::new_static("escape") + ]) + ); - let out_bindings = collection.as_keybindings(None).collect::>(); - assert_eq!(out_bindings.len(), 2); - } + let out_bindings = collection.as_keybindings(None).collect::>(); + assert_eq!(out_bindings.len(), 2); + } - #[gpui::test] - fn test_single_action_many_keys_context(_cx: &mut TestAppContext) { - let collection = ActionBindingCollection::default(); - let collection = collection.with::("enter"); - let collection = collection.with::("escape"); - let out_bindings = collection - .as_keybindings(Some("test_content")) - .collect::>(); - assert_eq!(out_bindings.len(), 2); - } + #[gpui::test] + fn test_single_action_many_keys_context(_cx: &mut TestAppContext) { + let collection = ActionBindingCollection::default(); + let collection = collection.with::("enter"); + let collection = collection.with::("escape"); + let out_bindings = collection + .as_keybindings(Some("test_content")) + .collect::>(); + assert_eq!(out_bindings.len(), 2); + } } diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 27f361bbe2..45222f6d11 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -12,157 +12,157 @@ 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, + /// 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; + type Error = anyhow::Error; - fn try_from(value: &'a str) -> Result { - Self::parse(value) - } + 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 - } + /// 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 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) - } + /// 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) - } + /// 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(()); - } + 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); - } + 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) - } + Self::parse_expr(source, context) + } - /// Check if this context is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + /// 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(); - } + /// 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()); - } - } - } + /// 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(); + /// 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 }) - } - } + 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()), - }) - } - } + /// 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) - } + /// 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() - } + /// 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(()) - } + 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 @@ -170,325 +170,325 @@ impl fmt::Debug for KeyContext { /// 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, - ), + /// 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(..)) - }), - } - } + 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) - } - } + /// 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 - } + /// 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. + #[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) - } - } - } + /// 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; - } + /// 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); - } + 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, - } - } + 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; + 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; + 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; - } + '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)) - } + 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 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_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_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_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_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 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( + &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; + 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}") - } - } - } - } + if needs_parens(node) { + write!(f, "({node})") + } else { + write!(f, "{node}") + } + } + } + } } #[derive(Clone, Copy)] enum LogicalOperator { - And, - Or, + And, + Or, } const PRECEDENCE_CHILD: u32 = 1; @@ -498,394 +498,394 @@ const PRECEDENCE_EQ: u32 = 4; const PRECEDENCE_NOT: u32 = 5; fn is_identifier_char(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c == '-' + c.is_alphanumeric() || c == '_' || c == '-' } fn is_vim_operator_char(c: char) -> bool { - c == '>' || c == '<' || c == '~' || c == '"' || c == '?' + 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..] + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] } #[cfg(test)] mod tests { - use core::slice; + use core::slice; - use super::*; - use crate as gpui; - use KeyBindingContextPredicate::*; + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; - #[test] - fn test_actions_definition() { - { - actions!(test_only, [A, B, C, D, E, F, G]); - } + #[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 - ] - ); - } - } + { + 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_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_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_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_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_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_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); + #[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", "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("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); + 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:?})"); - } - } + #[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(); + #[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 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 contexts = vec![parent_context.clone(), child_context.clone()]; + assert!(predicate.eval(&contexts)); - let grandparent_context = KeyContext::try_from("grandparent").unwrap(); + let grandparent_context = KeyContext::try_from("grandparent").unwrap(); - let contexts = vec![ - grandparent_context, - parent_context.clone(), - child_context.clone(), - ]; - assert!(predicate.eval(&contexts)); + let contexts = vec![ + grandparent_context, + parent_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); - let other_context = KeyContext::try_from("other").unwrap(); + let other_context = KeyContext::try_from("other").unwrap(); - let contexts = vec![other_context.clone(), child_context.clone()]; - assert!(!predicate.eval(&contexts)); + 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)); + 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])); + 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])); - } + 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(); + #[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()])); + 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 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 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 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.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 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))); + 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(); + // 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(), - ]; + // 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)); + // 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)); + 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 > 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()])); + // 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)); - } + // !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 + // 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)) - } + #[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", - ), - ]; + 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); - } - } + 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/src/lerp.rs b/crates/gpui/src/lerp.rs index 9fdc6f4a84..8c595479a9 100644 --- a/crates/gpui/src/lerp.rs +++ b/crates/gpui/src/lerp.rs @@ -1,28 +1,28 @@ //! Lerp trait defines behaviour for interpolating between two values of the same type. use std::{ - fmt::Debug, - ops::{Add, Mul, Sub}, + fmt::Debug, + ops::{Add, Mul, Sub}, }; use crate::{ - Bounds, Corners, DevicePixels, Edges, Percentage, Pixels, Point, Radians, Rems, Rgba, Size, - colors::Colors, + Bounds, Corners, DevicePixels, Edges, Percentage, Pixels, Point, Radians, Rems, Rgba, Size, + colors::Colors, }; /// A trait for types that can be linearly interpolated. pub trait Lerp where - Self: Sized, + Self: Sized, { - /// Interpolates between `self` and `to` based on `delta`. - fn lerp(&self, to: &Self, delta: f32) -> Output; + /// Interpolates between `self` and `to` based on `delta`. + fn lerp(&self, to: &Self, delta: f32) -> Output; } impl Lerp for bool { - fn lerp(&self, to: &Self, delta: f32) -> f32 { - lerp(*self as u8 as f32, *to as u8 as f32, delta) - } + fn lerp(&self, to: &Self, delta: f32) -> f32 { + lerp(*self as u8 as f32, *to as u8 as f32, delta) + } } macro_rules! float_lerps { @@ -52,18 +52,18 @@ macro_rules! int_lerps { } int_lerps!( - usize as f32, - u8 as f32, - u16 as f32, - u32 as f32, - u64 as f64, - u128 as f64, - isize as f32, - i8 as f32, - i16 as f32, - i32 as f32, - i64 as f64, - i128 as f64 + usize as f32, + u8 as f32, + u16 as f32, + u32 as f32, + u64 as f64, + u128 as f64, + isize as f32, + i8 as f32, + i16 as f32, + i32 as f32, + i64 as f64, + i128 as f64 ); macro_rules! struct_lerps { @@ -83,13 +83,13 @@ macro_rules! struct_lerps { } struct_lerps!( - Point { x, y }, - Size { width, height }, - Edges { top, right, bottom, left }, - Corners { top_left, top_right, bottom_right, bottom_left }, - Bounds { origin, size }, - Rgba { r, g, b, a }, - Colors { text, selected_text, background, disabled, selected, border, separator, container } + Point { x, y }, + Size { width, height }, + Edges { top, right, bottom, left }, + Corners { top_left, top_right, bottom_right, bottom_left }, + Bounds { origin, size }, + Rgba { r, g, b, a }, + Colors { text, selected_text, background, disabled, selected, border, separator, container } ); macro_rules! tuple_struct_lerps { @@ -105,279 +105,279 @@ macro_rules! tuple_struct_lerps { } tuple_struct_lerps!( - Radians(f32), - Percentage(f32), - DevicePixels(i32), - Rems(f32), - Pixels(f32) + Radians(f32), + Percentage(f32), + DevicePixels(i32), + Rems(f32), + Pixels(f32) ); fn lerp(from: T, to: T, alpha: T) -> T where - T: Copy + Add + Sub + Mul, + T: Copy + Add + Sub + Mul, { - from + (to - from) * alpha + from + (to - from) * alpha } #[cfg(all(test, feature = "test-support"))] mod tests { - use super::*; - use crate::px; + use super::*; + use crate::px; - #[test] - fn test_f32_lerp() { - let start = 0.0_f32; - let end = 100.0_f32; + #[test] + fn test_f32_lerp() { + let start = 0.0_f32; + let end = 100.0_f32; - assert_eq!(start.lerp(&end, 0.0), 0.0); - assert_eq!(start.lerp(&end, 0.5), 50.0); - assert_eq!(start.lerp(&end, 1.0), 100.0); - assert_eq!(start.lerp(&end, 0.25), 25.0); - } + assert_eq!(start.lerp(&end, 0.0), 0.0); + assert_eq!(start.lerp(&end, 0.5), 50.0); + assert_eq!(start.lerp(&end, 1.0), 100.0); + assert_eq!(start.lerp(&end, 0.25), 25.0); + } - #[test] - fn test_f64_lerp() { - let start = 0.0_f64; - let end = 100.0_f64; + #[test] + fn test_f64_lerp() { + let start = 0.0_f64; + let end = 100.0_f64; - assert_eq!(start.lerp(&end, 0.0), 0.0); - assert_eq!(start.lerp(&end, 0.5), 50.0); - assert_eq!(start.lerp(&end, 1.0), 100.0); - } + assert_eq!(start.lerp(&end, 0.0), 0.0); + assert_eq!(start.lerp(&end, 0.5), 50.0); + assert_eq!(start.lerp(&end, 1.0), 100.0); + } - #[test] - fn test_f32_lerp_negative_values() { - let start = -50.0_f32; - let end = 50.0_f32; + #[test] + fn test_f32_lerp_negative_values() { + let start = -50.0_f32; + let end = 50.0_f32; - assert_eq!(start.lerp(&end, 0.0), -50.0); - assert_eq!(start.lerp(&end, 0.5), 0.0); - assert_eq!(start.lerp(&end, 1.0), 50.0); - } + assert_eq!(start.lerp(&end, 0.0), -50.0); + assert_eq!(start.lerp(&end, 0.5), 0.0); + assert_eq!(start.lerp(&end, 1.0), 50.0); + } - #[test] - fn test_integer_lerp() { - // i32 - assert_eq!(0_i32.lerp(&100_i32, 0.0), 0); - assert_eq!(0_i32.lerp(&100_i32, 0.5), 50); - assert_eq!(0_i32.lerp(&100_i32, 1.0), 100); + #[test] + fn test_integer_lerp() { + // i32 + assert_eq!(0_i32.lerp(&100_i32, 0.0), 0); + assert_eq!(0_i32.lerp(&100_i32, 0.5), 50); + assert_eq!(0_i32.lerp(&100_i32, 1.0), 100); - // u8 - assert_eq!(0_u8.lerp(&100_u8, 0.5), 50); + // u8 + assert_eq!(0_u8.lerp(&100_u8, 0.5), 50); - // u16 - assert_eq!(0_u16.lerp(&1000_u16, 0.5), 500); + // u16 + assert_eq!(0_u16.lerp(&1000_u16, 0.5), 500); - // u32 - assert_eq!(0_u32.lerp(&10000_u32, 0.5), 5000); + // u32 + assert_eq!(0_u32.lerp(&10000_u32, 0.5), 5000); - // u64 - assert_eq!(0_u64.lerp(&100000_u64, 0.5), 50000); + // u64 + assert_eq!(0_u64.lerp(&100000_u64, 0.5), 50000); - // i64 - assert_eq!((-50000_i64).lerp(&50000_i64, 0.5), 0); + // i64 + assert_eq!((-50000_i64).lerp(&50000_i64, 0.5), 0); - // usize - assert_eq!(0_usize.lerp(&100_usize, 0.5), 50); + // usize + assert_eq!(0_usize.lerp(&100_usize, 0.5), 50); - // isize - assert_eq!((-100_isize).lerp(&100_isize, 0.5), 0); - } + // isize + assert_eq!((-100_isize).lerp(&100_isize, 0.5), 0); + } - #[test] - fn test_point_lerp() { - let start: Point = Point { x: 0.0, y: 0.0 }; - let end: Point = Point { x: 100.0, y: 200.0 }; + #[test] + fn test_point_lerp() { + let start: Point = Point { x: 0.0, y: 0.0 }; + let end: Point = Point { x: 100.0, y: 200.0 }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.x, 50.0); - assert_eq!(mid.y, 100.0); + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.x, 50.0); + assert_eq!(mid.y, 100.0); - let at_start = start.lerp(&end, 0.0); - assert_eq!(at_start.x, 0.0); - assert_eq!(at_start.y, 0.0); + let at_start = start.lerp(&end, 0.0); + assert_eq!(at_start.x, 0.0); + assert_eq!(at_start.y, 0.0); - let at_end = start.lerp(&end, 1.0); - assert_eq!(at_end.x, 100.0); - assert_eq!(at_end.y, 200.0); - } + let at_end = start.lerp(&end, 1.0); + assert_eq!(at_end.x, 100.0); + assert_eq!(at_end.y, 200.0); + } - #[test] - fn test_size_lerp() { - let start: Size = Size { - width: 10.0, - height: 20.0, - }; - let end: Size = Size { - width: 110.0, - height: 220.0, - }; + #[test] + fn test_size_lerp() { + let start: Size = Size { + width: 10.0, + height: 20.0, + }; + let end: Size = Size { + width: 110.0, + height: 220.0, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.width, 60.0); - assert_eq!(mid.height, 120.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.width, 60.0); + assert_eq!(mid.height, 120.0); + } - #[test] - fn test_edges_lerp() { - let start: Edges = Edges { - top: 0.0, - right: 0.0, - bottom: 0.0, - left: 0.0, - }; - let end: Edges = Edges { - top: 10.0, - right: 20.0, - bottom: 30.0, - left: 40.0, - }; + #[test] + fn test_edges_lerp() { + let start: Edges = Edges { + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, + }; + let end: Edges = Edges { + top: 10.0, + right: 20.0, + bottom: 30.0, + left: 40.0, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.top, 5.0); - assert_eq!(mid.right, 10.0); - assert_eq!(mid.bottom, 15.0); - assert_eq!(mid.left, 20.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.top, 5.0); + assert_eq!(mid.right, 10.0); + assert_eq!(mid.bottom, 15.0); + assert_eq!(mid.left, 20.0); + } - #[test] - fn test_corners_lerp() { - let start: Corners = Corners { - top_left: 0.0, - top_right: 0.0, - bottom_right: 0.0, - bottom_left: 0.0, - }; - let end: Corners = Corners { - top_left: 4.0, - top_right: 8.0, - bottom_right: 12.0, - bottom_left: 16.0, - }; + #[test] + fn test_corners_lerp() { + let start: Corners = Corners { + top_left: 0.0, + top_right: 0.0, + bottom_right: 0.0, + bottom_left: 0.0, + }; + let end: Corners = Corners { + top_left: 4.0, + top_right: 8.0, + bottom_right: 12.0, + bottom_left: 16.0, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.top_left, 2.0); - assert_eq!(mid.top_right, 4.0); - assert_eq!(mid.bottom_right, 6.0); - assert_eq!(mid.bottom_left, 8.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.top_left, 2.0); + assert_eq!(mid.top_right, 4.0); + assert_eq!(mid.bottom_right, 6.0); + assert_eq!(mid.bottom_left, 8.0); + } - #[test] - fn test_bounds_lerp() { - let start: Bounds = Bounds { - origin: Point { x: 0.0, y: 0.0 }, - size: Size { - width: 100.0, - height: 100.0, - }, - }; - let end: Bounds = Bounds { - origin: Point { x: 50.0, y: 50.0 }, - size: Size { - width: 200.0, - height: 200.0, - }, - }; + #[test] + fn test_bounds_lerp() { + let start: Bounds = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 100.0, + height: 100.0, + }, + }; + let end: Bounds = Bounds { + origin: Point { x: 50.0, y: 50.0 }, + size: Size { + width: 200.0, + height: 200.0, + }, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.origin.x, 25.0); - assert_eq!(mid.origin.y, 25.0); - assert_eq!(mid.size.width, 150.0); - assert_eq!(mid.size.height, 150.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.origin.x, 25.0); + assert_eq!(mid.origin.y, 25.0); + assert_eq!(mid.size.width, 150.0); + assert_eq!(mid.size.height, 150.0); + } - #[test] - fn test_rgba_lerp() { - let start = Rgba { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - let end = Rgba { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }; + #[test] + fn test_rgba_lerp() { + let start = Rgba { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + let end = Rgba { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.r, 0.5); - assert_eq!(mid.g, 0.5); - assert_eq!(mid.b, 0.5); - assert_eq!(mid.a, 1.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.r, 0.5); + assert_eq!(mid.g, 0.5); + assert_eq!(mid.b, 0.5); + assert_eq!(mid.a, 1.0); + } - #[test] - fn test_rgba_lerp_with_alpha() { - let start = Rgba { - r: 1.0, - g: 0.0, - b: 0.0, - a: 0.0, - }; - let end = Rgba { - r: 0.0, - g: 0.0, - b: 1.0, - a: 1.0, - }; + #[test] + fn test_rgba_lerp_with_alpha() { + let start = Rgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + let end = Rgba { + r: 0.0, + g: 0.0, + b: 1.0, + a: 1.0, + }; - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.r, 0.5); - assert_eq!(mid.g, 0.0); - assert_eq!(mid.b, 0.5); - assert_eq!(mid.a, 0.5); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.r, 0.5); + assert_eq!(mid.g, 0.0); + assert_eq!(mid.b, 0.5); + assert_eq!(mid.a, 0.5); + } - #[test] - fn test_pixels_lerp() { - let start = px(0.0); - let end = px(100.0); + #[test] + fn test_pixels_lerp() { + let start = px(0.0); + let end = px(100.0); - let mid = start.lerp(&end, 0.5); - assert_eq!(mid, px(50.0)); + let mid = start.lerp(&end, 0.5); + assert_eq!(mid, px(50.0)); - let at_start = start.lerp(&end, 0.0); - assert_eq!(at_start, px(0.0)); + let at_start = start.lerp(&end, 0.0); + assert_eq!(at_start, px(0.0)); - let at_end = start.lerp(&end, 1.0); - assert_eq!(at_end, px(100.0)); - } + let at_end = start.lerp(&end, 1.0); + assert_eq!(at_end, px(100.0)); + } - #[test] - fn test_rems_lerp() { - let start = Rems(0.0); - let end = Rems(2.0); + #[test] + fn test_rems_lerp() { + let start = Rems(0.0); + let end = Rems(2.0); - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.0, 1.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.0, 1.0); + } - #[test] - fn test_device_pixels_lerp() { - let start = DevicePixels(0); - let end = DevicePixels(100); + #[test] + fn test_device_pixels_lerp() { + let start = DevicePixels(0); + let end = DevicePixels(100); - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.0, 50); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.0, 50); + } - #[test] - fn test_percentage_lerp() { - let start = Percentage(0.0); - let end = Percentage(100.0); + #[test] + fn test_percentage_lerp() { + let start = Percentage(0.0); + let end = Percentage(100.0); - let mid = start.lerp(&end, 0.5); - assert_eq!(mid.0, 50.0); - } + let mid = start.lerp(&end, 0.5); + assert_eq!(mid.0, 50.0); + } - #[test] - fn test_radians_lerp() { - let start = Radians(0.0); - let end = Radians(std::f32::consts::PI); + #[test] + fn test_radians_lerp() { + let start = Radians(0.0); + let end = Radians(std::f32::consts::PI); - let mid = start.lerp(&end, 0.5); - assert!((mid.0 - std::f32::consts::FRAC_PI_2).abs() < 0.0001); - } + let mid = start.lerp(&end, 0.5); + assert!((mid.0 - std::f32::consts::FRAC_PI_2).abs() < 0.0001); + } } diff --git a/crates/gpui/src/node_graph/data.rs b/crates/gpui/src/node_graph/data.rs index a95dad6c40..f650b974df 100644 --- a/crates/gpui/src/node_graph/data.rs +++ b/crates/gpui/src/node_graph/data.rs @@ -34,12 +34,12 @@ pub struct EdgeId(pub u64); /// Whether a port accepts incoming connections or produces outgoing ones. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PortKind { - /// A port that consumes data; conventionally drawn on the left side of a - /// node and accepts connections *from* an [`PortKind::Output`] port. - Input, - /// A port that produces data; conventionally drawn on the right side of a - /// node and connects *to* an [`PortKind::Input`] port. - Output, + /// A port that consumes data; conventionally drawn on the left side of a + /// node and accepts connections *from* an [`PortKind::Output`] port. + Input, + /// A port that produces data; conventionally drawn on the right side of a + /// node and connects *to* an [`PortKind::Input`] port. + Output, } /// A lightweight, app-defined descriptor of the data flowing through a port. @@ -59,28 +59,28 @@ pub enum PortKind { /// distinct types sharing a name should disambiguate the name. #[derive(Clone, Debug)] pub struct PortDataType { - /// Human-readable type name, e.g. `"video"`, `"audio"`, `"matte"`. - /// Also used as the identity of the type (see type-level docs). - pub name: SharedString, - /// Color used to tint port dots and wires carrying this type. - pub color: Hsla, + /// Human-readable type name, e.g. `"video"`, `"audio"`, `"matte"`. + /// Also used as the identity of the type (see type-level docs). + pub name: SharedString, + /// Color used to tint port dots and wires carrying this type. + pub color: Hsla, } impl PortDataType { - /// Creates a new data-type descriptor with the given display name and - /// tint color. - pub fn new(name: impl Into, color: Hsla) -> Self { - Self { - name: name.into(), - color, - } - } + /// Creates a new data-type descriptor with the given display name and + /// tint color. + pub fn new(name: impl Into, color: Hsla) -> Self { + Self { + name: name.into(), + color, + } + } } impl PartialEq for PortDataType { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } } impl Eq for PortDataType {} @@ -91,27 +91,27 @@ impl Eq for PortDataType {} /// [`PortId`], a direction ([`PortKind`]) and a [`PortDataType`] used for /// tinting. pub trait PortData { - /// Returns the globally unique identifier of this port. - fn id(&self) -> PortId; + /// Returns the globally unique identifier of this port. + fn id(&self) -> PortId; - /// Returns whether this is an input or an output port. - fn kind(&self) -> PortKind; + /// Returns whether this is an input or an output port. + fn kind(&self) -> PortKind; - /// Returns the short label drawn next to the port dot (e.g. `"in"`, - /// `"mask"`). May be empty, in which case only the dot is drawn. - fn label(&self) -> SharedString; + /// Returns the short label drawn next to the port dot (e.g. `"in"`, + /// `"mask"`). May be empty, in which case only the dot is drawn. + fn label(&self) -> SharedString; - /// Returns the data type of this port, used to tint the port dot and any - /// wires connected to it. - fn data_type(&self) -> PortDataType; + /// Returns the data type of this port, used to tint the port dot and any + /// wires connected to it. + fn data_type(&self) -> PortDataType; - /// Returns whether this port currently has at least one edge attached. - /// - /// Used only for rendering (connected dots are filled, unconnected dots - /// are hollow) and for styling during wire drags; the widget does not - /// enforce any cardinality rules from it — that is the job of - /// [`NodeGraphDataSource::can_connect`]. - fn is_connected(&self) -> bool; + /// Returns whether this port currently has at least one edge attached. + /// + /// Used only for rendering (connected dots are filled, unconnected dots + /// are hollow) and for styling during wire drags; the widget does not + /// enforce any cardinality rules from it — that is the job of + /// [`NodeGraphDataSource::can_connect`]. + fn is_connected(&self) -> bool; } /// A single node in the graph. @@ -120,67 +120,67 @@ pub trait PortData { /// left and a column of output ports on the right (see /// [`NodeElement`](crate::node_graph::NodeElement)). pub trait NodeData { - /// The port type used by this node's inputs and outputs. - type Port: PortData; + /// The port type used by this node's inputs and outputs. + type Port: PortData; - /// Returns the unique identifier of this node. - fn id(&self) -> NodeId; + /// Returns the unique identifier of this node. + fn id(&self) -> NodeId; - /// Returns the title drawn in the node's header. - fn title(&self) -> SharedString; + /// Returns the title drawn in the node's header. + fn title(&self) -> SharedString; - /// Returns the position of the node's top-left corner in *graph space* - /// (the document coordinate system). - /// - /// Graph space is an unbounded, zoom-independent coordinate system: a - /// node at `point(px(100.), px(40.))` stays attached to that document - /// location regardless of pan and zoom. The view converts to screen - /// coordinates with - /// [`GraphViewState::graph_to_screen`](crate::node_graph::GraphViewState::graph_to_screen). - fn position(&self) -> Point; + /// Returns the position of the node's top-left corner in *graph space* + /// (the document coordinate system). + /// + /// Graph space is an unbounded, zoom-independent coordinate system: a + /// node at `point(px(100.), px(40.))` stays attached to that document + /// location regardless of pan and zoom. The view converts to screen + /// coordinates with + /// [`GraphViewState::graph_to_screen`](crate::node_graph::GraphViewState::graph_to_screen). + fn position(&self) -> Point; - /// Returns the input ports of this node, in top-to-bottom draw order. - fn inputs(&self) -> Vec; + /// Returns the input ports of this node, in top-to-bottom draw order. + fn inputs(&self) -> Vec; - /// Returns the output ports of this node, in top-to-bottom draw order. - fn outputs(&self) -> Vec; + /// Returns the output ports of this node, in top-to-bottom draw order. + fn outputs(&self) -> Vec; - /// Returns an optional accent color for the node header, or `None` to use - /// the theme default. Apps typically use this to group nodes by category - /// (inputs, transforms, color management, outputs, …). - fn header_color(&self) -> Option; + /// Returns an optional accent color for the node header, or `None` to use + /// the theme default. Apps typically use this to group nodes by category + /// (inputs, transforms, color management, outputs, …). + fn header_color(&self) -> Option; - /// Returns whether the node is collapsed to just its header. - /// - /// Collapsed nodes draw no ports and cannot be connection targets. The - /// collapsed state itself belongs to the app's model (or view state); the - /// widget only reflects it. - fn is_collapsed(&self) -> bool; + /// Returns whether the node is collapsed to just its header. + /// + /// Collapsed nodes draw no ports and cannot be connection targets. The + /// collapsed state itself belongs to the app's model (or view state); the + /// widget only reflects it. + fn is_collapsed(&self) -> bool; - /// Returns whether the node is enabled. - /// - /// Disabled nodes (e.g. a bypassed effect) are drawn dimmed. This is a - /// purely visual hint; the widget does not change interaction behavior - /// for disabled nodes. - fn is_enabled(&self) -> bool; + /// Returns whether the node is enabled. + /// + /// Disabled nodes (e.g. a bypassed effect) are drawn dimmed. This is a + /// purely visual hint; the widget does not change interaction behavior + /// for disabled nodes. + fn is_enabled(&self) -> bool; } /// A single directed connection from an output port to an input port. pub trait EdgeData { - /// Returns the unique identifier of this edge. - fn id(&self) -> EdgeId; + /// Returns the unique identifier of this edge. + fn id(&self) -> EdgeId; - /// Returns the id of the node the connection starts at. - fn from_node(&self) -> NodeId; + /// Returns the id of the node the connection starts at. + fn from_node(&self) -> NodeId; - /// Returns the id of the output port the connection starts at. - fn from_port(&self) -> PortId; + /// Returns the id of the output port the connection starts at. + fn from_port(&self) -> PortId; - /// Returns the id of the node the connection ends at. - fn to_node(&self) -> NodeId; + /// Returns the id of the node the connection ends at. + fn to_node(&self) -> NodeId; - /// Returns the id of the input port the connection ends at. - fn to_port(&self) -> PortId; + /// Returns the id of the input port the connection ends at. + fn to_port(&self) -> PortId; } /// The data source backing a [`NodeGraphView`](crate::node_graph::NodeGraphView). @@ -195,40 +195,40 @@ pub trait EdgeData { /// /// [`NodeGraphEvent`]: crate::node_graph::NodeGraphEvent pub trait NodeGraphDataSource { - /// The node type returned by [`nodes()`](Self::nodes). - type Node: NodeData; - /// The edge type returned by [`edges()`](Self::edges). - type Edge: EdgeData; + /// The node type returned by [`nodes()`](Self::nodes). + type Node: NodeData; + /// The edge type returned by [`edges()`](Self::edges). + type Edge: EdgeData; - /// Returns all nodes to display, in no required order (the view sorts for - /// painting; selection order is unaffected). - fn nodes(&self) -> Vec; + /// Returns all nodes to display, in no required order (the view sorts for + /// painting; selection order is unaffected). + fn nodes(&self) -> Vec; - /// Returns all edges to display. Edges referencing ports or nodes that - /// are not part of [`nodes()`](Self::nodes) are ignored by the view. - fn edges(&self) -> Vec; + /// Returns all edges to display. Edges referencing ports or nodes that + /// are not part of [`nodes()`](Self::nodes) are ignored by the view. + fn edges(&self) -> Vec; - /// Returns whether connecting output port `from` to input port `to` - /// would be valid. - /// - /// This is the single place where the app enforces its connection rules: - /// data-type compatibility (including implicit conversions), cycle - /// prevention, port cardinality, node enablement, and so on. The view - /// calls this: - /// - /// - *continuously during a wire drag* to highlight compatible target - /// ports and to mark the ghost wire as valid/invalid, and - /// - *once on drop* before emitting - /// [`NodeGraphEvent::ConnectionRequested`](crate::node_graph::NodeGraphEvent::ConnectionRequested) - /// — a drop on a port for which this returns `false` cancels the drag - /// silently. - /// - /// It must be cheap, pure, and consistent: the same arguments must yield - /// the same answer within a frame. The view passes output port first, - /// input port second, regardless of which end the user started the drag - /// from. Returning `true` here does not commit the app to accepting the - /// connection; the engine may still reject it when the event arrives - /// (e.g. it raced with another edit), in which case the app simply does - /// not apply it. - fn can_connect(&self, from: PortId, to: PortId) -> bool; + /// Returns whether connecting output port `from` to input port `to` + /// would be valid. + /// + /// This is the single place where the app enforces its connection rules: + /// data-type compatibility (including implicit conversions), cycle + /// prevention, port cardinality, node enablement, and so on. The view + /// calls this: + /// + /// - *continuously during a wire drag* to highlight compatible target + /// ports and to mark the ghost wire as valid/invalid, and + /// - *once on drop* before emitting + /// [`NodeGraphEvent::ConnectionRequested`](crate::node_graph::NodeGraphEvent::ConnectionRequested) + /// — a drop on a port for which this returns `false` cancels the drag + /// silently. + /// + /// It must be cheap, pure, and consistent: the same arguments must yield + /// the same answer within a frame. The view passes output port first, + /// input port second, regardless of which end the user started the drag + /// from. Returning `true` here does not commit the app to accepting the + /// connection; the engine may still reject it when the event arrives + /// (e.g. it raced with another edit), in which case the app simply does + /// not apply it. + fn can_connect(&self, from: PortId, to: PortId) -> bool; } diff --git a/crates/gpui/src/node_graph/graph_view.rs b/crates/gpui/src/node_graph/graph_view.rs index f730f3640a..6c65b3d109 100644 --- a/crates/gpui/src/node_graph/graph_view.rs +++ b/crates/gpui/src/node_graph/graph_view.rs @@ -8,16 +8,16 @@ use std::collections::{BTreeSet, HashMap}; use crate::{ - App, BorderStyle, Bounds, Context, Corners, Edges, Entity, EventEmitter, FocusHandle, - Focusable, Hsla, IntoElement, KeyDownEvent, KeyUpEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, PaintQuad, PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, - Size, Window, canvas, colors::DefaultColors, div, fill, hsla, point, prelude::*, px, size, + App, BorderStyle, Bounds, Context, Corners, Edges, Entity, EventEmitter, FocusHandle, + Focusable, Hsla, IntoElement, KeyDownEvent, KeyUpEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, PaintQuad, PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, + Size, Window, canvas, colors::DefaultColors, div, fill, hsla, point, prelude::*, px, size, }; use crate::node_graph::{ - DEFAULT_NODE_WIDTH, EdgeData, EdgeId, GhostWire, GraphViewState, NodeData, NodeElement, - NodeGraphDataSource, NodeId, NodeVisualState, PortData, PortDataType, PortId, PortKind, - SelectionRect, Wire, WireVisualState, paint_ghost, + DEFAULT_NODE_WIDTH, EdgeData, EdgeId, GhostWire, GraphViewState, NodeData, NodeElement, + NodeGraphDataSource, NodeId, NodeVisualState, PortData, PortDataType, PortId, PortKind, + SelectionRect, Wire, WireVisualState, paint_ghost, }; /// Spacing between grid lines, in graph-space pixels. @@ -34,87 +34,87 @@ const MARQUEE_DRAG_THRESHOLD: f32 = 3.0; /// What a mouse press on the canvas hit. #[derive(Clone, Copy, Debug, PartialEq)] enum HitTarget { - /// Empty background. - Background, - /// A port dot: start a wire drag. - Port(PortId), - /// A collapse or enable toggle in a node header: toggle selection only. - Toggle(NodeId), - /// A node body: select (and possibly drag) the node. - Node(NodeId), + /// Empty background. + Background, + /// A port dot: start a wire drag. + Port(PortId), + /// A collapse or enable toggle in a node header: toggle selection only. + Toggle(NodeId), + /// A node body: select (and possibly drag) the node. + Node(NodeId), } /// Transient state of a node move drag. struct NodeDragState { - /// The nodes being moved (the full selection at drag start). - nodes: Vec, - /// Element-local cursor position where the drag started. - anchor: Point, - /// Accumulated graph-space displacement since drag start. - delta: Point, + /// The nodes being moved (the full selection at drag start). + nodes: Vec, + /// Element-local cursor position where the drag started. + anchor: Point, + /// Accumulated graph-space displacement since drag start. + delta: Point, } /// Transient state of a wire drag (a "ghost" connection in progress). struct WireDragState { - /// The ghost wire, anchored at the drag source port. - ghost: GhostWire, - /// The port the drag started from. - source_port: PortId, - /// When the drag picked up an existing edge (from a connected input), the - /// edge id; dropping in empty space disconnects it. - picked_edge: Option, - /// Ports currently approved as drop targets by - /// [`NodeGraphDataSource::can_connect`]. - valid_ports: BTreeSet, + /// The ghost wire, anchored at the drag source port. + ghost: GhostWire, + /// The port the drag started from. + source_port: PortId, + /// When the drag picked up an existing edge (from a connected input), the + /// edge id; dropping in empty space disconnects it. + picked_edge: Option, + /// Ports currently approved as drop targets by + /// [`NodeGraphDataSource::can_connect`]. + valid_ports: BTreeSet, } /// Transient state of a pan drag (space-drag or middle-mouse drag). struct PanDragState { - /// Window-space cursor position where the pan started. - start_mouse: Point, - /// The viewport offset when the pan started. - start_offset: Point, + /// Window-space cursor position where the pan started. + start_mouse: Point, + /// The viewport offset when the pan started. + start_offset: Point, } /// A snapshot of everything the view paints in one frame, computed in the /// canvas prepaint and consumed by the paint closure. struct GraphDraw { - /// Node elements in paint order (bottom-most first), positioned in window - /// space. Dragged nodes are painted last (on top). - nodes: Vec<(Point, NodeElement)>, - /// Edge wires, in graph order, positioned in window space. - wires: Vec, - /// The in-progress ghost wire, if any, in window space. - ghost: Option, - /// The in-progress marquee rectangle, in element-local space. - marquee: Option, - /// The pan offset used to compute this frame. - offset: Point, - /// The zoom factor used to compute this frame. - zoom: f32, + /// Node elements in paint order (bottom-most first), positioned in window + /// space. Dragged nodes are painted last (on top). + nodes: Vec<(Point, NodeElement)>, + /// Edge wires, in graph order, positioned in window space. + wires: Vec, + /// The in-progress ghost wire, if any, in window space. + ghost: Option, + /// The in-progress marquee rectangle, in element-local space. + marquee: Option, + /// The pan offset used to compute this frame. + offset: Point, + /// The zoom factor used to compute this frame. + zoom: f32, } /// A copy of a [`GhostWire`]'s geometry, stored in the frame snapshot so the /// paint closure does not need to borrow the view. struct GhostSnapshot { - /// Window-space anchor of the fixed end. - from: Point, - /// Window-space position of the free (cursor) end. - to: Point, - /// Data-type tint of the source port. - color: Hsla, - /// Whether the current drop target is valid. - target_valid: bool, + /// Window-space anchor of the fixed end. + from: Point, + /// Window-space position of the free (cursor) end. + to: Point, + /// Data-type tint of the source port. + color: Hsla, + /// Whether the current drop target is valid. + target_valid: bool, } /// Returns whether the two axis-aligned rectangles overlap (touching counts). fn rects_intersect( - min1: Point, - max1: Point, - min2: Point, - max2: Point, + min1: Point, + max1: Point, + min2: Point, + max2: Point, ) -> bool { - min1.x <= max2.x && min2.x <= max1.x && min1.y <= max2.y && min2.y <= max1.y + min1.x <= max2.x && min2.x <= max1.x && min1.y <= max2.y && min2.y <= max1.y } /// Events emitted by [`NodeGraphView`]. @@ -127,95 +127,95 @@ fn rects_intersect( /// ignore. #[derive(Clone, Debug)] pub enum NodeGraphEvent { - /// Continuous preview emitted while the user drags one or more nodes: - /// reports the *accumulated* graph-space delta since the drag started. - /// - /// Emitted on every pointer move during a node drag, before the final - /// [`NodeMoveRequested`](Self::NodeMoveRequested). Apps may use it for - /// live feedback (e.g. snapping guides) but must not push undo states for - /// it. The widget draws dragged nodes at their model position plus this - /// delta, so the app does not need to apply it for the drag to look - /// right. - NodeMovePreview { - /// The nodes being dragged (the full selection at drag start). - nodes: Vec, - /// Accumulated graph-space displacement since drag start. - delta: Point, - }, + /// Continuous preview emitted while the user drags one or more nodes: + /// reports the *accumulated* graph-space delta since the drag started. + /// + /// Emitted on every pointer move during a node drag, before the final + /// [`NodeMoveRequested`](Self::NodeMoveRequested). Apps may use it for + /// live feedback (e.g. snapping guides) but must not push undo states for + /// it. The widget draws dragged nodes at their model position plus this + /// delta, so the app does not need to apply it for the drag to look + /// right. + NodeMovePreview { + /// The nodes being dragged (the full selection at drag start). + nodes: Vec, + /// Accumulated graph-space displacement since drag start. + delta: Point, + }, - /// Emitted exactly once when a node drag ends (pointer release). - /// - /// This is the undoable operation: the app should move all listed nodes - /// by `delta` in graph space as a single undo step. `delta` is the same - /// accumulated displacement reported by the last - /// [`NodeMovePreview`](Self::NodeMovePreview) of this drag. - NodeMoveRequested { - /// The nodes to move (the full selection at drag start). - nodes: Vec, - /// Total graph-space displacement to apply. - delta: Point, - }, + /// Emitted exactly once when a node drag ends (pointer release). + /// + /// This is the undoable operation: the app should move all listed nodes + /// by `delta` in graph space as a single undo step. `delta` is the same + /// accumulated displacement reported by the last + /// [`NodeMovePreview`](Self::NodeMovePreview) of this drag. + NodeMoveRequested { + /// The nodes to move (the full selection at drag start). + nodes: Vec, + /// Total graph-space displacement to apply. + delta: Point, + }, - /// The user dropped a wire drag on a port and - /// [`NodeGraphDataSource::can_connect`] approved the pair. - /// - /// `from` is always the output port, `to` the input port, regardless of - /// which end the drag started from. The app should still re-validate - /// before applying — the model may have changed since the drag started. - ConnectionRequested { - /// The output port the connection starts at. - from: PortId, - /// The input port the connection ends at. - to: PortId, - }, + /// The user dropped a wire drag on a port and + /// [`NodeGraphDataSource::can_connect`] approved the pair. + /// + /// `from` is always the output port, `to` the input port, regardless of + /// which end the drag started from. The app should still re-validate + /// before applying — the model may have changed since the drag started. + ConnectionRequested { + /// The output port the connection starts at. + from: PortId, + /// The input port the connection ends at. + to: PortId, + }, - /// The user asked to remove an existing edge (e.g. by clicking a wire - /// with the disconnect modifier, or dragging a connected input's wire - /// off into empty space). - DisconnectionRequested { - /// The edge to remove. - edge: EdgeId, - }, + /// The user asked to remove an existing edge (e.g. by clicking a wire + /// with the disconnect modifier, or dragging a connected input's wire + /// off into empty space). + DisconnectionRequested { + /// The edge to remove. + edge: EdgeId, + }, - /// The user pressed the delete/backspace key with a non-empty selection. - /// - /// Nodes and edges are delivered together so the app can remove them as - /// one undo step. `edges` contains both explicitly selected edges and - /// every edge incident to a deleted node (computed by the widget, since - /// those edges cannot outlive their endpoints). - DeleteRequested { - /// The nodes to delete. - nodes: Vec, - /// The edges to delete, including edges incident to `nodes`. - edges: Vec, - }, + /// The user pressed the delete/backspace key with a non-empty selection. + /// + /// Nodes and edges are delivered together so the app can remove them as + /// one undo step. `edges` contains both explicitly selected edges and + /// every edge incident to a deleted node (computed by the widget, since + /// those edges cannot outlive their endpoints). + DeleteRequested { + /// The nodes to delete. + nodes: Vec, + /// The edges to delete, including edges incident to `nodes`. + edges: Vec, + }, - /// The selection changed. The full new selection is included so listeners - /// do not need to track deltas. Oak uses this to keep the node graph and - /// the [`crate::effect_stack`] selections in sync. - SelectionChanged { - /// The complete new selection. - nodes: BTreeSet, - }, + /// The selection changed. The full new selection is included so listeners + /// do not need to track deltas. Oak uses this to keep the node graph and + /// the [`crate::effect_stack`] selections in sync. + SelectionChanged { + /// The complete new selection. + nodes: BTreeSet, + }, - /// The viewport (pan offset and/or zoom) changed. Emitted after the - /// gesture that caused it completes — for a zoom-to-cursor scroll this is - /// per scroll tick; apps that persist the viewport should debounce. - ViewChanged { - /// The new pan offset (screen-space position of the graph origin). - offset: Point, - /// The new zoom factor. - zoom: f32, - }, + /// The viewport (pan offset and/or zoom) changed. Emitted after the + /// gesture that caused it completes — for a zoom-to-cursor scroll this is + /// per scroll tick; apps that persist the viewport should debounce. + ViewChanged { + /// The new pan offset (screen-space position of the graph origin). + offset: Point, + /// The new zoom factor. + zoom: f32, + }, - /// The user clicked (or released a cancelled wire drag on) empty - /// background. `position` is the click position in *graph space*, ready - /// to be used as the position of a newly created node. Oak opens its - /// "add node" menu from this event. - BackgroundClicked { - /// Click position in graph space. - position: Point, - }, + /// The user clicked (or released a cancelled wire drag on) empty + /// background. `position` is the click position in *graph space*, ready + /// to be used as the position of a newly created node. Oak opens its + /// "add node" menu from this event. + BackgroundClicked { + /// Click position in graph space. + position: Point, + }, } use NodeGraphEvent::*; @@ -243,746 +243,784 @@ use NodeGraphEvent::*; /// convert to element-local space by subtracting the viewport origin, which is /// captured each frame by the canvas prepaint. pub struct NodeGraphView { - /// The app-supplied graph model. Read every frame; never mutated. - data: Entity, - /// Viewport and selection state. - state: GraphViewState, - /// Focus handle for keyboard interactions (delete, future shortcuts). - focus_handle: FocusHandle, - /// The view's bounds within the window, set every frame by the canvas - /// prepaint. Its origin converts between window-space and element-local - /// coordinates. - viewport: Bounds, - /// In-progress node move drag, if any. - node_drag: Option, - /// In-progress wire drag, if any. - wire_drag: Option, - /// In-progress pan drag, if any. - pan_drag: Option, - /// Whether the space key is currently held down (space-drag pans). - space_down: bool, + /// The app-supplied graph model. Read every frame; never mutated. + data: Entity, + /// Viewport and selection state. + state: GraphViewState, + /// Focus handle for keyboard interactions (delete, future shortcuts). + focus_handle: FocusHandle, + /// The view's bounds within the window, set every frame by the canvas + /// prepaint. Its origin converts between window-space and element-local + /// coordinates. + viewport: Bounds, + /// In-progress node move drag, if any. + node_drag: Option, + /// In-progress wire drag, if any. + wire_drag: Option, + /// In-progress pan drag, if any. + pan_drag: Option, + /// Whether the space key is currently held down (space-drag pans). + space_down: bool, } impl NodeGraphView { - /// Creates a new node-graph view over the given data-source entity. - /// - /// The view subscribes to the entity and re-renders whenever the app - /// calls `cx.notify()` on it after applying (or rejecting) edit requests. - pub fn new(data: Entity, _window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - cx.observe(&data, |_, _, cx| cx.notify()).detach(); - Self { - data, - state: GraphViewState::new(), - focus_handle, - viewport: Bounds::new(point(px(0.0), px(0.0)), size(px(0.0), px(0.0))), - node_drag: None, - wire_drag: None, - pan_drag: None, - space_down: false, - } - } + /// Creates a new node-graph view over the given data-source entity. + /// + /// The view subscribes to the entity and re-renders whenever the app + /// calls `cx.notify()` on it after applying (or rejecting) edit requests. + pub fn new(data: Entity, _window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + cx.observe(&data, |_, _, cx| cx.notify()).detach(); + Self { + data, + state: GraphViewState::new(), + focus_handle, + viewport: Bounds::new(point(px(0.0), px(0.0)), size(px(0.0), px(0.0))), + node_drag: None, + wire_drag: None, + pan_drag: None, + space_down: false, + } + } - /// Returns the current viewport/selection state. - pub fn state(&self) -> &GraphViewState { - &self.state - } + /// Returns the current viewport/selection state. + pub fn state(&self) -> &GraphViewState { + &self.state + } - /// Returns the size of the canvas the graph was last painted into, or a - /// zero size before the first frame. Hosts use this to fit the viewport - /// to the graph (see [`GraphViewState::fit_to_rect`]). - pub fn viewport_size(&self) -> Size { - self.viewport.size - } + /// Returns the size of the canvas the graph was last painted into, or a + /// zero size before the first frame. Hosts use this to fit the viewport + /// to the graph (see [`GraphViewState::fit_to_rect`]). + pub fn viewport_size(&self) -> Size { + self.viewport.size + } - /// Returns a mutable reference to the viewport/selection state, e.g. to - /// restore a persisted viewport or to sync selection with - /// [`crate::effect_stack`]. Does not emit events; call `cx.notify()` on - /// the view entity afterwards if you changed anything. - pub fn state_mut(&mut self) -> &mut GraphViewState { - &mut self.state - } + /// Returns a mutable reference to the viewport/selection state, e.g. to + /// restore a persisted viewport or to sync selection with + /// [`crate::effect_stack`]. Does not emit events; call `cx.notify()` on + /// the view entity afterwards if you changed anything. + pub fn state_mut(&mut self) -> &mut GraphViewState { + &mut self.state + } - /// Returns the data-source entity this view renders. - pub fn data(&self) -> &Entity { - &self.data - } + /// Returns the data-source entity this view renders. + pub fn data(&self) -> &Entity { + &self.data + } - /// Returns what is under `position` (in window space), or - /// [`HitTarget::Background`]. Nodes are tested in reverse paint order so - /// the topmost (last-painted) node wins. - fn hit_test(&self, position: Point, cx: &App) -> HitTarget { - let anchor = position - self.viewport.origin; - let data = self.data.read(cx); - for node in data.nodes().into_iter().rev() { - let element = NodeElement::from_node(&node, NodeVisualState::default()); - let screen_pos = self.state.graph_to_screen(node.position()); - let bounds = Bounds::new(screen_pos, size(DEFAULT_NODE_WIDTH, element.height())); - if bounds.contains(&anchor) { - let local = anchor - screen_pos; - if let Some(port) = element.port_at(local) { - return HitTarget::Port(port); - } - if element.collapse_toggle_hit(local) || element.enable_toggle_hit(local) { - return HitTarget::Toggle(node.id()); - } - return HitTarget::Node(node.id()); - } - } - HitTarget::Background - } + /// Returns what is under `position` (in window space), or + /// [`HitTarget::Background`]. Nodes are tested in reverse paint order so + /// the topmost (last-painted) node wins. + fn hit_test(&self, position: Point, cx: &App) -> HitTarget { + let anchor = position - self.viewport.origin; + let data = self.data.read(cx); + for node in data.nodes().into_iter().rev() { + let element = NodeElement::from_node(&node, NodeVisualState::default()); + let screen_pos = self.state.graph_to_screen(node.position()); + let bounds = Bounds::new(screen_pos, size(DEFAULT_NODE_WIDTH, element.height())); + if bounds.contains(&anchor) { + let local = anchor - screen_pos; + if let Some(port) = element.port_at(local) { + return HitTarget::Port(port); + } + if element.collapse_toggle_hit(local) || element.enable_toggle_hit(local) { + return HitTarget::Toggle(node.id()); + } + return HitTarget::Node(node.id()); + } + } + HitTarget::Background + } - /// Handles a press on a node's body: updates the selection according to - /// modifier keys (plain click selects exclusively, Shift toggles) and - /// begins a potential node drag. Emits - /// [`NodeGraphEvent::SelectionChanged`] when the selection changed. - fn on_node_mouse_down( - &mut self, - node: NodeId, - position: Point, - toggle: bool, - _window: &mut Window, - cx: &mut Context, - ) { - if toggle { - let mut new_selection = self.state.selection().clone(); - if !new_selection.remove(&node) { - new_selection.insert(node); - } - self.set_selection_and_emit(new_selection, cx); - } else if !self.state.is_selected(node) { - self.set_selection_and_emit(BTreeSet::from([node]), cx); - } - self.node_drag = Some(NodeDragState { - nodes: self.state.selection().iter().copied().collect(), - anchor: position - self.viewport.origin, - delta: point(px(0.0), px(0.0)), - }); - cx.notify(); - } + /// Handles a press on a node's body: updates the selection according to + /// modifier keys (plain click selects exclusively, Shift toggles) and + /// begins a potential node drag. Emits + /// [`NodeGraphEvent::SelectionChanged`] when the selection changed. + fn on_node_mouse_down( + &mut self, + node: NodeId, + position: Point, + toggle: bool, + _window: &mut Window, + cx: &mut Context, + ) { + if toggle { + let mut new_selection = self.state.selection().clone(); + if !new_selection.remove(&node) { + new_selection.insert(node); + } + self.set_selection_and_emit(new_selection, cx); + } else if !self.state.is_selected(node) { + self.set_selection_and_emit(BTreeSet::from([node]), cx); + } + self.node_drag = Some(NodeDragState { + nodes: self.state.selection().iter().copied().collect(), + anchor: position - self.viewport.origin, + delta: point(px(0.0), px(0.0)), + }); + cx.notify(); + } - /// Handles pointer movement during a node drag: updates the accumulated - /// drag delta in graph space and emits [`NodeGraphEvent::NodeMovePreview`]. - fn on_node_drag_move(&mut self, window: &mut Window, cx: &mut Context) { - let drag = self.node_drag.as_mut().expect("node drag in progress"); - let cursor = window.mouse_position() - self.viewport.origin; - drag.delta = self.state.screen_to_graph(cursor) - self.state.screen_to_graph(drag.anchor); - let (nodes, delta) = (drag.nodes.clone(), drag.delta); - cx.emit(NodeMovePreview { nodes, delta }); - cx.notify(); - } + /// Handles pointer movement during a node drag: updates the accumulated + /// drag delta in graph space and emits [`NodeGraphEvent::NodeMovePreview`]. + fn on_node_drag_move(&mut self, window: &mut Window, cx: &mut Context) { + let drag = self.node_drag.as_mut().expect("node drag in progress"); + let cursor = window.mouse_position() - self.viewport.origin; + drag.delta = self.state.screen_to_graph(cursor) - self.state.screen_to_graph(drag.anchor); + let (nodes, delta) = (drag.nodes.clone(), drag.delta); + cx.emit(NodeMovePreview { nodes, delta }); + cx.notify(); + } - /// Handles pointer release at the end of a node drag: emits the final - /// [`NodeGraphEvent::NodeMoveRequested`] with the accumulated delta and - /// clears the transient drag state. - fn on_node_drag_end(&mut self, _window: &mut Window, cx: &mut Context) { - if let Some(drag) = self.node_drag.take() { - if drag.delta != point(px(0.0), px(0.0)) { - cx.emit(NodeMoveRequested { - nodes: drag.nodes, - delta: drag.delta, - }); - } - cx.notify(); - } - } + /// Handles pointer release at the end of a node drag: emits the final + /// [`NodeGraphEvent::NodeMoveRequested`] with the accumulated delta and + /// clears the transient drag state. + fn on_node_drag_end(&mut self, _window: &mut Window, cx: &mut Context) { + if let Some(drag) = self.node_drag.take() { + if drag.delta != point(px(0.0), px(0.0)) { + cx.emit(NodeMoveRequested { + nodes: drag.nodes, + delta: drag.delta, + }); + } + cx.notify(); + } + } - /// Begins a wire drag from the given port. If the port is a connected - /// input, the existing edge is "picked up" instead: its other end becomes - /// the drag source and a [`NodeGraphEvent::DisconnectionRequested`] is - /// emitted only if the drag ends without a new connection. - fn begin_wire_drag(&mut self, port: PortId, _window: &mut Window, cx: &mut Context) { - let data = self.data.read(cx); - let mut found: Option<(Point, Point, PortKind, Option)> = - None; - for node in data.nodes() { - let element = NodeElement::from_node(&node, NodeVisualState::default()); - if let Some(anchor) = element.port_anchor(port) { - let kind = if node.inputs().into_iter().any(|p| p.id() == port) { - PortKind::Input - } else { - PortKind::Output - }; - let data_type = node - .inputs() - .into_iter() - .chain(node.outputs()) - .find(|p| p.id() == port) - .map(|p| p.data_type()); - found = Some((node.position(), anchor, kind, data_type)); - break; - } - } - let (node_pos, anchor, kind, data_type) = match found { - Some((node_pos, anchor, kind, Some(data_type))) => (node_pos, anchor, kind, data_type), - _ => return, - }; - let screen_anchor = self.viewport.origin + self.state.graph_to_screen(node_pos + anchor); + /// Begins a wire drag from the given port. If the port is a connected + /// input, the existing edge is "picked up" instead: its other end becomes + /// the drag source and a [`NodeGraphEvent::DisconnectionRequested`] is + /// emitted only if the drag ends without a new connection. + fn begin_wire_drag(&mut self, port: PortId, _window: &mut Window, cx: &mut Context) { + let data = self.data.read(cx); + let mut found: Option<(Point, Point, PortKind, Option)> = + None; + for node in data.nodes() { + let element = NodeElement::from_node(&node, NodeVisualState::default()); + if let Some(anchor) = element.port_anchor(port) { + let kind = if node.inputs().into_iter().any(|p| p.id() == port) { + PortKind::Input + } else { + PortKind::Output + }; + let data_type = node + .inputs() + .into_iter() + .chain(node.outputs()) + .find(|p| p.id() == port) + .map(|p| p.data_type()); + found = Some((node.position(), anchor, kind, data_type)); + break; + } + } + let (node_pos, anchor, kind, data_type) = match found { + Some((node_pos, anchor, kind, Some(data_type))) => (node_pos, anchor, kind, data_type), + _ => return, + }; + let screen_anchor = self.viewport.origin + self.state.graph_to_screen(node_pos + anchor); - // Picking up an existing edge: only a connected input drag re-roots the - // ghost at the far (output) end; an output drag always starts fresh. - if kind == PortKind::Input { - if let Some(edge) = data.edges().into_iter().find(|e| e.to_port() == port) { - if let Some(from_node) = - data.nodes().into_iter().find(|n| n.id() == edge.from_node()) - { - let from_element = - NodeElement::from_node(&from_node, NodeVisualState::default()); - if let Some(far_anchor) = from_element.port_anchor(edge.from_port()) { - let far_screen = self.viewport.origin - + self.state.graph_to_screen(from_node.position() + far_anchor); - if let Some(far_type) = from_node - .outputs() - .into_iter() - .find(|p| p.id() == edge.from_port()) - .map(|p| p.data_type()) - { - self.wire_drag = Some(WireDragState { - ghost: GhostWire::new(far_screen, &far_type, true), - source_port: port, - picked_edge: Some(edge.id()), - valid_ports: BTreeSet::new(), - }); - cx.notify(); - return; - } - } - } - } - } + // Picking up an existing edge: only a connected input drag re-roots the + // ghost at the far (output) end; an output drag always starts fresh. + if kind == PortKind::Input { + if let Some(edge) = data.edges().into_iter().find(|e| e.to_port() == port) { + if let Some(from_node) = data + .nodes() + .into_iter() + .find(|n| n.id() == edge.from_node()) + { + let from_element = + NodeElement::from_node(&from_node, NodeVisualState::default()); + if let Some(far_anchor) = from_element.port_anchor(edge.from_port()) { + let far_screen = self.viewport.origin + + self + .state + .graph_to_screen(from_node.position() + far_anchor); + if let Some(far_type) = from_node + .outputs() + .into_iter() + .find(|p| p.id() == edge.from_port()) + .map(|p| p.data_type()) + { + self.wire_drag = Some(WireDragState { + ghost: GhostWire::new(far_screen, &far_type, true), + source_port: port, + picked_edge: Some(edge.id()), + valid_ports: BTreeSet::new(), + }); + cx.notify(); + return; + } + } + } + } + } - self.wire_drag = Some(WireDragState { - ghost: GhostWire::new(screen_anchor, &data_type, kind == PortKind::Output), - source_port: port, - picked_edge: None, - valid_ports: BTreeSet::new(), - }); - cx.notify(); - } + self.wire_drag = Some(WireDragState { + ghost: GhostWire::new(screen_anchor, &data_type, kind == PortKind::Output), + source_port: port, + picked_edge: None, + valid_ports: BTreeSet::new(), + }); + cx.notify(); + } - /// Updates the wire drag: moves the ghost wire's free end to the cursor - /// and recomputes which ports are valid drop targets by calling - /// [`NodeGraphDataSource::can_connect`] for each port of the opposite - /// kind. Ports that pass are highlighted; the ghost wire is drawn in its - /// invalid state while hovering a port that fails. - fn update_wire_drag(&mut self, window: &mut Window, cx: &mut Context) { - let data = self.data.read(cx); - let drag = self.wire_drag.as_mut().expect("wire drag in progress"); - let from_output = drag.ghost.is_from_output(); - let source_port = drag.source_port; - let picked_edge = drag.picked_edge; - let output_id = if from_output { - picked_edge - .and_then(|edge_id| data.edges().into_iter().find(|e| e.id() == edge_id)) - .map(|edge| edge.from_port()) - .unwrap_or(source_port) - } else { - source_port - }; - let cursor = window.mouse_position(); - let mut valid: BTreeSet = BTreeSet::new(); - let mut snapped: Option> = None; - let mut target_valid = false; - for node in data.nodes() { - let element = NodeElement::from_node(&node, NodeVisualState::default()); - for port in node.inputs().into_iter().chain(node.outputs()) { - let port_id = port.id(); - let candidate = if from_output { - port.kind() == PortKind::Input && data.can_connect(output_id, port_id) - } else { - port.kind() == PortKind::Output && data.can_connect(port_id, source_port) - }; - if candidate { - valid.insert(port_id); - } - if let Some(anchor) = element.port_anchor(port_id) { - let screen = - self.viewport.origin + self.state.graph_to_screen(node.position() + anchor); - let dx = screen.x.0 - cursor.x.0; - let dy = screen.y.0 - cursor.y.0; - if dx * dx + dy * dy <= PORT_GRAB_RADIUS.0 * PORT_GRAB_RADIUS.0 { - snapped = Some(screen); - target_valid = candidate; - } - } - } - } - drag.valid_ports = valid; - drag.ghost.update(cursor, snapped, target_valid); - cx.notify(); - } + /// Updates the wire drag: moves the ghost wire's free end to the cursor + /// and recomputes which ports are valid drop targets by calling + /// [`NodeGraphDataSource::can_connect`] for each port of the opposite + /// kind. Ports that pass are highlighted; the ghost wire is drawn in its + /// invalid state while hovering a port that fails. + fn update_wire_drag(&mut self, window: &mut Window, cx: &mut Context) { + let data = self.data.read(cx); + let drag = self.wire_drag.as_mut().expect("wire drag in progress"); + let from_output = drag.ghost.is_from_output(); + let source_port = drag.source_port; + let picked_edge = drag.picked_edge; + let output_id = if from_output { + picked_edge + .and_then(|edge_id| data.edges().into_iter().find(|e| e.id() == edge_id)) + .map(|edge| edge.from_port()) + .unwrap_or(source_port) + } else { + source_port + }; + let cursor = window.mouse_position(); + let mut valid: BTreeSet = BTreeSet::new(); + let mut snapped: Option> = None; + let mut target_valid = false; + for node in data.nodes() { + let element = NodeElement::from_node(&node, NodeVisualState::default()); + for port in node.inputs().into_iter().chain(node.outputs()) { + let port_id = port.id(); + let candidate = if from_output { + port.kind() == PortKind::Input && data.can_connect(output_id, port_id) + } else { + port.kind() == PortKind::Output && data.can_connect(port_id, source_port) + }; + if candidate { + valid.insert(port_id); + } + if let Some(anchor) = element.port_anchor(port_id) { + let screen = + self.viewport.origin + self.state.graph_to_screen(node.position() + anchor); + let dx = screen.x.0 - cursor.x.0; + let dy = screen.y.0 - cursor.y.0; + if dx * dx + dy * dy <= PORT_GRAB_RADIUS.0 * PORT_GRAB_RADIUS.0 { + snapped = Some(screen); + target_valid = candidate; + } + } + } + } + drag.valid_ports = valid; + drag.ghost.update(cursor, snapped, target_valid); + cx.notify(); + } - /// Ends the wire drag. On a compatible port: emits - /// [`NodeGraphEvent::ConnectionRequested`]. On empty space: cancels and - /// emits [`NodeGraphEvent::BackgroundClicked`] at the drop position so - /// the app may open an "add node" menu pre-wired to the dragged port. On - /// an incompatible port (or back on the source port): cancels silently. - fn end_wire_drag(&mut self, window: &mut Window, cx: &mut Context) { - let drag = match self.wire_drag.take() { - Some(drag) => drag, - None => return, - }; - let data = self.data.read(cx); - let from_output = drag.ghost.is_from_output(); - let source_port = drag.source_port; - let picked_edge = drag.picked_edge; - let output_id = if from_output { - picked_edge - .and_then(|edge_id| data.edges().into_iter().find(|e| e.id() == edge_id)) - .map(|edge| edge.from_port()) - .unwrap_or(source_port) - } else { - source_port - }; - let cursor = window.mouse_position(); - let mut hit: Option<(PortId, bool)> = None; - 'ports: for node in data.nodes() { - let element = NodeElement::from_node(&node, NodeVisualState::default()); - for port in node.inputs().into_iter().chain(node.outputs()) { - let port_id = port.id(); - let candidate = if from_output { - port.kind() == PortKind::Input && data.can_connect(output_id, port_id) - } else { - port.kind() == PortKind::Output && data.can_connect(port_id, source_port) - }; - if let Some(anchor) = element.port_anchor(port_id) { - let screen = self.viewport.origin - + self.state.graph_to_screen(node.position() + anchor); - let dx = screen.x.0 - cursor.x.0; - let dy = screen.y.0 - cursor.y.0; - if dx * dx + dy * dy <= PORT_GRAB_RADIUS.0 * PORT_GRAB_RADIUS.0 { - hit = Some((port_id, candidate)); - break 'ports; - } - } - } - } - match hit { - Some((target, true)) if target != source_port => { - cx.emit(ConnectionRequested { - from: output_id, - to: target, - }); - } - // An incompatible port or the port the drag started from: cancel. - Some(_) => {} - None => { - if let Some(edge) = picked_edge { - cx.emit(DisconnectionRequested { edge }); - } else { - cx.emit(BackgroundClicked { - position: self.state.screen_to_graph(cursor - self.viewport.origin), - }); - } - } - } - cx.notify(); - } + /// Ends the wire drag. On a compatible port: emits + /// [`NodeGraphEvent::ConnectionRequested`]. On empty space: cancels and + /// emits [`NodeGraphEvent::BackgroundClicked`] at the drop position so + /// the app may open an "add node" menu pre-wired to the dragged port. On + /// an incompatible port (or back on the source port): cancels silently. + fn end_wire_drag(&mut self, window: &mut Window, cx: &mut Context) { + let drag = match self.wire_drag.take() { + Some(drag) => drag, + None => return, + }; + let data = self.data.read(cx); + let from_output = drag.ghost.is_from_output(); + let source_port = drag.source_port; + let picked_edge = drag.picked_edge; + let output_id = if from_output { + picked_edge + .and_then(|edge_id| data.edges().into_iter().find(|e| e.id() == edge_id)) + .map(|edge| edge.from_port()) + .unwrap_or(source_port) + } else { + source_port + }; + let cursor = window.mouse_position(); + let mut hit: Option<(PortId, bool)> = None; + 'ports: for node in data.nodes() { + let element = NodeElement::from_node(&node, NodeVisualState::default()); + for port in node.inputs().into_iter().chain(node.outputs()) { + let port_id = port.id(); + let candidate = if from_output { + port.kind() == PortKind::Input && data.can_connect(output_id, port_id) + } else { + port.kind() == PortKind::Output && data.can_connect(port_id, source_port) + }; + if let Some(anchor) = element.port_anchor(port_id) { + let screen = + self.viewport.origin + self.state.graph_to_screen(node.position() + anchor); + let dx = screen.x.0 - cursor.x.0; + let dy = screen.y.0 - cursor.y.0; + if dx * dx + dy * dy <= PORT_GRAB_RADIUS.0 * PORT_GRAB_RADIUS.0 { + hit = Some((port_id, candidate)); + break 'ports; + } + } + } + } + match hit { + Some((target, true)) if target != source_port => { + cx.emit(ConnectionRequested { + from: output_id, + to: target, + }); + } + // An incompatible port or the port the drag started from: cancel. + Some(_) => {} + None => { + if let Some(edge) = picked_edge { + cx.emit(DisconnectionRequested { edge }); + } else { + cx.emit(BackgroundClicked { + position: self.state.screen_to_graph(cursor - self.viewport.origin), + }); + } + } + } + cx.notify(); + } - /// Handles background presses: begins panning (space/middle button) or a - /// marquee selection (left button), or emits - /// [`NodeGraphEvent::BackgroundClicked`] on a right click. - fn on_background_mouse_down( - &mut self, - position: Point, - button: MouseButton, - _window: &mut Window, - cx: &mut Context, - ) { - let anchor = position - self.viewport.origin; - if self.space_down || button == MouseButton::Middle { - self.pan_drag = Some(PanDragState { - start_mouse: position, - start_offset: self.state.offset(), - }); - } - if button == MouseButton::Left { - self.state.begin_marquee(anchor); - } - if button == MouseButton::Right { - cx.emit(BackgroundClicked { - position: self.state.screen_to_graph(anchor), - }); - } - cx.notify(); - } + /// Handles background presses: begins panning (space/middle button) or a + /// marquee selection (left button), or emits + /// [`NodeGraphEvent::BackgroundClicked`] on a right click. + fn on_background_mouse_down( + &mut self, + position: Point, + button: MouseButton, + _window: &mut Window, + cx: &mut Context, + ) { + let anchor = position - self.viewport.origin; + if self.space_down || button == MouseButton::Middle { + self.pan_drag = Some(PanDragState { + start_mouse: position, + start_offset: self.state.offset(), + }); + } + if button == MouseButton::Left { + self.state.begin_marquee(anchor); + } + if button == MouseButton::Right { + cx.emit(BackgroundClicked { + position: self.state.screen_to_graph(anchor), + }); + } + cx.notify(); + } - /// Handles pointer movement during a pan drag: repositions the viewport - /// offset and emits [`NodeGraphEvent::ViewChanged`]. - fn on_pan_drag_move(&mut self, position: Point, cx: &mut Context) { - let pan = self.pan_drag.as_ref().expect("pan drag in progress"); - let (start_mouse, start_offset) = (pan.start_mouse, pan.start_offset); - self.state.set_offset(start_offset + (position - start_mouse)); - cx.emit(ViewChanged { - offset: self.state.offset(), - zoom: self.state.zoom(), - }); - cx.notify(); - } + /// Handles pointer movement during a pan drag: repositions the viewport + /// offset and emits [`NodeGraphEvent::ViewChanged`]. + fn on_pan_drag_move(&mut self, position: Point, cx: &mut Context) { + let pan = self.pan_drag.as_ref().expect("pan drag in progress"); + let (start_mouse, start_offset) = (pan.start_mouse, pan.start_offset); + self.state + .set_offset(start_offset + (position - start_mouse)); + cx.emit(ViewChanged { + offset: self.state.offset(), + zoom: self.state.zoom(), + }); + cx.notify(); + } - /// Handles scroll-wheel and pinch gestures: zooms at the cursor via - /// [`GraphViewState::zoom_at`] and emits [`NodeGraphEvent::ViewChanged`]. - fn on_scroll_or_pinch(&mut self, position: Point, factor: f32, cx: &mut Context) { - self.state.zoom_at(position - self.viewport.origin, factor); - cx.emit(ViewChanged { - offset: self.state.offset(), - zoom: self.state.zoom(), - }); - cx.notify(); - } + /// Handles scroll-wheel and pinch gestures: zooms at the cursor via + /// [`GraphViewState::zoom_at`] and emits [`NodeGraphEvent::ViewChanged`]. + fn on_scroll_or_pinch(&mut self, position: Point, factor: f32, cx: &mut Context) { + self.state.zoom_at(position - self.viewport.origin, factor); + cx.emit(ViewChanged { + offset: self.state.offset(), + zoom: self.state.zoom(), + }); + cx.notify(); + } - /// Handles the delete/backspace key: collects the selected nodes plus all - /// edges incident to them and emits [`NodeGraphEvent::DeleteRequested`]. - /// Does nothing with an empty selection. - fn on_delete_key(&mut self, _window: &mut Window, cx: &mut Context) { - let nodes = self.state.selection().iter().copied().collect::>(); - if nodes.is_empty() { - return; - } - let data = self.data.read(cx); - let edges = data - .edges() - .into_iter() - .filter(|edge| { - nodes - .iter() - .any(|node| *node == edge.from_node() || *node == edge.to_node()) - }) - .map(|edge| edge.id()) - .collect::>(); - cx.emit(DeleteRequested { nodes, edges }); - cx.notify(); - } + /// Handles the delete/backspace key: collects the selected nodes plus all + /// edges incident to them and emits [`NodeGraphEvent::DeleteRequested`]. + /// Does nothing with an empty selection. + fn on_delete_key(&mut self, _window: &mut Window, cx: &mut Context) { + let nodes = self.state.selection().iter().copied().collect::>(); + if nodes.is_empty() { + return; + } + let data = self.data.read(cx); + let edges = data + .edges() + .into_iter() + .filter(|edge| { + nodes + .iter() + .any(|node| *node == edge.from_node() || *node == edge.to_node()) + }) + .map(|edge| edge.id()) + .collect::>(); + cx.emit(DeleteRequested { nodes, edges }); + cx.notify(); + } - /// Emits [`NodeGraphEvent::SelectionChanged`] if `new` differs from the - /// current selection, and stores `new`. - fn set_selection_and_emit(&mut self, new: BTreeSet, cx: &mut Context) { - if self.state.selection() == &new { - return; - } - self.state.set_selection(new.clone()); - cx.emit(SelectionChanged { nodes: new }); - } + /// Emits [`NodeGraphEvent::SelectionChanged`] if `new` differs from the + /// current selection, and stores `new`. + fn set_selection_and_emit(&mut self, new: BTreeSet, cx: &mut Context) { + if self.state.selection() == &new { + return; + } + self.state.set_selection(new.clone()); + cx.emit(SelectionChanged { nodes: new }); + } - /// Ends a marquee drag: selects all nodes intersecting the rectangle, or - /// treats the press as a plain background click (clear selection + emit - /// [`NodeGraphEvent::BackgroundClicked`]) when the drag was too small to - /// count. - fn end_marquee_or_click(&mut self, _window: &mut Window, cx: &mut Context) { - let rect = match self.state.end_marquee() { - Some(rect) => rect, - None => return, - }; - let (min, max) = rect.normalized(); - let dragged = (max.x - min.x).0 >= MARQUEE_DRAG_THRESHOLD - || (max.y - min.y).0 >= MARQUEE_DRAG_THRESHOLD; - if dragged { - let g_min = self.state.screen_to_graph(min); - let g_max = self.state.screen_to_graph(max); - let data = self.data.read(cx); - let mut new_selection = BTreeSet::new(); - for node in data.nodes() { - let element = NodeElement::from_node(&node, NodeVisualState::default()); - let pos = node.position(); - if rects_intersect( - g_min, - g_max, - pos, - pos + point(DEFAULT_NODE_WIDTH, element.height()), - ) { - new_selection.insert(node.id()); - } - } - self.set_selection_and_emit(new_selection, cx); - } else { - self.set_selection_and_emit(BTreeSet::new(), cx); - cx.emit(BackgroundClicked { - position: self.state.screen_to_graph(min), - }); - } - cx.notify(); - } + /// Ends a marquee drag: selects all nodes intersecting the rectangle, or + /// treats the press as a plain background click (clear selection + emit + /// [`NodeGraphEvent::BackgroundClicked`]) when the drag was too small to + /// count. + fn end_marquee_or_click(&mut self, _window: &mut Window, cx: &mut Context) { + let rect = match self.state.end_marquee() { + Some(rect) => rect, + None => return, + }; + let (min, max) = rect.normalized(); + let dragged = (max.x - min.x).0 >= MARQUEE_DRAG_THRESHOLD + || (max.y - min.y).0 >= MARQUEE_DRAG_THRESHOLD; + if dragged { + let g_min = self.state.screen_to_graph(min); + let g_max = self.state.screen_to_graph(max); + let data = self.data.read(cx); + let mut new_selection = BTreeSet::new(); + for node in data.nodes() { + let element = NodeElement::from_node(&node, NodeVisualState::default()); + let pos = node.position(); + if rects_intersect( + g_min, + g_max, + pos, + pos + point(DEFAULT_NODE_WIDTH, element.height()), + ) { + new_selection.insert(node.id()); + } + } + self.set_selection_and_emit(new_selection, cx); + } else { + self.set_selection_and_emit(BTreeSet::new(), cx); + cx.emit(BackgroundClicked { + position: self.state.screen_to_graph(min), + }); + } + cx.notify(); + } - /// Snapshot of the frame the canvas is about to paint: nodes (in paint - /// order, dragged nodes last), wires, ghost wire and marquee, all in - /// window space where applicable. - fn build_draw(&self, cx: &mut Context) -> GraphDraw { - let data = self.data.read(cx); - let selection = self.state.selection().clone(); - let drag = self.node_drag.as_ref(); - let wire = self.wire_drag.as_ref(); - let viewport_origin = self.viewport.origin; - let fallback = PortDataType::new("", hsla(0.0, 0.0, 0.5, 1.0)); + /// Snapshot of the frame the canvas is about to paint: nodes (in paint + /// order, dragged nodes last), wires, ghost wire and marquee, all in + /// window space where applicable. + fn build_draw(&self, cx: &mut Context) -> GraphDraw { + let data = self.data.read(cx); + let selection = self.state.selection().clone(); + let drag = self.node_drag.as_ref(); + let wire = self.wire_drag.as_ref(); + let viewport_origin = self.viewport.origin; + let fallback = PortDataType::new("", hsla(0.0, 0.0, 0.5, 1.0)); - let mut port_types: HashMap = HashMap::new(); - let mut elements: HashMap, NodeElement)> = HashMap::new(); - let mut order: Vec = Vec::new(); - let mut top: Vec = Vec::new(); + let mut port_types: HashMap = HashMap::new(); + let mut elements: HashMap, NodeElement)> = HashMap::new(); + let mut order: Vec = Vec::new(); + let mut top: Vec = Vec::new(); - for node in data.nodes() { - let node_id = node.id(); - for port in node.inputs().into_iter().chain(node.outputs()) { - port_types.insert(port.id(), port.data_type()); - } - let has_compatible_port = wire.map_or(false, |w| { - node.inputs() - .into_iter() - .chain(node.outputs()) - .any(|port| w.valid_ports.contains(&port.id())) - }); - let element = NodeElement::from_node( - &node, - NodeVisualState { - selected: selection.contains(&node_id), - has_compatible_port, - }, - ); - let mut pos = node.position(); - if let Some(d) = drag { - if d.nodes.contains(&node_id) { - pos = pos + d.delta; - top.push(node_id); - } else { - order.push(node_id); - } - } else { - order.push(node_id); - } - elements.insert( - node_id, - (viewport_origin + self.state.graph_to_screen(pos), element), - ); - } - order.extend(top); + for node in data.nodes() { + let node_id = node.id(); + for port in node.inputs().into_iter().chain(node.outputs()) { + port_types.insert(port.id(), port.data_type()); + } + let has_compatible_port = wire.map_or(false, |w| { + node.inputs() + .into_iter() + .chain(node.outputs()) + .any(|port| w.valid_ports.contains(&port.id())) + }); + let element = NodeElement::from_node( + &node, + NodeVisualState { + selected: selection.contains(&node_id), + has_compatible_port, + }, + ); + let mut pos = node.position(); + if let Some(d) = drag { + if d.nodes.contains(&node_id) { + pos = pos + d.delta; + top.push(node_id); + } else { + order.push(node_id); + } + } else { + order.push(node_id); + } + elements.insert( + node_id, + (viewport_origin + self.state.graph_to_screen(pos), element), + ); + } + order.extend(top); - let mut wires = Vec::new(); - for edge in data.edges() { - let (from_pos, from_element) = match elements.get(&edge.from_node()) { - Some(entry) => entry, - None => continue, - }; - let (to_pos, to_element) = match elements.get(&edge.to_node()) { - Some(entry) => entry, - None => continue, - }; - let from_anchor = match from_element.port_anchor(edge.from_port()) { - Some(anchor) => anchor, - None => continue, - }; - let to_anchor = match to_element.port_anchor(edge.to_port()) { - Some(anchor) => anchor, - None => continue, - }; - let data_type = port_types - .get(&edge.from_port()) - .or_else(|| port_types.get(&edge.to_port())) - .unwrap_or(&fallback); - let selected = - selection.contains(&edge.from_node()) || selection.contains(&edge.to_node()); - let wire_state = if selected { - WireVisualState::Selected - } else { - WireVisualState::Normal - }; - wires.push(Wire::new( - edge.id(), - *from_pos + from_anchor, - *to_pos + to_anchor, - data_type, - wire_state, - )); - } + let mut wires = Vec::new(); + for edge in data.edges() { + let (from_pos, from_element) = match elements.get(&edge.from_node()) { + Some(entry) => entry, + None => continue, + }; + let (to_pos, to_element) = match elements.get(&edge.to_node()) { + Some(entry) => entry, + None => continue, + }; + let from_anchor = match from_element.port_anchor(edge.from_port()) { + Some(anchor) => anchor, + None => continue, + }; + let to_anchor = match to_element.port_anchor(edge.to_port()) { + Some(anchor) => anchor, + None => continue, + }; + let data_type = port_types + .get(&edge.from_port()) + .or_else(|| port_types.get(&edge.to_port())) + .unwrap_or(&fallback); + let selected = + selection.contains(&edge.from_node()) || selection.contains(&edge.to_node()); + let wire_state = if selected { + WireVisualState::Selected + } else { + WireVisualState::Normal + }; + wires.push(Wire::new( + edge.id(), + *from_pos + from_anchor, + *to_pos + to_anchor, + data_type, + wire_state, + )); + } - let nodes = order - .into_iter() - .map(|id| { - elements - .remove(&id) - .expect("every painted node must have an element") - }) - .collect(); + let nodes = order + .into_iter() + .map(|id| { + elements + .remove(&id) + .expect("every painted node must have an element") + }) + .collect(); - let ghost = wire.map(|d| GhostSnapshot { - from: d.ghost.source(), - to: d.ghost.free_end(), - color: d.ghost.color(), - target_valid: d.ghost.is_target_valid(), - }); + let ghost = wire.map(|d| GhostSnapshot { + from: d.ghost.source(), + to: d.ghost.free_end(), + color: d.ghost.color(), + target_valid: d.ghost.is_target_valid(), + }); - GraphDraw { - nodes, - wires, - ghost, - marquee: self.state.marquee().copied(), - offset: self.state.offset(), - zoom: self.state.zoom(), - } - } + GraphDraw { + nodes, + wires, + ghost, + marquee: self.state.marquee().copied(), + offset: self.state.offset(), + zoom: self.state.zoom(), + } + } - /// Paints a [`GraphDraw`] snapshot: background, grid, wires, nodes, ghost - /// wire and marquee overlay. - fn paint_draw(draw: &GraphDraw, bounds: Bounds, window: &mut Window, cx: &mut App) { - let colors = cx.default_colors().clone(); - window.paint_quad(fill(bounds, Hsla::from(colors.background))); + /// Paints a [`GraphDraw`] snapshot: background, grid, wires, nodes, ghost + /// wire and marquee overlay. + fn paint_draw(draw: &GraphDraw, bounds: Bounds, window: &mut Window, cx: &mut App) { + let colors = cx.default_colors().clone(); + window.paint_quad(fill(bounds, Hsla::from(colors.background))); - // Grid lines. Lines are spaced GRID_SIZE graph pixels apart; a line - // with graph coordinate k lands at screen x = offset.x + k*GRID_SIZE*zoom. - let zoom = draw.zoom; - let x0 = ((-draw.offset.x.0) / (GRID_SIZE * zoom)).floor() as i64; - let x1 = ((bounds.size.width.0 - draw.offset.x.0) / (GRID_SIZE * zoom)).ceil() as i64; - for k in x0..=x1 { - let x = bounds.left() + px(k as f32 * GRID_SIZE * zoom + draw.offset.x.0); - window.paint_quad(fill( - Bounds::new(point(x, bounds.top()), size(px(1.0), bounds.size.height)), - Hsla::from(colors.border).opacity(0.5), - )); - } - let y0 = ((-draw.offset.y.0) / (GRID_SIZE * zoom)).floor() as i64; - let y1 = ((bounds.size.height.0 - draw.offset.y.0) / (GRID_SIZE * zoom)).ceil() as i64; - for k in y0..=y1 { - let y = bounds.top() + px(k as f32 * GRID_SIZE * zoom + draw.offset.y.0); - window.paint_quad(fill( - Bounds::new(point(bounds.left(), y), size(bounds.size.width, px(1.0))), - Hsla::from(colors.border).opacity(0.5), - )); - } + // Grid lines. Lines are spaced GRID_SIZE graph pixels apart; a line + // with graph coordinate k lands at screen x = offset.x + k*GRID_SIZE*zoom. + let zoom = draw.zoom; + let x0 = ((-draw.offset.x.0) / (GRID_SIZE * zoom)).floor() as i64; + let x1 = ((bounds.size.width.0 - draw.offset.x.0) / (GRID_SIZE * zoom)).ceil() as i64; + for k in x0..=x1 { + let x = bounds.left() + px(k as f32 * GRID_SIZE * zoom + draw.offset.x.0); + window.paint_quad(fill( + Bounds::new(point(x, bounds.top()), size(px(1.0), bounds.size.height)), + Hsla::from(colors.border).opacity(0.5), + )); + } + let y0 = ((-draw.offset.y.0) / (GRID_SIZE * zoom)).floor() as i64; + let y1 = ((bounds.size.height.0 - draw.offset.y.0) / (GRID_SIZE * zoom)).ceil() as i64; + for k in y0..=y1 { + let y = bounds.top() + px(k as f32 * GRID_SIZE * zoom + draw.offset.y.0); + window.paint_quad(fill( + Bounds::new(point(bounds.left(), y), size(bounds.size.width, px(1.0))), + Hsla::from(colors.border).opacity(0.5), + )); + } - for wire in &draw.wires { - wire.paint(window, zoom); - } - for (origin, element) in &draw.nodes { - element.paint(*origin, window, cx); - } - if let Some(ghost) = &draw.ghost { - paint_ghost(window, ghost.from, ghost.to, ghost.color, ghost.target_valid, zoom); - } - if let Some(marquee) = &draw.marquee { - let (min, max) = marquee.normalized(); - let marquee_bounds = Bounds::from_corners(bounds.origin + min, bounds.origin + max); - window.paint_quad(fill( - marquee_bounds, - Hsla::from(colors.selected).opacity(0.15), - )); - window.paint_quad(PaintQuad { - bounds: marquee_bounds, - corner_radii: Corners::all(px(0.0)), - background: hsla(0.0, 0.0, 0.0, 0.0).into(), - border_widths: Edges::all(px(1.0)), - border_color: Hsla::from(colors.selected), - border_style: BorderStyle::Solid, - }); - } - } + for wire in &draw.wires { + wire.paint(window, zoom); + } + for (origin, element) in &draw.nodes { + element.paint(*origin, window, cx); + } + if let Some(ghost) = &draw.ghost { + paint_ghost( + window, + ghost.from, + ghost.to, + ghost.color, + ghost.target_valid, + zoom, + ); + } + if let Some(marquee) = &draw.marquee { + let (min, max) = marquee.normalized(); + let marquee_bounds = Bounds::from_corners(bounds.origin + min, bounds.origin + max); + window.paint_quad(fill( + marquee_bounds, + Hsla::from(colors.selected).opacity(0.15), + )); + window.paint_quad(PaintQuad { + bounds: marquee_bounds, + corner_radii: Corners::all(px(0.0)), + background: hsla(0.0, 0.0, 0.0, 0.0).into(), + border_widths: Edges::all(px(1.0)), + border_color: Hsla::from(colors.selected), + border_style: BorderStyle::Solid, + }); + } + } } impl EventEmitter for NodeGraphView {} impl Focusable for NodeGraphView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for NodeGraphView { - /// Renders the graph: a full-size background layer (grid + pan/zoom - /// handlers), then wires below nodes in graph-space order, then the - /// marquee rectangle and the ghost wire as overlays. - /// - /// Layout/painting is done in screen space; node and wire geometry is - /// computed by mapping graph-space model coordinates through - /// [`GraphViewState::graph_to_screen`]. Wire anchors come from - /// [`NodeElement::port_anchor`](crate::node_graph::NodeElement::port_anchor) - /// so wires always land on port dots. - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle.clone(); - let entity = cx.entity(); + /// Renders the graph: a full-size background layer (grid + pan/zoom + /// handlers), then wires below nodes in graph-space order, then the + /// marquee rectangle and the ghost wire as overlays. + /// + /// Layout/painting is done in screen space; node and wire geometry is + /// computed by mapping graph-space model coordinates through + /// [`GraphViewState::graph_to_screen`]. Wire anchors come from + /// [`NodeElement::port_anchor`](crate::node_graph::NodeElement::port_anchor) + /// so wires always land on port dots. + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle.clone(); + let entity = cx.entity(); - div() - .relative() - .size_full() - .track_focus(&focus_handle) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { - if event.keystroke.key == "space" { - this.space_down = true; - } else if event.keystroke.key == "delete" || event.keystroke.key == "backspace" { - this.on_delete_key(window, cx); - } - })) - .on_key_up(cx.listener(|this, event: &KeyUpEvent, _window, _cx| { - if event.keystroke.key == "space" { - this.space_down = false; - } - })) - .on_mouse_down(MouseButton::Left, cx.listener(|this, event: &MouseDownEvent, window, cx| { - window.focus(&this.focus_handle, cx); - match this.hit_test(event.position, cx) { - HitTarget::Port(port) => this.begin_wire_drag(port, window, cx), - HitTarget::Toggle(node) => { - this.set_selection_and_emit(BTreeSet::from([node]), cx); - cx.notify(); - } - HitTarget::Node(node) => { - this.on_node_mouse_down(node, event.position, event.modifiers.shift, window, cx); - } - HitTarget::Background => { - this.on_background_mouse_down(event.position, MouseButton::Left, window, cx); - } - } - })) - .on_mouse_down(MouseButton::Middle, cx.listener(|this, event: &MouseDownEvent, window, cx| { - window.focus(&this.focus_handle, cx); - this.on_background_mouse_down(event.position, MouseButton::Middle, window, cx); - })) - .on_mouse_down(MouseButton::Right, cx.listener(|this, event: &MouseDownEvent, window, cx| { - window.focus(&this.focus_handle, cx); - if this.hit_test(event.position, cx) == HitTarget::Background { - this.on_background_mouse_down(event.position, MouseButton::Right, window, cx); - } - })) - .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, window, cx| { - if this.node_drag.is_some() { - this.on_node_drag_move(window, cx); - } else if this.wire_drag.is_some() { - this.update_wire_drag(window, cx); - } else if this.pan_drag.is_some() { - this.on_pan_drag_move(event.position, cx); - } else if this.state.marquee().is_some() { - this.state.update_marquee(event.position - this.viewport.origin); - cx.notify(); - } - })) - .capture_any_mouse_up(cx.listener(|this, _event, window, cx| { - if this.node_drag.is_some() { - this.on_node_drag_end(window, cx); - } else if this.wire_drag.is_some() { - this.end_wire_drag(window, cx); - } else if this.pan_drag.is_some() { - this.pan_drag = None; - cx.notify(); - } else { - this.end_marquee_or_click(window, cx); - } - })) - .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { - let factor = match event.delta { - ScrollDelta::Pixels(delta) => 1.0 + delta.y.0 * 0.002, - ScrollDelta::Lines(lines) => 1.0 + lines.y * 0.1, - }; - this.on_scroll_or_pinch(event.position, factor, cx); - })) - .on_pinch(cx.listener(|this, event: &PinchEvent, _window, cx| { - this.on_scroll_or_pinch(event.position, 1.0 + event.delta, cx); - })) - .child(canvas( - move |bounds, _window, cx| { - entity.update(cx, |this, cx| { - this.viewport = bounds; - this.build_draw(cx) - }) - }, - move |bounds, draw: GraphDraw, window, cx| { - NodeGraphView::::paint_draw(&draw, bounds, window, cx); - }, - )) - } + div() + .relative() + .size_full() + .track_focus(&focus_handle) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { + if event.keystroke.key == "space" { + this.space_down = true; + } else if event.keystroke.key == "delete" || event.keystroke.key == "backspace" { + this.on_delete_key(window, cx); + } + })) + .on_key_up(cx.listener(|this, event: &KeyUpEvent, _window, _cx| { + if event.keystroke.key == "space" { + this.space_down = false; + } + })) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, window, cx| { + window.focus(&this.focus_handle, cx); + match this.hit_test(event.position, cx) { + HitTarget::Port(port) => this.begin_wire_drag(port, window, cx), + HitTarget::Toggle(node) => { + this.set_selection_and_emit(BTreeSet::from([node]), cx); + cx.notify(); + } + HitTarget::Node(node) => { + this.on_node_mouse_down( + node, + event.position, + event.modifiers.shift, + window, + cx, + ); + } + HitTarget::Background => { + this.on_background_mouse_down( + event.position, + MouseButton::Left, + window, + cx, + ); + } + } + }), + ) + .on_mouse_down( + MouseButton::Middle, + cx.listener(|this, event: &MouseDownEvent, window, cx| { + window.focus(&this.focus_handle, cx); + this.on_background_mouse_down(event.position, MouseButton::Middle, window, cx); + }), + ) + .on_mouse_down( + MouseButton::Right, + cx.listener(|this, event: &MouseDownEvent, window, cx| { + window.focus(&this.focus_handle, cx); + if this.hit_test(event.position, cx) == HitTarget::Background { + this.on_background_mouse_down( + event.position, + MouseButton::Right, + window, + cx, + ); + } + }), + ) + .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, window, cx| { + if this.node_drag.is_some() { + this.on_node_drag_move(window, cx); + } else if this.wire_drag.is_some() { + this.update_wire_drag(window, cx); + } else if this.pan_drag.is_some() { + this.on_pan_drag_move(event.position, cx); + } else if this.state.marquee().is_some() { + this.state + .update_marquee(event.position - this.viewport.origin); + cx.notify(); + } + })) + .capture_any_mouse_up(cx.listener(|this, _event, window, cx| { + if this.node_drag.is_some() { + this.on_node_drag_end(window, cx); + } else if this.wire_drag.is_some() { + this.end_wire_drag(window, cx); + } else if this.pan_drag.is_some() { + this.pan_drag = None; + cx.notify(); + } else { + this.end_marquee_or_click(window, cx); + } + })) + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { + let factor = match event.delta { + ScrollDelta::Pixels(delta) => 1.0 + delta.y.0 * 0.002, + ScrollDelta::Lines(lines) => 1.0 + lines.y * 0.1, + }; + this.on_scroll_or_pinch(event.position, factor, cx); + })) + .on_pinch(cx.listener(|this, event: &PinchEvent, _window, cx| { + this.on_scroll_or_pinch(event.position, 1.0 + event.delta, cx); + })) + .child(canvas( + move |bounds, _window, cx| { + entity.update(cx, |this, cx| { + this.viewport = bounds; + this.build_draw(cx) + }) + }, + move |bounds, draw: GraphDraw, window, cx| { + NodeGraphView::::paint_draw(&draw, bounds, window, cx); + }, + )) + } } diff --git a/crates/gpui/src/node_graph/minimap.rs b/crates/gpui/src/node_graph/minimap.rs index 67e41b2f39..745f3699ab 100644 --- a/crates/gpui/src/node_graph/minimap.rs +++ b/crates/gpui/src/node_graph/minimap.rs @@ -19,10 +19,13 @@ // lands. #![allow(clippy::extra_unused_type_parameters)] -use crate::{Bounds, Empty, IntoElement, Pixels, Point, Window, canvas, deferred, fill, hsla, point, px, size}; +use crate::{ + Bounds, Empty, IntoElement, Pixels, Point, Window, canvas, deferred, fill, hsla, point, px, + size, +}; use crate::node_graph::{ - DEFAULT_NODE_WIDTH, GraphViewState, NodeElement, NodeGraphDataSource, NodeData, NodeVisualState, + DEFAULT_NODE_WIDTH, GraphViewState, NodeData, NodeElement, NodeGraphDataSource, NodeVisualState, }; /// Scale factor from graph-space coordinates to minimap coordinates. @@ -35,118 +38,118 @@ pub const MINIMAP_CONTENT_SCALE: f32 = 0.15; /// minimap draws no node rectangles and does not react to clicks; it is a /// passive viewport indicator. pub struct GraphMinimap { - /// Whether the minimap is shown. Toggled by the app's view menu; the - /// minimap renders nothing and ignores input when `false`. - visible: bool, + /// Whether the minimap is shown. Toggled by the app's view menu; the + /// minimap renders nothing and ignores input when `false`. + visible: bool, } impl Default for GraphMinimap { - fn default() -> Self { - Self { visible: true } - } + fn default() -> Self { + Self { visible: true } + } } impl GraphMinimap { - /// Creates a visible minimap overlay. - pub fn new() -> Self { - Self::default() - } + /// Creates a visible minimap overlay. + pub fn new() -> Self { + Self::default() + } - /// Shows or hides the minimap. - pub fn set_visible(&mut self, visible: bool) { - self.visible = visible; - } + /// Shows or hides the minimap. + pub fn set_visible(&mut self, visible: bool) { + self.visible = visible; + } - /// Returns whether the minimap is currently shown. - pub fn is_visible(&self) -> bool { - self.visible - } + /// Returns whether the minimap is currently shown. + pub fn is_visible(&self) -> bool { + self.visible + } - /// Computes the axis-aligned bounding box of all nodes in graph space, - /// used as the minimap's content rect. Returns `None` for an empty - /// graph (the minimap then renders only its backdrop). - /// - /// Node extents are derived from each node's position plus its rendered - /// card size ([`DEFAULT_NODE_WIDTH`] × [`NodeElement::height`]). - fn graph_bounds(data: &D) -> Option<(Point, Point)> { - let mut nodes = data.nodes().into_iter(); - let first = nodes.next()?; - let extent = |node: &D::Node| { - let pos = node.position(); - ( - pos, - pos + point( - DEFAULT_NODE_WIDTH, - NodeElement::from_node(node, NodeVisualState::default()).height(), - ), - ) - }; - let (mut min, mut max) = extent(&first); - for node in nodes { - let (node_min, node_max) = extent(&node); - min = min.min(&node_min); - max = max.max(&node_max); - } - Some((min, max)) - } + /// Computes the axis-aligned bounding box of all nodes in graph space, + /// used as the minimap's content rect. Returns `None` for an empty + /// graph (the minimap then renders only its backdrop). + /// + /// Node extents are derived from each node's position plus its rendered + /// card size ([`DEFAULT_NODE_WIDTH`] × [`NodeElement::height`]). + fn graph_bounds(data: &D) -> Option<(Point, Point)> { + let mut nodes = data.nodes().into_iter(); + let first = nodes.next()?; + let extent = |node: &D::Node| { + let pos = node.position(); + ( + pos, + pos + point( + DEFAULT_NODE_WIDTH, + NodeElement::from_node(node, NodeVisualState::default()).height(), + ), + ) + }; + let (mut min, mut max) = extent(&first); + for node in nodes { + let (node_min, node_max) = extent(&node); + min = min.min(&node_min); + max = max.max(&node_max); + } + Some((min, max)) + } - /// Renders the minimap: a translucent backdrop with a viewport indicator - /// rectangle, laid out over `viewport_bounds` (the main view's - /// screen-space bounds). - /// - /// The viewport rectangle is derived from `state` (offset + zoom): the - /// screen-space viewport is mapped back into graph space (`-offset / zoom` - /// plus `viewport size / zoom`) and then down to minimap scale. - pub fn render( - &mut self, - state: &GraphViewState, - viewport_bounds: Bounds, - _window: &mut Window, - ) -> impl IntoElement { - if !self.visible { - return deferred(Empty); - } - let offset = state.offset(); - let zoom = state.zoom(); - deferred(canvas( - move |_bounds, _window, _cx| MinimapDraw { - offset, - zoom, - viewport_bounds, - }, - move |bounds, draw, window, _cx| { - // Backdrop. - window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.6))); + /// Renders the minimap: a translucent backdrop with a viewport indicator + /// rectangle, laid out over `viewport_bounds` (the main view's + /// screen-space bounds). + /// + /// The viewport rectangle is derived from `state` (offset + zoom): the + /// screen-space viewport is mapped back into graph space (`-offset / zoom` + /// plus `viewport size / zoom`) and then down to minimap scale. + pub fn render( + &mut self, + state: &GraphViewState, + viewport_bounds: Bounds, + _window: &mut Window, + ) -> impl IntoElement { + if !self.visible { + return deferred(Empty); + } + let offset = state.offset(); + let zoom = state.zoom(); + deferred(canvas( + move |_bounds, _window, _cx| MinimapDraw { + offset, + zoom, + viewport_bounds, + }, + move |bounds, draw, window, _cx| { + // Backdrop. + window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.6))); - // Viewport indicator: the graph-space viewport rect (screen - // size scaled back through `zoom`) mapped down to minimap - // scale, positioned at `-offset / zoom`. - let scale = MINIMAP_CONTENT_SCALE; - let origin = bounds.origin - + point( - px(-(draw.offset.x.0 / draw.zoom) * scale), - px(-(draw.offset.y.0 / draw.zoom) * scale), - ); - let vp_size = size( - px(draw.viewport_bounds.size.width.0 / draw.zoom * scale), - px(draw.viewport_bounds.size.height.0 / draw.zoom * scale), - ); - window.paint_quad(fill( - Bounds::new(origin, vp_size), - hsla(0.63, 0.55, 0.55, 0.5), - )); - }, - )) - } + // Viewport indicator: the graph-space viewport rect (screen + // size scaled back through `zoom`) mapped down to minimap + // scale, positioned at `-offset / zoom`. + let scale = MINIMAP_CONTENT_SCALE; + let origin = bounds.origin + + point( + px(-(draw.offset.x.0 / draw.zoom) * scale), + px(-(draw.offset.y.0 / draw.zoom) * scale), + ); + let vp_size = size( + px(draw.viewport_bounds.size.width.0 / draw.zoom * scale), + px(draw.viewport_bounds.size.height.0 / draw.zoom * scale), + ); + window.paint_quad(fill( + Bounds::new(origin, vp_size), + hsla(0.63, 0.55, 0.55, 0.5), + )); + }, + )) + } } /// Per-frame snapshot passed from the canvas prepaint to its paint closure. #[derive(Clone, Copy)] struct MinimapDraw { - /// Pan offset (screen-space position of the graph origin). - offset: Point, - /// Zoom factor. - zoom: f32, - /// The main view's screen-space bounds. - viewport_bounds: Bounds, + /// Pan offset (screen-space position of the graph origin). + offset: Point, + /// Zoom factor. + zoom: f32, + /// The main view's screen-space bounds. + viewport_bounds: Bounds, } diff --git a/crates/gpui/src/node_graph/node_element.rs b/crates/gpui/src/node_graph/node_element.rs index ebd682b42a..2f680313e0 100644 --- a/crates/gpui/src/node_graph/node_element.rs +++ b/crates/gpui/src/node_graph/node_element.rs @@ -36,11 +36,11 @@ //! same function, so anchors and dots can never drift apart. use crate::{ - colors::DefaultColors, App, BorderStyle, Bounds, Corners, Edges, Font, Hsla, PaintQuad, Pixels, - Point, SharedString, TextAlign, TextRun, Window, fill, hsla, point, px, size, + App, BorderStyle, Bounds, Corners, Edges, Font, Hsla, PaintQuad, Pixels, Point, SharedString, + TextAlign, TextRun, Window, colors::DefaultColors, fill, hsla, point, px, size, }; -use crate::node_graph::{data::PortData, NodeData, NodeId, PortId}; +use crate::node_graph::{NodeData, NodeId, PortId, data::PortData}; /// The default width of a node card. Node width is fixed; only the height /// grows with the port count. @@ -61,15 +61,15 @@ pub const PORT_INSET: Pixels = Pixels(8.0); /// Visual state of a node card, supplied by the view at render time. #[derive(Clone, Copy, Debug, Default)] pub struct NodeVisualState { - /// Whether the node is part of the current selection (drawn with a - /// selection outline). - pub selected: bool, - /// Whether a wire drag is in progress and this node contains at least - /// one port that [`NodeGraphDataSource::can_connect`] approved as a drop - /// target (drawn with a subtle glow). - /// - /// [`NodeGraphDataSource::can_connect`]: crate::node_graph::NodeGraphDataSource::can_connect - pub has_compatible_port: bool, + /// Whether the node is part of the current selection (drawn with a + /// selection outline). + pub selected: bool, + /// Whether a wire drag is in progress and this node contains at least + /// one port that [`NodeGraphDataSource::can_connect`] approved as a drop + /// target (drawn with a subtle glow). + /// + /// [`NodeGraphDataSource::can_connect`]: crate::node_graph::NodeGraphDataSource::can_connect + pub has_compatible_port: bool, } /// A single rendered node card. @@ -80,335 +80,346 @@ pub struct NodeVisualState { /// [`NodeGraphView`](crate::node_graph::NodeGraphView), which owns the /// gesture state machine. pub struct NodeElement { - node: NodeId, - title: SharedString, - header_color: Option, - collapsed: bool, - enabled: bool, - visual: NodeVisualState, - inputs: Vec, - outputs: Vec, + node: NodeId, + title: SharedString, + header_color: Option, + collapsed: bool, + enabled: bool, + visual: NodeVisualState, + inputs: Vec, + outputs: Vec, } /// One rendered port row: everything needed to draw a port dot and label /// without re-querying the data source. #[derive(Clone, Debug)] struct PortRow { - id: PortId, - label: SharedString, - color: Hsla, - connected: bool, + id: PortId, + label: SharedString, + color: Hsla, + connected: bool, } impl NodeElement { - /// Builds the element from a node snapshot and its visual state. - /// - /// Reads title, header color, collapse/enable flags and both port columns - /// off `node`. Port rows are taken in the order returned by - /// [`NodeData::inputs`] / [`NodeData::outputs`], which defines their - /// top-to-bottom draw order. - pub fn from_node(node: &N, visual: NodeVisualState) -> Self { - let inputs = node - .inputs() - .into_iter() - .map(|port| PortRow { - id: port.id(), - label: port.label(), - color: port.data_type().color, - connected: port.is_connected(), - }) - .collect(); - let outputs = node - .outputs() - .into_iter() - .map(|port| PortRow { - id: port.id(), - label: port.label(), - color: port.data_type().color, - connected: port.is_connected(), - }) - .collect(); - Self { - node: node.id(), - title: node.title(), - header_color: node.header_color(), - collapsed: node.is_collapsed(), - enabled: node.is_enabled(), - visual, - inputs, - outputs, - } - } + /// Builds the element from a node snapshot and its visual state. + /// + /// Reads title, header color, collapse/enable flags and both port columns + /// off `node`. Port rows are taken in the order returned by + /// [`NodeData::inputs`] / [`NodeData::outputs`], which defines their + /// top-to-bottom draw order. + pub fn from_node(node: &N, visual: NodeVisualState) -> Self { + let inputs = node + .inputs() + .into_iter() + .map(|port| PortRow { + id: port.id(), + label: port.label(), + color: port.data_type().color, + connected: port.is_connected(), + }) + .collect(); + let outputs = node + .outputs() + .into_iter() + .map(|port| PortRow { + id: port.id(), + label: port.label(), + color: port.data_type().color, + connected: port.is_connected(), + }) + .collect(); + Self { + node: node.id(), + title: node.title(), + header_color: node.header_color(), + collapsed: node.is_collapsed(), + enabled: node.is_enabled(), + visual, + inputs, + outputs, + } + } - /// Returns the id of the node this element renders. - pub fn node_id(&self) -> NodeId { - self.node - } + /// Returns the id of the node this element renders. + pub fn node_id(&self) -> NodeId { + self.node + } - /// Returns the total height of the node card: the header plus - /// `max(inputs, outputs)` port rows (zero rows when collapsed). - pub fn height(&self) -> Pixels { - if self.collapsed { - HEADER_HEIGHT - } else { - HEADER_HEIGHT + PORT_ROW_HEIGHT * self.inputs.len().max(self.outputs.len()) as f32 - } - } + /// Returns the total height of the node card: the header plus + /// `max(inputs, outputs)` port rows (zero rows when collapsed). + pub fn height(&self) -> Pixels { + if self.collapsed { + HEADER_HEIGHT + } else { + HEADER_HEIGHT + PORT_ROW_HEIGHT * self.inputs.len().max(self.outputs.len()) as f32 + } + } - /// Computes the node-local anchor point (port dot center) of the given - /// port, per the formula in the [module docs](crate::node_graph::node_element). - /// Wires attach here. - /// - /// Returns `None` when the port is not part of this node or the node is - /// collapsed (collapsed nodes expose no anchors and cannot be - /// connection targets). - /// - /// # Panics - /// - /// Never panics; unknown ports yield `None`. - pub fn port_anchor(&self, port: PortId) -> Option> { - if self.collapsed { - return None; - } - let row_y = |row: usize| HEADER_HEIGHT + PORT_ROW_HEIGHT * row as f32 + PORT_ROW_HEIGHT * 0.5; - if let Some(row) = self.inputs.iter().position(|p| p.id == port) { - return Some(point(PORT_DOT_RADIUS + PORT_INSET, row_y(row))); - } - if let Some(row) = self.outputs.iter().position(|p| p.id == port) { - return Some(point( - DEFAULT_NODE_WIDTH - PORT_DOT_RADIUS - PORT_INSET, - row_y(row), - )); - } - None - } + /// Computes the node-local anchor point (port dot center) of the given + /// port, per the formula in the [module docs](crate::node_graph::node_element). + /// Wires attach here. + /// + /// Returns `None` when the port is not part of this node or the node is + /// collapsed (collapsed nodes expose no anchors and cannot be + /// connection targets). + /// + /// # Panics + /// + /// Never panics; unknown ports yield `None`. + pub fn port_anchor(&self, port: PortId) -> Option> { + if self.collapsed { + return None; + } + let row_y = + |row: usize| HEADER_HEIGHT + PORT_ROW_HEIGHT * row as f32 + PORT_ROW_HEIGHT * 0.5; + if let Some(row) = self.inputs.iter().position(|p| p.id == port) { + return Some(point(PORT_DOT_RADIUS + PORT_INSET, row_y(row))); + } + if let Some(row) = self.outputs.iter().position(|p| p.id == port) { + return Some(point( + DEFAULT_NODE_WIDTH - PORT_DOT_RADIUS - PORT_INSET, + row_y(row), + )); + } + None + } - /// Hit-tests a node-local point against port dots and returns the id of - /// the port whose dot (inflated by a small grab margin) contains it. - /// Used to start wire drags. Header and body hits return `None`. - pub fn port_at(&self, position: Point) -> Option { - let hit_radius = PORT_DOT_RADIUS + px(4.0); - for port in self.inputs.iter().chain(self.outputs.iter()) { - if let Some(anchor) = self.port_anchor(port.id) { - let dx = (position.x - anchor.x).0; - let dy = (position.y - anchor.y).0; - if dx * dx + dy * dy <= hit_radius.0 * hit_radius.0 { - return Some(port.id); - } - } - } - None - } + /// Hit-tests a node-local point against port dots and returns the id of + /// the port whose dot (inflated by a small grab margin) contains it. + /// Used to start wire drags. Header and body hits return `None`. + pub fn port_at(&self, position: Point) -> Option { + let hit_radius = PORT_DOT_RADIUS + px(4.0); + for port in self.inputs.iter().chain(self.outputs.iter()) { + if let Some(anchor) = self.port_anchor(port.id) { + let dx = (position.x - anchor.x).0; + let dy = (position.y - anchor.y).0; + if dx * dx + dy * dy <= hit_radius.0 * hit_radius.0 { + return Some(port.id); + } + } + } + None + } - /// Returns whether a node-local point lands on the collapse toggle in the - /// header. The view uses this to distinguish "toggle collapse" clicks - /// from drag starts. - pub fn collapse_toggle_hit(&self, position: Point) -> bool { - position.x.0 >= 0.0 - && position.x.0 <= HEADER_HEIGHT.0 - && position.y.0 >= 0.0 - && position.y.0 <= HEADER_HEIGHT.0 - } + /// Returns whether a node-local point lands on the collapse toggle in the + /// header. The view uses this to distinguish "toggle collapse" clicks + /// from drag starts. + pub fn collapse_toggle_hit(&self, position: Point) -> bool { + position.x.0 >= 0.0 + && position.x.0 <= HEADER_HEIGHT.0 + && position.y.0 >= 0.0 + && position.y.0 <= HEADER_HEIGHT.0 + } - /// Returns whether a node-local point lands on the enable/bypass toggle - /// in the header. Toggling emits no dedicated event — it is handled like - /// any other edit: the view emits a request and the app flips the flag in - /// its model. - pub fn enable_toggle_hit(&self, position: Point) -> bool { - position.x.0 >= DEFAULT_NODE_WIDTH.0 - HEADER_HEIGHT.0 - && position.x.0 <= DEFAULT_NODE_WIDTH.0 - && position.y.0 >= 0.0 - && position.y.0 <= HEADER_HEIGHT.0 - } + /// Returns whether a node-local point lands on the enable/bypass toggle + /// in the header. Toggling emits no dedicated event — it is handled like + /// any other edit: the view emits a request and the app flips the flag in + /// its model. + pub fn enable_toggle_hit(&self, position: Point) -> bool { + position.x.0 >= DEFAULT_NODE_WIDTH.0 - HEADER_HEIGHT.0 + && position.x.0 <= DEFAULT_NODE_WIDTH.0 + && position.y.0 >= 0.0 + && position.y.0 <= HEADER_HEIGHT.0 + } - /// Paints the node card into the current window layer: header with title - /// and toggles, port dots tinted by data type (filled when connected, - /// hollow otherwise) with labels, selection outline, disabled dimming and - /// the compatible-port glow. `origin` is the card's screen-space top-left - /// corner; all geometry within the card is node-local. - pub(crate) fn paint(&self, origin: Point, window: &mut Window, cx: &mut App) { - let colors = cx.default_colors().clone(); - let bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, self.height())); + /// Paints the node card into the current window layer: header with title + /// and toggles, port dots tinted by data type (filled when connected, + /// hollow otherwise) with labels, selection outline, disabled dimming and + /// the compatible-port glow. `origin` is the card's screen-space top-left + /// corner; all geometry within the card is node-local. + pub(crate) fn paint(&self, origin: Point, window: &mut Window, cx: &mut App) { + let colors = cx.default_colors().clone(); + let bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, self.height())); - // Compatible-port glow: a slightly inflated rect behind the card while - // a wire drag offers at least one valid drop target on this node. - if self.visual.has_compatible_port { - let glow = Bounds::new( - point(origin.x - px(2.0), origin.y - px(2.0)), - size(DEFAULT_NODE_WIDTH + px(4.0), self.height() + px(4.0)), - ); - window.paint_quad(fill(glow, Hsla::from(colors.selected).opacity(0.2))); - } + // Compatible-port glow: a slightly inflated rect behind the card while + // a wire drag offers at least one valid drop target on this node. + if self.visual.has_compatible_port { + let glow = Bounds::new( + point(origin.x - px(2.0), origin.y - px(2.0)), + size(DEFAULT_NODE_WIDTH + px(4.0), self.height() + px(4.0)), + ); + window.paint_quad(fill(glow, Hsla::from(colors.selected).opacity(0.2))); + } - // Card body. - window.paint_quad(fill(bounds, colors.background)); + // Card body. + window.paint_quad(fill(bounds, colors.background)); - // Border quad: transparent fill, themed border (accent when selected). - window.paint_quad(PaintQuad { - bounds, - corner_radii: Corners::all(px(4.0)), - background: hsla(0.0, 0.0, 0.0, 0.0).into(), - border_widths: Edges::all(if self.visual.selected { px(1.5) } else { px(1.0) }), - border_color: if self.visual.selected { - Hsla::from(colors.selected) - } else { - Hsla::from(colors.border) - }, - border_style: BorderStyle::Solid, - }); + // Border quad: transparent fill, themed border (accent when selected). + window.paint_quad(PaintQuad { + bounds, + corner_radii: Corners::all(px(4.0)), + background: hsla(0.0, 0.0, 0.0, 0.0).into(), + border_widths: Edges::all(if self.visual.selected { + px(1.5) + } else { + px(1.0) + }), + border_color: if self.visual.selected { + Hsla::from(colors.selected) + } else { + Hsla::from(colors.border) + }, + border_style: BorderStyle::Solid, + }); - // Header bar with the node's accent color (or the theme container - // color), containing the title and the collapse/enable toggles. - let header_bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, HEADER_HEIGHT)); - window.paint_quad(fill( - header_bounds, - self.header_color.unwrap_or(Hsla::from(colors.container)), - )); + // Header bar with the node's accent color (or the theme container + // color), containing the title and the collapse/enable toggles. + let header_bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, HEADER_HEIGHT)); + window.paint_quad(fill( + header_bounds, + self.header_color.unwrap_or(Hsla::from(colors.container)), + )); - let text_y = bounds.top() + px((HEADER_HEIGHT.0 - 12.0) / 2.0); - paint_text( - window, - cx, - &self.title, - px(12.0), - point(bounds.left() + px(28.0), text_y), - px(12.0), - Hsla::from(colors.text), - TextAlign::Left, - None, - ); + let text_y = bounds.top() + px((HEADER_HEIGHT.0 - 12.0) / 2.0); + paint_text( + window, + cx, + &self.title, + px(12.0), + point(bounds.left() + px(28.0), text_y), + px(12.0), + Hsla::from(colors.text), + TextAlign::Left, + None, + ); - // Collapse toggle: "▶" when collapsed (click to expand), "▼" when - // expanded (click to collapse). - paint_text( - window, - cx, - if self.collapsed { "▶" } else { "▼" }, - px(10.0), - point(bounds.left() + px(10.0), text_y), - px(12.0), - Hsla::from(colors.text), - TextAlign::Left, - None, - ); + // Collapse toggle: "▶" when collapsed (click to expand), "▼" when + // expanded (click to collapse). + paint_text( + window, + cx, + if self.collapsed { "▶" } else { "▼" }, + px(10.0), + point(bounds.left() + px(10.0), text_y), + px(12.0), + Hsla::from(colors.text), + TextAlign::Left, + None, + ); - // Enable toggle glyph (power symbol) on the right edge of the header. - paint_text( - window, - cx, - "⏻", - px(12.0), - point(bounds.right() - px(20.0), text_y), - px(12.0), - Hsla::from(colors.text), - TextAlign::Left, - None, - ); + // Enable toggle glyph (power symbol) on the right edge of the header. + paint_text( + window, + cx, + "⏻", + px(12.0), + point(bounds.right() - px(20.0), text_y), + px(12.0), + Hsla::from(colors.text), + TextAlign::Left, + None, + ); - // Port dots and labels, only when the node is expanded. - if !self.collapsed { - let label_font_size = px(11.0); - let label_height = px(12.0); - for port in self.inputs.iter().chain(self.outputs.iter()) { - let Some(anchor) = self.port_anchor(port.id) else { - continue; - }; - let dot_bounds = Bounds::new( - point(anchor.x - PORT_DOT_RADIUS, anchor.y - PORT_DOT_RADIUS), - size(PORT_DOT_RADIUS * 2.0, PORT_DOT_RADIUS * 2.0), - ); - if port.connected { - // Connected dots are solid tinted circles. - window.paint_quad(PaintQuad { - bounds: dot_bounds, - corner_radii: Corners::all(PORT_DOT_RADIUS), - background: port.color.into(), - border_widths: Edges::all(px(0.0)), - border_color: hsla(0.0, 0.0, 0.0, 0.0), - border_style: BorderStyle::Solid, - }); - } else { - // Unconnected dots are hollow: a tinted ring around the - // card's background color. - window.paint_quad(fill(dot_bounds, port.color)); - let inner = Bounds::new( - point(anchor.x - PORT_DOT_RADIUS + px(2.0), anchor.y - PORT_DOT_RADIUS + px(2.0)), - size(PORT_DOT_RADIUS * 2.0 - px(4.0), PORT_DOT_RADIUS * 2.0 - px(4.0)), - ); - window.paint_quad(fill(inner, colors.background)); - } + // Port dots and labels, only when the node is expanded. + if !self.collapsed { + let label_font_size = px(11.0); + let label_height = px(12.0); + for port in self.inputs.iter().chain(self.outputs.iter()) { + let Some(anchor) = self.port_anchor(port.id) else { + continue; + }; + let dot_bounds = Bounds::new( + point(anchor.x - PORT_DOT_RADIUS, anchor.y - PORT_DOT_RADIUS), + size(PORT_DOT_RADIUS * 2.0, PORT_DOT_RADIUS * 2.0), + ); + if port.connected { + // Connected dots are solid tinted circles. + window.paint_quad(PaintQuad { + bounds: dot_bounds, + corner_radii: Corners::all(PORT_DOT_RADIUS), + background: port.color.into(), + border_widths: Edges::all(px(0.0)), + border_color: hsla(0.0, 0.0, 0.0, 0.0), + border_style: BorderStyle::Solid, + }); + } else { + // Unconnected dots are hollow: a tinted ring around the + // card's background color. + window.paint_quad(fill(dot_bounds, port.color)); + let inner = Bounds::new( + point( + anchor.x - PORT_DOT_RADIUS + px(2.0), + anchor.y - PORT_DOT_RADIUS + px(2.0), + ), + size( + PORT_DOT_RADIUS * 2.0 - px(4.0), + PORT_DOT_RADIUS * 2.0 - px(4.0), + ), + ); + window.paint_quad(fill(inner, colors.background)); + } - if !port.label.is_empty() { - if self.inputs.iter().any(|p| p.id == port.id) { - // Input labels: left-aligned, starting right of the dot. - paint_text( - window, - cx, - &port.label, - label_font_size, - point(anchor.x + PORT_DOT_RADIUS + px(6.0), anchor.y - px(6.0)), - label_height, - Hsla::from(colors.text), - TextAlign::Left, - None, - ); - } else { - // Output labels: right-aligned so they end just left of - // the dot. The box origin sits `align_width` left of the - // dot; the label's right edge lands at the box right. - let align_width = px(100.0); - paint_text( - window, - cx, - &port.label, - label_font_size, - point( - anchor.x - PORT_DOT_RADIUS - px(6.0) - align_width, - anchor.y - px(6.0), - ), - label_height, - Hsla::from(colors.text), - TextAlign::Right, - Some(align_width), - ); - } - } - } - } + if !port.label.is_empty() { + if self.inputs.iter().any(|p| p.id == port.id) { + // Input labels: left-aligned, starting right of the dot. + paint_text( + window, + cx, + &port.label, + label_font_size, + point(anchor.x + PORT_DOT_RADIUS + px(6.0), anchor.y - px(6.0)), + label_height, + Hsla::from(colors.text), + TextAlign::Left, + None, + ); + } else { + // Output labels: right-aligned so they end just left of + // the dot. The box origin sits `align_width` left of the + // dot; the label's right edge lands at the box right. + let align_width = px(100.0); + paint_text( + window, + cx, + &port.label, + label_font_size, + point( + anchor.x - PORT_DOT_RADIUS - px(6.0) - align_width, + anchor.y - px(6.0), + ), + label_height, + Hsla::from(colors.text), + TextAlign::Right, + Some(align_width), + ); + } + } + } + } - // Disabled nodes are dimmed with a dark overlay. - if !self.enabled { - window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.5))); - } - } + // Disabled nodes are dimmed with a dark overlay. + if !self.enabled { + window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.5))); + } + } } /// Shapes and paints a single text line at `origin` (the top-left of the /// line box) with the given font size, line height, alignment and color. fn paint_text( - window: &mut Window, - cx: &mut App, - text: &str, - font_size: Pixels, - origin: Point, - line_height: Pixels, - color: Hsla, - align: TextAlign, - align_width: Option, + window: &mut Window, + cx: &mut App, + text: &str, + font_size: Pixels, + origin: Point, + line_height: Pixels, + color: Hsla, + align: TextAlign, + align_width: Option, ) { - let line = window.text_system().shape_line( - SharedString::from(text), - font_size, - &[TextRun { - len: text.len(), - font: Font::default(), - color, - background_color: None, - underline: None, - strikethrough: None, - letter_spacing: None, - }], - None, - ); - let _ = line.paint(origin, line_height, align, align_width, window, cx); + let line = window.text_system().shape_line( + SharedString::from(text), + font_size, + &[TextRun { + len: text.len(), + font: Font::default(), + color, + background_color: None, + underline: None, + strikethrough: None, + letter_spacing: None, + }], + None, + ); + let _ = line.paint(origin, line_height, align, align_width, window, cx); } diff --git a/crates/gpui/src/node_graph/state.rs b/crates/gpui/src/node_graph/state.rs index d770ea0aad..cadd1a0b09 100644 --- a/crates/gpui/src/node_graph/state.rs +++ b/crates/gpui/src/node_graph/state.rs @@ -39,213 +39,213 @@ pub const MAX_ZOOM: f32 = 4.0; /// ``` #[derive(Clone, Debug)] pub struct GraphViewState { - /// Pan offset in screen space: the screen-space position of the graph - /// origin. Positive values move the graph content down-right. - offset: Point, - /// Zoom factor, always within [`MIN_ZOOM`]..=[`MAX_ZOOM`]. `1.0` is 100%. - zoom: f32, - /// The currently selected nodes. Kept sorted (B-Tree) so that - /// `SelectionChanged` events are deterministic and cheap to diff. - selection: BTreeSet, - /// An in-progress marquee (rubber-band) selection rectangle, in screen - /// space, if the user is currently dragging one. - marquee: Option, + /// Pan offset in screen space: the screen-space position of the graph + /// origin. Positive values move the graph content down-right. + offset: Point, + /// Zoom factor, always within [`MIN_ZOOM`]..=[`MAX_ZOOM`]. `1.0` is 100%. + zoom: f32, + /// The currently selected nodes. Kept sorted (B-Tree) so that + /// `SelectionChanged` events are deterministic and cheap to diff. + selection: BTreeSet, + /// An in-progress marquee (rubber-band) selection rectangle, in screen + /// space, if the user is currently dragging one. + marquee: Option, } impl Default for GraphViewState { - fn default() -> Self { - Self { - offset: point(Pixels::ZERO, Pixels::ZERO), - zoom: 1.0, - selection: BTreeSet::new(), - marquee: None, - } - } + fn default() -> Self { + Self { + offset: point(Pixels::ZERO, Pixels::ZERO), + zoom: 1.0, + selection: BTreeSet::new(), + marquee: None, + } + } } impl GraphViewState { - /// Creates a fresh view state: no pan, 100% zoom, empty selection. - pub fn new() -> Self { - Self::default() - } + /// Creates a fresh view state: no pan, 100% zoom, empty selection. + pub fn new() -> Self { + Self::default() + } - /// Returns the current pan offset (the screen-space position of the - /// graph origin). - pub fn offset(&self) -> Point { - self.offset - } + /// Returns the current pan offset (the screen-space position of the + /// graph origin). + pub fn offset(&self) -> Point { + self.offset + } - /// Sets the pan offset directly. No clamping is applied — the graph is - /// unbounded. - pub fn set_offset(&mut self, offset: Point) { - self.offset = offset; - } + /// Sets the pan offset directly. No clamping is applied — the graph is + /// unbounded. + pub fn set_offset(&mut self, offset: Point) { + self.offset = offset; + } - /// Pans the view by a screen-space delta (typically a drag delta). - pub fn pan_by(&mut self, delta: Point) { - self.offset = self.offset + delta; - } + /// Pans the view by a screen-space delta (typically a drag delta). + pub fn pan_by(&mut self, delta: Point) { + self.offset = self.offset + delta; + } - /// Returns the current zoom factor, guaranteed within - /// [`MIN_ZOOM`]..=[`MAX_ZOOM`]. - pub fn zoom(&self) -> f32 { - self.zoom - } + /// Returns the current zoom factor, guaranteed within + /// [`MIN_ZOOM`]..=[`MAX_ZOOM`]. + pub fn zoom(&self) -> f32 { + self.zoom + } - /// Sets the zoom factor, clamped to [`MIN_ZOOM`]..=[`MAX_ZOOM`]. - /// - /// Unlike [`zoom_at`](Self::zoom_at) this does not preserve any anchor - /// point; the graph origin stays put and content scales around it. - pub fn set_zoom(&mut self, zoom: f32) { - self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM); - } + /// Sets the zoom factor, clamped to [`MIN_ZOOM`]..=[`MAX_ZOOM`]. + /// + /// Unlike [`zoom_at`](Self::zoom_at) this does not preserve any anchor + /// point; the graph origin stays put and content scales around it. + pub fn set_zoom(&mut self, zoom: f32) { + self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM); + } - /// Zooms by `factor` (e.g. `1.1` per scroll step) while keeping the - /// graph point under `anchor` (a screen-space position, usually the - /// cursor) stationary on screen. - /// - /// # Math contract - /// - /// Let `z` be the old zoom and `z' = clamp(z * factor, MIN_ZOOM, - /// MAX_ZOOM)` the new one. The offset is adjusted so that - /// `graph_to_screen(g)` is identical before and after for the graph point - /// `g = screen_to_graph(anchor)`: - /// - /// ```text - /// offset' = anchor - (anchor - offset) * (z' / z) - /// ``` - /// - /// When the zoom is clamped (already at the min/max), `z' == z` and the - /// offset is left untouched — the call is then a no-op. - pub fn zoom_at(&mut self, anchor: Point, factor: f32) { - let new_zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM); - if new_zoom == self.zoom { - return; - } - let scale = new_zoom / self.zoom; - self.offset = point( - anchor.x - (anchor.x - self.offset.x) * scale, - anchor.y - (anchor.y - self.offset.y) * scale, - ); - self.zoom = new_zoom; - } + /// Zooms by `factor` (e.g. `1.1` per scroll step) while keeping the + /// graph point under `anchor` (a screen-space position, usually the + /// cursor) stationary on screen. + /// + /// # Math contract + /// + /// Let `z` be the old zoom and `z' = clamp(z * factor, MIN_ZOOM, + /// MAX_ZOOM)` the new one. The offset is adjusted so that + /// `graph_to_screen(g)` is identical before and after for the graph point + /// `g = screen_to_graph(anchor)`: + /// + /// ```text + /// offset' = anchor - (anchor - offset) * (z' / z) + /// ``` + /// + /// When the zoom is clamped (already at the min/max), `z' == z` and the + /// offset is left untouched — the call is then a no-op. + pub fn zoom_at(&mut self, anchor: Point, factor: f32) { + let new_zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM); + if new_zoom == self.zoom { + return; + } + let scale = new_zoom / self.zoom; + self.offset = point( + anchor.x - (anchor.x - self.offset.x) * scale, + anchor.y - (anchor.y - self.offset.y) * scale, + ); + self.zoom = new_zoom; + } - /// Fits the graph-space rectangle `rect` (typically the union of every - /// node's bounds) into the `viewport` screen-space size: zooms so the - /// rect occupies at most 95% of the viewport (clamped to - /// [`MIN_ZOOM`]..=[`MAX_ZOOM`]) and pans so the rect is centered. - /// - /// No-op when either size is non-positive. Used by hosts for a "fit - /// window" command and as the initial viewport after the first layout. - pub fn fit_to_rect(&mut self, rect: Bounds, viewport: Size) { - const PADDING: f32 = 40.0; - let (rw, rh) = (rect.size.width.0, rect.size.height.0); - let (vw, vh) = (viewport.width.0, viewport.height.0); - if rw <= 0.0 || rh <= 0.0 || vw <= 0.0 || vh <= 0.0 { - return; - } - // Fit the larger axis; the padding keeps a breathing margin. - let zoom = (vw / (rw + PADDING * 2.0)) - .min(vh / (rh + PADDING * 2.0)) - .clamp(MIN_ZOOM, MAX_ZOOM); - // Center the rect: offset = (viewport - rect_size * zoom) / 2 - // - rect_origin * zoom. - self.zoom = zoom; - self.offset = point( - Pixels((vw - rw * zoom) * 0.5 - rect.origin.x.0 * zoom), - Pixels((vh - rh * zoom) * 0.5 - rect.origin.y.0 * zoom), - ); - } + /// Fits the graph-space rectangle `rect` (typically the union of every + /// node's bounds) into the `viewport` screen-space size: zooms so the + /// rect occupies at most 95% of the viewport (clamped to + /// [`MIN_ZOOM`]..=[`MAX_ZOOM`]) and pans so the rect is centered. + /// + /// No-op when either size is non-positive. Used by hosts for a "fit + /// window" command and as the initial viewport after the first layout. + pub fn fit_to_rect(&mut self, rect: Bounds, viewport: Size) { + const PADDING: f32 = 40.0; + let (rw, rh) = (rect.size.width.0, rect.size.height.0); + let (vw, vh) = (viewport.width.0, viewport.height.0); + if rw <= 0.0 || rh <= 0.0 || vw <= 0.0 || vh <= 0.0 { + return; + } + // Fit the larger axis; the padding keeps a breathing margin. + let zoom = (vw / (rw + PADDING * 2.0)) + .min(vh / (rh + PADDING * 2.0)) + .clamp(MIN_ZOOM, MAX_ZOOM); + // Center the rect: offset = (viewport - rect_size * zoom) / 2 + // - rect_origin * zoom. + self.zoom = zoom; + self.offset = point( + Pixels((vw - rw * zoom) * 0.5 - rect.origin.x.0 * zoom), + Pixels((vh - rh * zoom) * 0.5 - rect.origin.y.0 * zoom), + ); + } - /// Maps a graph-space (document) point to screen space: - /// `screen = graph * zoom + offset`. - pub fn graph_to_screen(&self, graph: Point) -> Point { - point( - graph.x * self.zoom + self.offset.x, - graph.y * self.zoom + self.offset.y, - ) - } + /// Maps a graph-space (document) point to screen space: + /// `screen = graph * zoom + offset`. + pub fn graph_to_screen(&self, graph: Point) -> Point { + point( + graph.x * self.zoom + self.offset.x, + graph.y * self.zoom + self.offset.y, + ) + } - /// Maps a screen-space point to graph space: - /// `graph = (screen - offset) / zoom`. This is the exact inverse of - /// [`graph_to_screen`](Self::graph_to_screen). - pub fn screen_to_graph(&self, screen: Point) -> Point { - point( - (screen.x - self.offset.x) / self.zoom, - (screen.y - self.offset.y) / self.zoom, - ) - } + /// Maps a screen-space point to graph space: + /// `graph = (screen - offset) / zoom`. This is the exact inverse of + /// [`graph_to_screen`](Self::graph_to_screen). + pub fn screen_to_graph(&self, screen: Point) -> Point { + point( + (screen.x - self.offset.x) / self.zoom, + (screen.y - self.offset.y) / self.zoom, + ) + } - /// Returns the set of currently selected nodes. - pub fn selection(&self) -> &BTreeSet { - &self.selection - } + /// Returns the set of currently selected nodes. + pub fn selection(&self) -> &BTreeSet { + &self.selection + } - /// Returns whether the given node is currently selected. - pub fn is_selected(&self, node: NodeId) -> bool { - self.selection.contains(&node) - } + /// Returns whether the given node is currently selected. + pub fn is_selected(&self, node: NodeId) -> bool { + self.selection.contains(&node) + } - /// Replaces the selection with exactly the given nodes. - /// - /// The view compares before/after and emits - /// [`NodeGraphEvent::SelectionChanged`](crate::node_graph::NodeGraphEvent::SelectionChanged) - /// when the set actually changed; calling this directly does not emit - /// events on its own. - pub fn set_selection(&mut self, nodes: impl IntoIterator) { - self.selection = nodes.into_iter().collect(); - } + /// Replaces the selection with exactly the given nodes. + /// + /// The view compares before/after and emits + /// [`NodeGraphEvent::SelectionChanged`](crate::node_graph::NodeGraphEvent::SelectionChanged) + /// when the set actually changed; calling this directly does not emit + /// events on its own. + pub fn set_selection(&mut self, nodes: impl IntoIterator) { + self.selection = nodes.into_iter().collect(); + } - /// Adds `node` to the selection (shift-click semantics). - pub fn select(&mut self, node: NodeId) { - self.selection.insert(node); - } + /// Adds `node` to the selection (shift-click semantics). + pub fn select(&mut self, node: NodeId) { + self.selection.insert(node); + } - /// Removes `node` from the selection; returns whether it was selected. - pub fn deselect(&mut self, node: NodeId) -> bool { - self.selection.remove(&node) - } + /// Removes `node` from the selection; returns whether it was selected. + pub fn deselect(&mut self, node: NodeId) -> bool { + self.selection.remove(&node) + } - /// Toggles `node` in the selection (shift-click toggle semantics). - pub fn toggle_selection(&mut self, node: NodeId) { - if !self.deselect(node) { - self.select(node); - } - } + /// Toggles `node` in the selection (shift-click toggle semantics). + pub fn toggle_selection(&mut self, node: NodeId) { + if !self.deselect(node) { + self.select(node); + } + } - /// Clears the selection. - pub fn clear_selection(&mut self) { - self.selection.clear(); - } + /// Clears the selection. + pub fn clear_selection(&mut self) { + self.selection.clear(); + } - /// Returns the in-progress marquee selection rectangle, if any. - pub fn marquee(&self) -> Option<&SelectionRect> { - self.marquee.as_ref() - } + /// Returns the in-progress marquee selection rectangle, if any. + pub fn marquee(&self) -> Option<&SelectionRect> { + self.marquee.as_ref() + } - /// Begins a marquee selection anchored at the given screen-space point. - pub fn begin_marquee(&mut self, anchor: Point) { - self.marquee = Some(SelectionRect { - anchor, - current: anchor, - }); - } + /// Begins a marquee selection anchored at the given screen-space point. + pub fn begin_marquee(&mut self, anchor: Point) { + self.marquee = Some(SelectionRect { + anchor, + current: anchor, + }); + } - /// Updates the current corner of the in-progress marquee. Does nothing if - /// no marquee is in progress. - pub fn update_marquee(&mut self, current: Point) { - if let Some(marquee) = &mut self.marquee { - marquee.current = current; - } - } + /// Updates the current corner of the in-progress marquee. Does nothing if + /// no marquee is in progress. + pub fn update_marquee(&mut self, current: Point) { + if let Some(marquee) = &mut self.marquee { + marquee.current = current; + } + } - /// Ends the marquee and returns it, or `None` if none was in progress. - /// - /// The caller (the view) converts the rect to graph space and selects all - /// nodes intersecting it. - pub fn end_marquee(&mut self) -> Option { - self.marquee.take() - } + /// Ends the marquee and returns it, or `None` if none was in progress. + /// + /// The caller (the view) converts the rect to graph space and selects all + /// nodes intersecting it. + pub fn end_marquee(&mut self) -> Option { + self.marquee.take() + } } /// A marquee (rubber-band) selection rectangle in screen space. @@ -255,78 +255,92 @@ impl GraphViewState { /// well-ordered rect regardless of drag direction. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SelectionRect { - /// The screen-space point where the marquee drag started. - pub anchor: Point, - /// The current screen-space corner (usually the cursor position). - pub current: Point, + /// The screen-space point where the marquee drag started. + pub anchor: Point, + /// The current screen-space corner (usually the cursor position). + pub current: Point, } impl SelectionRect { - /// Returns the axis-aligned rectangle with `min` as the top-left and - /// `max` as the bottom-right corner, independent of drag direction. - pub fn normalized(&self) -> (Point, Point) { - let min = point(self.anchor.x.min(self.current.x), self.anchor.y.min(self.current.y)); - let max = point(self.anchor.x.max(self.current.x), self.anchor.y.max(self.current.y)); - (min, max) - } + /// Returns the axis-aligned rectangle with `min` as the top-left and + /// `max` as the bottom-right corner, independent of drag direction. + pub fn normalized(&self) -> (Point, Point) { + let min = point( + self.anchor.x.min(self.current.x), + self.anchor.y.min(self.current.y), + ); + let max = point( + self.anchor.x.max(self.current.x), + self.anchor.y.max(self.current.y), + ); + (min, max) + } } #[cfg(test)] mod tests { - use super::*; - use crate::{px, size}; + use super::*; + use crate::{px, size}; - /// Fitting a graph rect into a viewport centers it and picks a zoom that - /// fits the larger axis; the mapping must stay consistent afterwards. - #[test] - fn fit_centers_and_fits_the_rect() { - let mut state = GraphViewState::new(); - let rect = Bounds::new(point(px(40.0), px(60.0)), size(px(1040.0), px(230.0))); - state.fit_to_rect(rect, size(px(640.0), px(500.0))); + /// Fitting a graph rect into a viewport centers it and picks a zoom that + /// fits the larger axis; the mapping must stay consistent afterwards. + #[test] + fn fit_centers_and_fits_the_rect() { + let mut state = GraphViewState::new(); + let rect = Bounds::new(point(px(40.0), px(60.0)), size(px(1040.0), px(230.0))); + state.fit_to_rect(rect, size(px(640.0), px(500.0))); - // The rect's center must map to the viewport's center. - let graph_center = rect.center(); - let screen_center = state.graph_to_screen(graph_center); - assert!((screen_center.x.0 - 320.0).abs() < 0.5, "x center: {}", screen_center.x.0); - assert!((screen_center.y.0 - 250.0).abs() < 0.5, "y center: {}", screen_center.y.0); + // The rect's center must map to the viewport's center. + let graph_center = rect.center(); + let screen_center = state.graph_to_screen(graph_center); + assert!( + (screen_center.x.0 - 320.0).abs() < 0.5, + "x center: {}", + screen_center.x.0 + ); + assert!( + (screen_center.y.0 - 250.0).abs() < 0.5, + "y center: {}", + screen_center.y.0 + ); - // The fitted rect must fit within the viewport (with the 40px padding). - let top_left = state.graph_to_screen(rect.origin); - let bottom_right = state.graph_to_screen(rect.bottom_right()); - assert!(top_left.x.0 >= 0.0 && bottom_right.x.0 <= 640.0); - assert!(top_left.y.0 >= 0.0 && bottom_right.y.0 <= 500.0); - } + // The fitted rect must fit within the viewport (with the 40px padding). + let top_left = state.graph_to_screen(rect.origin); + let bottom_right = state.graph_to_screen(rect.bottom_right()); + assert!(top_left.x.0 >= 0.0 && bottom_right.x.0 <= 640.0); + assert!(top_left.y.0 >= 0.0 && bottom_right.y.0 <= 500.0); + } - /// The width and height both shrink when the rect is tall and wide - /// (whichever axis is more constraining drives the zoom). - #[test] - fn fit_respects_both_axes() { - let mut state = GraphViewState::new(); - // A wide rect in a narrow viewport: width drives the zoom. - let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(2000.0), px(100.0))); - state.fit_to_rect(rect, size(px(400.0), px(400.0))); - let fitted = state.graph_to_screen(rect.bottom_right()); - assert!(fitted.x.0 <= 400.0 && fitted.y.0 <= 400.0); - assert!(state.zoom() < 1.0); - } + /// The width and height both shrink when the rect is tall and wide + /// (whichever axis is more constraining drives the zoom). + #[test] + fn fit_respects_both_axes() { + let mut state = GraphViewState::new(); + // A wide rect in a narrow viewport: width drives the zoom. + let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(2000.0), px(100.0))); + state.fit_to_rect(rect, size(px(400.0), px(400.0))); + let fitted = state.graph_to_screen(rect.bottom_right()); + assert!(fitted.x.0 <= 400.0 && fitted.y.0 <= 400.0); + assert!(state.zoom() < 1.0); + } - /// A rect smaller than the viewport zooms in (clamped to [`MAX_ZOOM`]). - #[test] - fn fit_zooms_in_for_small_graphs() { - let mut state = GraphViewState::new(); - let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0))); - state.fit_to_rect(rect, size(px(1000.0), px(800.0))); - assert_eq!(state.zoom(), MAX_ZOOM); - } + /// A rect smaller than the viewport zooms in (clamped to [`MAX_ZOOM`]). + #[test] + fn fit_zooms_in_for_small_graphs() { + let mut state = GraphViewState::new(); + let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0))); + state.fit_to_rect(rect, size(px(1000.0), px(800.0))); + assert_eq!(state.zoom(), MAX_ZOOM); + } - /// Non-positive viewport or rect sizes are ignored. - #[test] - fn fit_ignores_non_positive_sizes() { - let mut state = GraphViewState::new(); - let before = state.clone(); - let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0))); - state.fit_to_rect(rect, size(px(0.0), px(800.0))); - assert_eq!(state.zoom(), before.zoom()); - assert_eq!(state.offset(), before.offset()); - } + /// Non-positive viewport or rect sizes are ignored. + #[test] + fn fit_ignores_non_positive_sizes() { + let mut state = GraphViewState::new(); + let before = state.clone(); + let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0))); + state.fit_to_rect(rect, size(px(0.0), px(800.0))); + assert_eq!(state.zoom(), before.zoom()); + assert_eq!(state.offset(), before.offset()); + } } diff --git a/crates/gpui/src/node_graph/wire.rs b/crates/gpui/src/node_graph/wire.rs index a15061a5a5..84f8a67535 100644 --- a/crates/gpui/src/node_graph/wire.rs +++ b/crates/gpui/src/node_graph/wire.rs @@ -18,19 +18,19 @@ pub const WIRE_CURVATURE: Pixels = px(60.0); /// The visual state of a wire, chosen by the view per frame. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum WireVisualState { - /// A regular, idle wire. - #[default] - Normal, - /// The wire is hovered (slightly brightened; click targets become - /// discoverable). - Hovered, - /// The wire is part of the selection (accent color, thicker stroke). - Selected, - /// The ghost wire of an in-progress drag whose current hover target (if - /// any) was rejected by - /// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect). - /// Drawn dashed/red to signal "dropping here will not connect". - InvalidDrag, + /// A regular, idle wire. + #[default] + Normal, + /// The wire is hovered (slightly brightened; click targets become + /// discoverable). + Hovered, + /// The wire is part of the selection (accent color, thicker stroke). + Selected, + /// The ghost wire of an in-progress drag whose current hover target (if + /// any) was rejected by + /// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect). + /// Drawn dashed/red to signal "dropping here will not connect". + InvalidDrag, } /// A fully-resolved wire ready to paint: both endpoints are already computed @@ -40,76 +40,72 @@ pub enum WireVisualState { /// from an [`EdgeData`](crate::node_graph::EdgeData) plus the port anchors of /// the two endpoint nodes. pub struct Wire { - edge: EdgeId, - from: Point, - to: Point, - color: Hsla, - state: WireVisualState, + edge: EdgeId, + from: Point, + to: Point, + color: Hsla, + state: WireVisualState, } impl Wire { - /// Creates a wire between two screen-space anchor points, tinted with the - /// connection's data-type color. - pub fn new( - edge: EdgeId, - from: Point, - to: Point, - data_type: &PortDataType, - state: WireVisualState, - ) -> Self { - Self { - edge, - from, - to, - color: data_type.color, - state, - } - } + /// Creates a wire between two screen-space anchor points, tinted with the + /// connection's data-type color. + pub fn new( + edge: EdgeId, + from: Point, + to: Point, + data_type: &PortDataType, + state: WireVisualState, + ) -> Self { + Self { + edge, + from, + to, + color: data_type.color, + state, + } + } - /// Returns the edge this wire represents. - pub fn edge(&self) -> EdgeId { - self.edge - } + /// Returns the edge this wire represents. + pub fn edge(&self) -> EdgeId { + self.edge + } - /// Builds the cubic bezier [`crate::Path`] for a wire from `from` to - /// `to`, leaving both endpoints horizontally: the control points are - /// placed `WIRE_CURVATURE * zoom` to the right of `from` and to the left - /// of `to`. Shared by regular wires and the ghost wire so both have - /// identical curvature behavior. - /// - /// Returns `None` when the path cannot be built (degenerate input); the - /// caller simply skips painting that frame. - pub fn build_path( - from: Point, - to: Point, - zoom: f32, - ) -> Option> { - wire_path(from, to, zoom, px(2.0), None) - } + /// Builds the cubic bezier [`crate::Path`] for a wire from `from` to + /// `to`, leaving both endpoints horizontally: the control points are + /// placed `WIRE_CURVATURE * zoom` to the right of `from` and to the left + /// of `to`. Shared by regular wires and the ghost wire so both have + /// identical curvature behavior. + /// + /// Returns `None` when the path cannot be built (degenerate input); the + /// caller simply skips painting that frame. + pub fn build_path(from: Point, to: Point, zoom: f32) -> Option> { + wire_path(from, to, zoom, px(2.0), None) + } - /// Paints the wire with [`Window::paint_path`], applying the stroke width - /// and color adjustments implied by its [`WireVisualState`]. - pub fn paint(&self, window: &mut Window, zoom: f32) { - let (color, width, dash) = match self.state { - WireVisualState::Normal => (self.color.opacity(0.6), px(2.0), None), - WireVisualState::Hovered => (self.color, px(2.5), None), - WireVisualState::Selected => (self.color, px(3.0), None), - WireVisualState::InvalidDrag => ( - hsla(0.0, 0.85, 0.55, 1.0), - px(2.0), - Some([px(6.0), px(4.0)]), - ), - }; - if let Some(path) = wire_path( - self.from, - self.to, - zoom, - width, - dash.as_ref().map(|dash| &dash[..]), - ) { - window.paint_path(path, color); - } - } + /// Paints the wire with [`Window::paint_path`], applying the stroke width + /// and color adjustments implied by its [`WireVisualState`]. + pub fn paint(&self, window: &mut Window, zoom: f32) { + let (color, width, dash) = match self.state { + WireVisualState::Normal => (self.color.opacity(0.6), px(2.0), None), + WireVisualState::Hovered => (self.color, px(2.5), None), + WireVisualState::Selected => (self.color, px(3.0), None), + WireVisualState::InvalidDrag => ( + hsla(0.0, 0.85, 0.55, 1.0), + px(2.0), + Some([px(6.0), px(4.0)]), + ), + }; + if let Some(path) = wire_path( + self.from, + self.to, + zoom, + width, + dash.as_ref().map(|dash| &dash[..]), + ) { + window.paint_path(path, color); + } + } } /// Builds the cubic bezier path for a wire stroke with the given width and @@ -117,24 +113,24 @@ impl Wire { /// [`Wire::build_path`] and the ghost wire both delegate to it so every wire /// shares the same curvature behavior. pub(crate) fn wire_path( - from: Point, - to: Point, - zoom: f32, - width: Pixels, - dash: Option<&[Pixels]>, + from: Point, + to: Point, + zoom: f32, + width: Pixels, + dash: Option<&[Pixels]>, ) -> Option> { - let mut builder = PathBuilder::stroke(width); - if let Some(dash) = dash { - builder = builder.dash_array(dash); - } - let curvature = WIRE_CURVATURE * zoom; - builder.move_to(from); - builder.cubic_bezier_to( - to, - point(from.x + curvature, from.y), - point(to.x - curvature, to.y), - ); - builder.build().ok() + let mut builder = PathBuilder::stroke(width); + if let Some(dash) = dash { + builder = builder.dash_array(dash); + } + let curvature = WIRE_CURVATURE * zoom; + builder.move_to(from); + builder.cubic_bezier_to( + to, + point(from.x + curvature, from.y), + point(to.x - curvature, to.y), + ); + builder.build().ok() } /// The transient "ghost" wire shown while the user drags a connection from a @@ -146,89 +142,85 @@ pub(crate) fn wire_path( /// [`WireVisualState::InvalidDrag`] depending on /// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect). pub struct GhostWire { - /// The screen-space anchor of the port the drag started from. - source: Point, - /// The current screen-space position of the free end (cursor, or a - /// snapped hover-target anchor). - free_end: Point, - /// Data type of the source port; tints the ghost. - color: Hsla, - /// Whether the current hover target is a valid drop (drives the - /// [`WireVisualState::InvalidDrag`] styling). - target_valid: bool, - /// Whether the drag started from an output port. When `false` (drag - /// started from an input), `source`/`free_end` are swapped when building - /// the path so the bezier tangents still point the right way. - from_output: bool, + /// The screen-space anchor of the port the drag started from. + source: Point, + /// The current screen-space position of the free end (cursor, or a + /// snapped hover-target anchor). + free_end: Point, + /// Data type of the source port; tints the ghost. + color: Hsla, + /// Whether the current hover target is a valid drop (drives the + /// [`WireVisualState::InvalidDrag`] styling). + target_valid: bool, + /// Whether the drag started from an output port. When `false` (drag + /// started from an input), `source`/`free_end` are swapped when building + /// the path so the bezier tangents still point the right way. + from_output: bool, } impl GhostWire { - /// Creates a ghost wire anchored at `source` (screen space), tinted with - /// the source port's data type. `from_output` records the drag direction; - /// see the field docs. - pub fn new( - source: Point, - data_type: &PortDataType, - from_output: bool, - ) -> Self { - Self { - source, - free_end: source, - color: data_type.color, - target_valid: false, - from_output, - } - } + /// Creates a ghost wire anchored at `source` (screen space), tinted with + /// the source port's data type. `from_output` records the drag direction; + /// see the field docs. + pub fn new(source: Point, data_type: &PortDataType, from_output: bool) -> Self { + Self { + source, + free_end: source, + color: data_type.color, + target_valid: false, + from_output, + } + } - /// Returns the screen-space anchor of the port the drag started from. - pub(crate) fn source(&self) -> Point { - self.source - } + /// Returns the screen-space anchor of the port the drag started from. + pub(crate) fn source(&self) -> Point { + self.source + } - /// Returns the current screen-space position of the free end. - pub(crate) fn free_end(&self) -> Point { - self.free_end - } + /// Returns the current screen-space position of the free end. + pub(crate) fn free_end(&self) -> Point { + self.free_end + } - /// Returns the data-type color tinting the ghost. - pub(crate) fn color(&self) -> Hsla { - self.color - } + /// Returns the data-type color tinting the ghost. + pub(crate) fn color(&self) -> Hsla { + self.color + } - /// Returns whether the currently hovered port is a valid drop target. - pub(crate) fn is_target_valid(&self) -> bool { - self.target_valid - } + /// Returns whether the currently hovered port is a valid drop target. + pub(crate) fn is_target_valid(&self) -> bool { + self.target_valid + } - /// Returns whether the drag started from an output port. - pub(crate) fn is_from_output(&self) -> bool { - self.from_output - } + /// Returns whether the drag started from an output port. + pub(crate) fn is_from_output(&self) -> bool { + self.from_output + } - /// Moves the free end to `cursor` (screen space) and records whether the - /// currently hovered port — if any — is a valid drop target. Pass - /// `snapped = Some(anchor)` instead of the raw cursor when the cursor is - /// inside a port's grab radius, so the ghost visually snaps onto it. - pub fn update( - &mut self, - cursor: Point, - snapped: Option>, - target_valid: bool, - ) { - self.free_end = snapped.unwrap_or(cursor); - self.target_valid = target_valid; - } + /// Moves the free end to `cursor` (screen space) and records whether the + /// currently hovered port — if any — is a valid drop target. Pass + /// `snapped = Some(anchor)` instead of the raw cursor when the cursor is + /// inside a port's grab radius, so the ghost visually snaps onto it. + pub fn update( + &mut self, + cursor: Point, + snapped: Option>, + target_valid: bool, + ) { + self.free_end = snapped.unwrap_or(cursor); + self.target_valid = target_valid; + } - /// Paints the ghost wire using the same bezier shape as [`Wire`], with - /// its state styling. - pub fn paint(&self, window: &mut Window, zoom: f32) { - let (from, to) = if self.from_output { - (self.source, self.free_end) - } else { - (self.free_end, self.source) - }; - paint_ghost(window, from, to, self.color, self.target_valid, zoom); - } + /// Paints the ghost wire using the same bezier shape as [`Wire`], with + /// its state styling. + pub fn paint(&self, window: &mut Window, zoom: f32) { + let (from, to) = if self.from_output { + (self.source, self.free_end) + } else { + (self.free_end, self.source) + }; + paint_ghost(window, from, to, self.color, self.target_valid, zoom); + } } /// Paints the ghost wire between two screen-space anchors. Valid drops are @@ -237,20 +229,20 @@ impl GhostWire { /// connect. Used both by [`GhostWire::paint`] and by the view's frame /// snapshot. pub(crate) fn paint_ghost( - window: &mut Window, - from: Point, - to: Point, - color: Hsla, - target_valid: bool, - zoom: f32, + window: &mut Window, + from: Point, + to: Point, + color: Hsla, + target_valid: bool, + zoom: f32, ) { - if target_valid { - if let Some(path) = wire_path(from, to, zoom, px(2.5), None) { - window.paint_path(path, color); - } - } else if let Some(path) = wire_path(from, to, zoom, px(2.0), Some(&[px(6.0), px(4.0)])) { - window.paint_path(path, hsla(0.0, 0.85, 0.55, 1.0)); - } + if target_valid { + if let Some(path) = wire_path(from, to, zoom, px(2.5), None) { + window.paint_path(path, color); + } + } else if let Some(path) = wire_path(from, to, zoom, px(2.0), Some(&[px(6.0), px(4.0)])) { + window.paint_path(path, hsla(0.0, 0.85, 0.55, 1.0)); + } } /// Pixels per second the [`FlowAnimation`] dash phase advances while active. @@ -267,36 +259,36 @@ pub const FLOW_SPEED: f32 = 60.0; /// their static style while inactive. #[derive(Clone, Debug, Default)] pub struct FlowAnimation { - /// Current dash phase in pixels, monotonically increasing while active. - phase: Pixels, - /// Whether the animation is currently running (e.g. during playback). - active: bool, + /// Current dash phase in pixels, monotonically increasing while active. + phase: Pixels, + /// Whether the animation is currently running (e.g. during playback). + active: bool, } impl FlowAnimation { - /// Starts the flow animation (e.g. when playback begins), resetting the - /// phase to zero. - pub fn start(&mut self) { - self.active = true; - self.phase = px(0.0); - } + /// Starts the flow animation (e.g. when playback begins), resetting the + /// phase to zero. + pub fn start(&mut self) { + self.active = true; + self.phase = px(0.0); + } - /// Stops the flow animation; wires fall back to their static style. - pub fn stop(&mut self) { - self.active = false; - } + /// Stops the flow animation; wires fall back to their static style. + pub fn stop(&mut self) { + self.active = false; + } - /// Advances the phase by one frame. `dt` is the elapsed frame time in - /// seconds; flow speed is a fixed px/s constant. No-op while inactive. - pub fn advance(&mut self, dt: f32) { - if self.active { - self.phase += px(FLOW_SPEED * dt); - } - } + /// Advances the phase by one frame. `dt` is the elapsed frame time in + /// seconds; flow speed is a fixed px/s constant. No-op while inactive. + pub fn advance(&mut self, dt: f32) { + if self.active { + self.phase += px(FLOW_SPEED * dt); + } + } - /// Returns the current dash phase to apply to wire strokes, or `None` - /// while inactive. - pub fn phase(&self) -> Option { - self.active.then_some(self.phase) - } + /// Returns the current dash phase to apply to wire strokes, or `None` + /// while inactive. + pub fn phase(&self) -> Option { + self.active.then_some(self.phase) + } } diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 40a6e71e0a..3b12933226 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -5,7 +5,7 @@ use lyon::math::{Vector, vector}; use lyon::path::traits::SvgPathBuilder; use lyon::path::{ArcFlags, Polygon}; use lyon::tessellation::{ - BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, + BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, }; pub use lyon::math::Transform; @@ -15,333 +15,333 @@ use crate::{Path, Pixels, Point, point, px}; /// Style of the PathBuilder pub enum PathStyle { - /// Stroke style - Stroke(StrokeOptions), - /// Fill style - Fill(FillOptions), + /// 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>, + 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() - } - } + 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() - } - } + 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)) - } + 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) - } + 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) - } + 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) - } + 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, - } - } + 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 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() - } + /// 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 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() - }; + /// 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 - } + 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()); - } + /// 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 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()); - } + /// 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 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(), - ); - } + /// 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(), - ); - } + /// 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, - }); - } + /// 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(); - } + /// 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 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 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 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))); - } - } + /// 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() - }; + /// 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), - } - } + 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(); + 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()), - )?; + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), + )?; - Ok(Self::build_path(buf)) - } + 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(); + 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; - } + 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 - }; + &builder.build() + } else { + path + }; - // Will contain the result of the tessellation. - let mut buf: VertexBuffers = VertexBuffers::new(); - let mut tessellator = StrokeTessellator::new(); + // 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()), - )?; + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), + )?; - Ok(Self::build_path(buf)) - } + 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()); - } + /// 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 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 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]; + 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.push_triangle( + (v0.into(), v1.into(), v2.into()), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } - path - } + path + } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 2fbbea87a5..944912e0a1 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -13,14 +13,14 @@ mod test; mod visual_test; #[cfg(all( - feature = "screen-capture", - any(target_os = "windows", target_os = "linux", target_os = "freebsd",) + 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" + any(target_os = "windows", target_os = "linux"), + feature = "screen-capture" ))] pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; #[cfg(not(feature = "screen-capture"))] @@ -29,12 +29,12 @@ pub(crate) type PlatformScreenCaptureFrame = (); pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer; use crate::{ - Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, - DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, - ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels, - PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, - RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, - SystemWindowTab, Task, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size, + Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, + DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, + ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels, + PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, + RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, + SystemWindowTab, Task, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; #[cfg(any(target_os = "linux", target_os = "freebsd"))] @@ -58,11 +58,11 @@ use std::io::Cursor; use std::ops; use std::time::Duration; use std::{ - fmt::{self, Debug}, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, + fmt::{self, Debug}, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, }; use strum::EnumIter; use uuid::Uuid; @@ -86,226 +86,226 @@ pub use visual_test::VisualTestPlatform; #[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"; - } + 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 = "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; + #[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()); + 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" - } + 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 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); - fn activate(&self, ignoring_other_apps: bool); - fn hide(&self); - fn hide_other_apps(&self); - fn unhide_other_apps(&self); + fn run(&self, on_finish_launching: Box); + fn quit(&self); + fn restart(&self, binary_path: Option); + 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 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 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 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>; + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; - /// Returns the appearance of the application's windows. - fn window_appearance(&self) -> WindowAppearance; + /// Returns the appearance of the application's windows. + fn window_appearance(&self) -> WindowAppearance; - /// Returns the window button layout configuration when supported. - fn button_layout(&self) -> Option { - None - } + /// 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 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 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); - fn on_reopen(&self, callback: Box); + fn on_quit(&self, callback: Box); + fn on_reopen(&self, callback: Box); - fn set_menus(&self, menus: Vec

, keymap: &Keymap); - fn get_menus(&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 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); + fn thermal_state(&self) -> ThermalState; + fn on_thermal_state_change(&self, callback: Box); - fn compositor_name(&self) -> &'static str { - "" - } - fn app_path(&self) -> Result; - fn path_for_auxiliary_executable(&self, name: &str) -> Result; + 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); + 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); + /// 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; + /// Returns whether the mouse cursor is currently visible. + fn is_cursor_visible(&self) -> bool; - fn should_auto_hide_scrollbars(&self) -> bool; + fn should_auto_hide_scrollbars(&self) -> bool; - fn read_from_clipboard(&self) -> Option; - fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_clipboard(&self) -> Option; + fn write_to_clipboard(&self, item: ClipboardItem); - #[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(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); + #[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 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); + fn keyboard_layout(&self) -> Box; + fn keyboard_mapper(&self) -> Rc; + fn on_keyboard_layout_change(&self, callback: Box); - /// Register additional GPU device requirements (features, limits) before - /// the first window is opened. The concrete type inside the `Box` must be - /// `gpui_wgpu::WgpuDeviceRequirements`. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn set_gpu_requirements(&self, _requirements: Box) {} + /// Register additional GPU device requirements (features, limits) before + /// the first window is opened. The concrete type inside the `Box` must be + /// `gpui_wgpu::WgpuDeviceRequirements`. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn set_gpu_requirements(&self, _requirements: Box) {} - /// Sets the label applied to credentials stored in the system keyring. - /// Only Linux/FreeBSD use this label. - fn set_keyring_label(&self, _label: SharedString) {} + /// Sets the label applied to credentials stored in the system keyring. + /// Only Linux/FreeBSD use this label. + fn set_keyring_label(&self, _label: SharedString) {} - /// Whether the current platform supports haptic feedback. - fn supports_haptic_feedback(&self) -> bool { - false - } + /// Whether the current platform supports haptic feedback. + fn supports_haptic_feedback(&self) -> bool { + false + } - /// Play a haptic feedback of the given style. - /// - /// No-op on platforms that don't support haptic feedback. - fn play_haptic_feedback(&self, _style: HapticFeedbackStyle) {} + /// Play a haptic feedback of the given style. + /// + /// No-op on platforms that don't support haptic feedback. + fn play_haptic_feedback(&self, _style: HapticFeedbackStyle) {} } /// 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; + /// 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; + /// 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 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 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); + /// 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) - } + let offset = clipped_window_size / 2.0; + let origin = point(center.x - offset.width, center.y - offset.height); + Bounds::new(origin, clipped_window_size) + } } /// 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, + /// 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, } /// Styles of haptic feedback that can be played via the platform. @@ -314,46 +314,46 @@ pub enum ThermalState { /// values on macOS. On other platforms, all styles are no-ops. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HapticFeedbackStyle { - /// A generic haptic tap — suitable for most interactions. - Generic, - /// A sharp snap — for alignment guides, detents, and snapping. - Alignment, - /// A distinct level-change click — for slider steps, toggles, and - /// discrete state changes. - LevelChange, + /// A generic haptic tap — suitable for most interactions. + Generic, + /// A sharp snap — for alignment guides, detents, and snapping. + Alignment, + /// A distinct level-change click — for slider steps, toggles, and + /// discrete state changes. + LevelChange, } /// 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, + /// 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; + /// 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>>; + /// 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; + /// Returns metadata for this source. + fn metadata(&self) -> Result; } /// A frame of video captured from a screen. @@ -364,128 +364,128 @@ pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); 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) - } + /// 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) - } + fn from(id: u64) -> Self { + Self(id) + } } impl From for u64 { - fn from(id: DisplayId) -> Self { - id.0 - } + 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) - } + 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, + /// 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, + #[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, - }, + /// 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, + /// 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, - } - } + 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, + /// 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", - } - } + /// 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, - } - } + #[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. @@ -497,309 +497,309 @@ pub const MAX_BUTTONS_PER_SIDE: usize = 3; /// (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], + /// 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), - ], - } - } + /// 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 - } + /// 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), - }; + 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(", ") - ); - } + 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) - } + 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(",") - } + /// 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)) - } + 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, + /// 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, - } - } + /// 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 - } + /// 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, + /// 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, + /// Whether a presentation is required. + pub require_presentation: bool, + /// Force refresh of all rendering states when true. + pub force_render: bool, } #[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; - fn prompt( - &self, - level: PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option>; - fn activate(&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 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 completed_frame(&self) {} - fn sprite_atlas(&self) -> Arc; - fn is_subpixel_rendering_supported(&self) -> bool; + 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; + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option>; + fn activate(&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 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 completed_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 show_character_palette(&self) {} - fn titlebar_double_click(&self) {} - 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) {} + // 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 show_character_palette(&self) {} + fn titlebar_double_click(&self) {} + 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; + #[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 start_window_resize(&self, _edge: ResizeEdge) {} - fn set_input_region(&self, _rects: &[Bounds]) {} - fn set_exclusive_zone(&self, _zone: Pixels) {} - 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; + // 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 start_window_resize(&self, _edge: ResizeEdge) {} + fn set_input_region(&self, _rects: &[Bounds]) {} + fn set_exclusive_zone(&self, _zone: Pixels) {} + 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; - /// Returns the GPU context for this window's renderer. - /// The returned `Box` contains `(Arc, Arc)`. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn gpu_context(&self) -> Option> { - None - } + /// Returns the GPU context for this window's renderer. + /// The returned `Box` contains `(Arc, Arc)`. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn gpu_context(&self) -> Option> { + None + } - /// Whether this window's GPU device has been lost (the platform renderer - /// recovers it on a subsequent draw). `None` when the backend cannot - /// know. Safe to call mid-recovery, unlike `gpu_context`. Embedders that - /// captured the device from `gpu_context` should stop submitting while - /// this is `Some(true)` and re-acquire the device once it reads - /// `Some(false)` again. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn gpu_device_lost(&self) -> Option { - None - } + /// Whether this window's GPU device has been lost (the platform renderer + /// recovers it on a subsequent draw). `None` when the backend cannot + /// know. Safe to call mid-recovery, unlike `gpu_context`. Embedders that + /// captured the device from `gpu_context` should stop submitting while + /// this is `Some(true)` and re-acquire the device once it reads + /// `Some(false)` again. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn gpu_device_lost(&self) -> Option { + None + } - fn update_ime_position(&self, _bounds: Bounds); + fn update_ime_position(&self, _bounds: Bounds); - fn play_system_bell(&self) {} + fn play_system_bell(&self) {} - /// Initialize the accessibility adapter with callbacks. - fn a11y_init(&self, _callbacks: A11yCallbacks) {} + /// 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) {} + /// 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) {} + /// Inform the adapter of updated window bounds. + fn a11y_update_window_bounds(&self) {} - #[cfg(any(test, feature = "test-support"))] - fn as_test(&mut self) -> Option<&mut TestWindow> { - None - } + #[cfg(any(test, feature = "test-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") - } + /// 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"))] 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 and return the result as an RGBA image. + fn render_scene_to_image( + &mut self, + scene: &Scene, + size: Size, + ) -> Result; - /// Returns the sprite atlas used by this renderer. - fn sprite_atlas(&self) -> Arc; + /// Returns the sprite atlas used by this renderer. + fn sprite_atlas(&self) -> Arc; } /// Type alias for runnables with metadata. @@ -814,61 +814,61 @@ pub type TimerResolutionGuard = gpui_util::Deferred>; /// be considered part of our public API. #[doc(hidden)] pub trait PlatformDispatcher: Send + Sync { - fn get_all_timings(&self) -> Vec; - fn get_current_thread_timings(&self) -> ThreadTaskTimings; - 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 get_all_timings(&self) -> Vec; + fn get_current_thread_timings(&self) -> ThreadTaskTimings; + 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 spawn_realtime(&self, f: Box); + fn spawn_realtime(&self, f: Box); - fn now(&self) -> Instant { - Instant::now() - } + fn now(&self) -> Instant { + Instant::now() + } - fn increase_timer_resolution(&self) -> TimerResolutionGuard { - gpui_util::defer(Box::new(|| {})) - } + fn increase_timer_resolution(&self) -> TimerResolutionGuard { + gpui_util::defer(Box::new(|| {})) + } - #[cfg(any(test, feature = "test-support"))] - fn as_test(&self) -> Option<&TestDispatcher> { - None - } + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestDispatcher> { + 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; - /// 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 - } + 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; + /// 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)] @@ -876,143 +876,142 @@ pub struct NoopTextSystem; #[expect(missing_docs)] impl NoopTextSystem { - #[allow(dead_code)] - pub fn new() -> Self { - Self - } + #[allow(dead_code)] + pub fn new() -> Self { + Self + } } impl PlatformTextSystem for NoopTextSystem { - fn add_fonts(&self, _fonts: Vec>) -> Result<()> { - Ok(()) - } + fn add_fonts(&self, _fonts: Vec>) -> Result<()> { + Ok(()) + } - fn all_font_names(&self) -> Vec { - Vec::new() - } + fn all_font_names(&self) -> Vec { + Vec::new() + } - fn font_id(&self, _descriptor: &Font) -> Result { - Ok(FontId(1)) - } + 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 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 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 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_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 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 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, font_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 shaped_runs = Vec::default(); - if !glyphs.is_empty() { - shaped_runs.push(ShapedRun { - font_id: FontId(0), - glyphs, - }); - } else { - position = px(0.); - } + fn layout_line(&self, text: &str, font_size: Pixels, font_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 shaped_runs = Vec::default(); + if !glyphs.is_empty() { + shaped_runs.push(ShapedRun { + font_id: FontId(0), + glyphs, + }); + } else { + position = px(0.); + } - let mut tracking = px(0.); - let mut byte_offset = 0usize; - for run in font_runs { - let end = byte_offset.saturating_add(run.len).min(text.len()); - let slice = text.get(byte_offset..end).unwrap_or(""); - let n = slice.chars().count(); - if n > 1 { - if let Some(spacing) = run.letter_spacing { - tracking += spacing * (n - 1) as f32; - } - } - byte_offset = byte_offset.saturating_add(run.len); - } + let mut tracking = px(0.); + let mut byte_offset = 0usize; + for run in font_runs { + let end = byte_offset.saturating_add(run.len).min(text.len()); + let slice = text.get(byte_offset..end).unwrap_or(""); + let n = slice.chars().count(); + if n > 1 { + if let Some(spacing) = run.letter_spacing { + tracking += spacing * (n - 1) as f32; + } + } + byte_offset = byte_offset.saturating_add(run.len); + } - LineLayout { - font_size, - width: position + tracking, - ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), - descent: font_size * (metrics.descent / metrics.units_per_em as f32), - runs: shaped_runs, - len: text.len(), - } - } + LineLayout { + font_size, + width: position + tracking, + ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), + descent: font_size * (metrics.descent / metrics.units_per_em as f32), + runs: shaped_runs, + len: text.len(), + } + } - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - TextRenderingMode::Grayscale - } + 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 @@ -1021,173 +1020,173 @@ impl PlatformTextSystem for NoopTextSystem { /// 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 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; + 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]; + 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, - ] + [ + 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), + 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, - } - } + #[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) - } + fn from(params: RenderGlyphParams) -> Self { + Self::Glyph(params) + } } impl From for AtlasKey { - fn from(params: RenderSvgParams) -> Self { - Self::Svg(params) - } + fn from(params: RenderSvgParams) -> Self { + Self::Svg(params) + } } impl From for AtlasKey { - fn from(params: RenderImageParams) -> Self { - Self::Image(params) - } + 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); + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result>; + fn remove(&self, key: &AtlasKey); } #[doc(hidden)] pub struct AtlasTextureList { - pub textures: Vec>, - pub free_list: Vec, + pub textures: Vec>, + pub free_list: Vec, } impl Default for AtlasTextureList { - fn default() -> Self { - Self { - textures: Vec::default(), - free_list: Vec::default(), - } - } + fn default() -> Self { + Self { + textures: Vec::default(), + free_list: Vec::default(), + } + } } impl ops::Index for AtlasTextureList { - type Output = Option; + type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.textures[index] - } + 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(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() - } + #[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, + /// 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, + // 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) + 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, + Monochrome = 0, + Polychrome = 1, + Subpixel = 2, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -1196,174 +1195,174 @@ pub enum AtlasTextureKind { pub struct TileId(pub u32); impl From for TileId { - fn from(id: etagere::AllocId) -> Self { - Self(id.serialize()) - } + fn from(id: etagere::AllocId) -> Self { + Self(id.serialize()) + } } impl From for etagere::AllocId { - fn from(id: TileId) -> Self { - Self::deserialize(id.0) - } + fn from(id: TileId) -> Self { + Self::deserialize(id.0) + } } #[expect(missing_docs)] pub struct PlatformInputHandler { - cx: AsyncWindowContext, - handler: Box, + 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) + 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 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() - } + 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(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() - } + #[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_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(); - } + 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(); - } + #[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 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() - } + 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() - } + #[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 dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) { + self.handler.replace_text_in_range(None, input, window, cx); + } - pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { - let selection = self.handler.selected_text_range(true, window, cx)?; - self.handler.bounds_for_range( - if selection.reversed { - selection.range.start..selection.range.start - } else { - selection.range.end..selection.range.end - }, - window, - cx, - ) - } + pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { + let selection = self.handler.selected_text_range(true, window, cx)?; + self.handler.bounds_for_range( + if selection.reversed { + selection.range.start..selection.range.start + } else { + selection.range.end..selection.range.end + }, + window, + cx, + ) + } - #[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() - } + #[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() + } - #[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 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) - } + #[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) + } - #[allow(dead_code)] - pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool { - self.cx - .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx)) - .unwrap_or(false) - } + #[allow(dead_code)] + pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool { + self.cx + .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx)) + .unwrap_or(false) + } } /// 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, + /// 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 @@ -1371,317 +1370,317 @@ pub struct UTF16Selection { /// /// 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 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 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; + /// 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 + /// 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, - ); + /// 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); + /// 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); - /// 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 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; + /// 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; - /// 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 - } + /// 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 - } + /// 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 + } - /// 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 - } + /// 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 + } } /// 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, + /// 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, + /// 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 focused when created + pub focus: bool, - /// Whether the window should be shown when created - pub show: bool, + /// Whether the window should be shown when created + pub show: bool, - /// The kind of window to create - pub kind: WindowKind, + /// The kind of window to create + pub kind: WindowKind, - /// Whether the window should be movable by the user - pub is_movable: bool, + /// Whether the window should be movable by the user + pub is_movable: bool, - /// Whether the window should be resizable by the user - pub is_resizable: bool, + /// 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, + /// 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 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, + /// 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, + /// 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>, + /// Window minimum size + pub window_min_size: Option>, - /// Whether to use client or server side decorations. Wayland only - /// Note that this may be ignored. - pub window_decorations: Option, + /// Whether to use client or server side decorations. Wayland only + /// Note that this may be ignored. + pub window_decorations: Option, - /// Icon image (X11 only) - pub icon: 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, + /// 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) + 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, + pub bounds: Bounds, - /// The titlebar configuration of the window - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub titlebar: Option, + /// 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, + /// 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 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 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 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, + /// 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", target_os = "windows"), + allow(dead_code) + )] + pub focus: bool, - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub show: 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>, + /// 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 display_id: Option, - pub window_min_size: Option>, - #[cfg(target_os = "macos")] - pub tabbing_identifier: 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), + /// 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()) - } + 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, - } - } + /// 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)) - } + /// 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, - 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, - } - } + 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, + 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, + /// 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, + /// 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 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 normal application window + Normal, - /// A window that appears above all other windows, usually used for alerts or popups - /// use sparingly! - PopUp, + /// A window that appears above all other windows, usually used for alerts or popups + /// use sparingly! + PopUp, - /// A floating window that appears on top of its parent window - Floating, + /// 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 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, + /// 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. @@ -1690,834 +1689,834 @@ pub enum WindowKind { /// 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. + /// + /// 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 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. + /// + /// On macOS, this corresponds to the `darkAqua` appearance. + Dark, - /// A dark appearance with vibrant colors. - /// - /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. - VibrantDark, + /// 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, + /// 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, + /// 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, + /// 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 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 the user needs to be warned of a potential problem + Warning, - /// A prompt that is shown when a critical problem has occurred - Critical, + /// 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), + /// 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 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 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()) - } + /// 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 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, - } - } + /// 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())), - } - } + 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, + /// The default cursor + #[default] + Arrow, - /// A text input cursor - /// corresponds to the CSS cursor value `text` - IBeam, + /// A text input cursor + /// corresponds to the CSS cursor value `text` + IBeam, - /// A crosshair cursor - /// corresponds to the CSS cursor value `crosshair` - Crosshair, + /// A crosshair cursor + /// corresponds to the CSS cursor value `crosshair` + Crosshair, - /// A closed hand cursor - /// corresponds to the CSS cursor value `grabbing` - ClosedHand, + /// A closed hand cursor + /// corresponds to the CSS cursor value `grabbing` + ClosedHand, - /// An open hand cursor - /// corresponds to the CSS cursor value `grab` - OpenHand, + /// 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 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 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 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 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 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 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 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-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 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/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 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 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 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 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 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 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, + /// The entries in this clipboard item. + pub entries: Vec, } /// 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), + /// 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 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_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::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())], - } - } + /// 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(); + /// 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); - } - } + 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() { + 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 !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, - } - } + /// 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 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() - } + /// 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) - } + fn from(value: ClipboardString) -> Self { + Self::String(value) + } } impl From for ClipboardEntry { - fn from(value: String) -> Self { - Self::from(ClipboardString::from(value)) - } + fn from(value: String) -> Self { + Self::from(ClipboardString::from(value)) + } } impl From for ClipboardEntry { - fn from(value: Image) -> Self { - Self::Image(value) - } + fn from(value: Image) -> Self { + Self::Image(value) + } } impl From for ClipboardItem { - fn from(value: ClipboardEntry) -> Self { - Self { - entries: vec![value], - } - } + fn from(value: ClipboardEntry) -> Self { + Self { + entries: vec![value], + } + } } impl From for ClipboardItem { - fn from(value: String) -> Self { - Self::from(ClipboardEntry::from(value)) - } + fn from(value: String) -> Self { + Self::from(ClipboardEntry::from(value)) + } } impl From for ClipboardItem { - fn from(value: Image) -> Self { - Self::from(ClipboardEntry::from(value)) - } + 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, + // 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 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 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)) - } + /// 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, - } - } + /// 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, + /// 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, } impl Hash for Image { - fn hash(&self, state: &mut H) { - state.write_u64(self.id); - } + 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()) - } + /// 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, - } - } + /// 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 - } + /// 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 `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 `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); - } + /// 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); + } - /// Convert the clipboard image to an `ImageData` object. - pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { - fn frames_for_image( - bytes: &[u8], - format: image::ImageFormat, - ) -> Result> { - let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8(); + /// Convert the clipboard image to an `ImageData` object. + pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { + fn frames_for_image( + bytes: &[u8], + format: image::ImageFormat, + ) -> Result> { + let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8(); - // Convert from RGBA to BGRA. - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } - Ok(SmallVec::from_elem(Frame::new(data), 1)) - } + Ok(SmallVec::from_elem(Frame::new(data), 1)) + } - let frames = match self.format { - ImageFormat::Gif => { - let decoder = GifDecoder::new(Cursor::new(&self.bytes))?; - let mut frames = SmallVec::new(); + 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}"); - } - } - } + 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"); - } + if frames.is_empty() { + anyhow::bail!("GIF could not be decoded: all frames failed"); + } - frames - } - ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?, - ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?, - ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?, - ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, - ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, - ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?, - ImageFormat::Svg => { - return svg_renderer - .render_single_frame(&self.bytes, 1.0) - .map_err(Into::into); - } - ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?, - }; + frames + } + ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?, + ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?, + ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?, + ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, + ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, + ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?, + ImageFormat::Svg => { + return svg_renderer + .render_single_frame(&self.bytes, 1.0) + .map_err(Into::into); + } + ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?, + }; - Ok(Arc::new(RenderImage::new(frames))) - } + Ok(Arc::new(RenderImage::new(frames))) + } - /// Get the format of the clipboard image - pub fn format(&self) -> ImageFormat { - self.format - } + /// 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() - } + /// 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, + /// 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, - } - } + /// 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 - } + /// 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 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 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()) - } + /// 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() - } + #[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, - } - } + fn from(value: String) -> Self { + Self { + text: value, + metadata: None, + } + } } #[cfg(test)] mod image_tests { - use super::*; - use std::sync::Arc; + use super::*; + use std::sync::Arc; - #[test] - fn test_svg_image_to_image_data_converts_to_bgra() { - let image = Image::from_bytes( - ImageFormat::Svg, - br##" + #[test] + fn test_svg_image_to_image_data_converts_to_bgra() { + let image = Image::from_bytes( + ImageFormat::Svg, + br##" "## - .to_vec(), - ); + .to_vec(), + ); - let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); - let bytes = render_image.as_bytes(0).unwrap(); + 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]); - } - } + 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; + 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_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_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_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_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_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_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_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_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 - ] - ); + #[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"); - } + 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_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_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", - ":", - ]; + #[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); - } - } + 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) - ] - ); + #[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); - } + 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()); - } + #[test] + fn test_window_button_layout_parse_all_invalid() { + assert!(WindowButtonLayout::parse("asdfghjkl").is_err()); + } } diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 27c20c00ba..b171dab7d5 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -2,223 +2,223 @@ 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 name of the menu + pub name: SharedString, - /// The items in the menu - pub items: Vec, + /// The items in the menu + pub items: Vec, - /// Whether this menu is disabled - pub disabled: bool, + /// 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, - } - } + /// 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 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 - } + /// 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, - } - } + /// 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 name of the menu + pub name: SharedString, - /// The type of menu - pub menu_type: SystemMenuType, + /// 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, - } - } + /// 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 '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 separator between items + Separator, - /// A submenu - Submenu(Menu), + /// A submenu + Submenu(Menu), - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OsMenu), + /// 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, + /// 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 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, + /// 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 checked + checked: bool, - /// Whether this action is disabled - disabled: 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 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 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 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 + 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, - } - } + /// 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()), - } - } + /// 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 - } + /// 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, - } - } + /// 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 - } + /// 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, - } - } + /// 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 @@ -226,78 +226,78 @@ impl MenuItem { /// these menus #[derive(Clone)] pub struct OwnedOsMenu { - /// The name of the menu - pub name: SharedString, + /// The name of the menu + pub name: SharedString, - /// The type of menu - pub menu_type: SystemMenuType, + /// 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 name of the menu + pub name: SharedString, - /// The items in the menu - pub items: Vec, + /// The items in the menu + pub items: Vec, - /// Whether this menu is disabled - pub disabled: bool, + /// 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 separator between items + Separator, - /// A submenu - Submenu(OwnedMenu), + /// A submenu + Submenu(OwnedMenu), - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OwnedOsMenu), + /// 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, + /// 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 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, + /// 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 checked + checked: bool, - /// Whether this action is disabled - disabled: 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()), - } - } + 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 @@ -309,118 +309,118 @@ impl Clone for OwnedMenuItem { /// these actions #[derive(Copy, Clone, Eq, PartialEq)] pub enum OsAction { - /// The 'cut' action - Cut, + /// The 'cut' action + Cut, - /// The 'copy' action - Copy, + /// The 'copy' action + Copy, - /// The 'paste' action - Paste, + /// The 'paste' action + Paste, - /// The 'select all' action - SelectAll, + /// The 'select all' action + SelectAll, - /// The 'undo' action - Undo, + /// The 'undo' action + Undo, - /// The 'redo' action - Redo, + /// 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_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_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)); - } - } - })); + 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; + 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); + #[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); - } + 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; + #[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, - .. - } - )); + 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() - ); + 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()); - } + 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/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..98c0d60da9 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -4,38 +4,38 @@ 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; + /// 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>; + /// 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 map_key_equivalent( + &self, + keystroke: Keystroke, + _use_key_equivalents: bool, + ) -> KeybindingKeystroke { + KeybindingKeystroke::from_keystroke(keystroke) + } - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } } diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index c45c7c1b33..d660b2980f 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -1,68 +1,68 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ - error::Error, - fmt::{Display, Write}, + 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; + /// 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, + /// 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 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, + /// 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, + /// 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, + /// 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 - ) - } + 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 ..." @@ -71,706 +71,685 @@ pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modif 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() - }; + /// 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; - } - } + 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; - } - } + #[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 - } + 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; + /// 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 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"); + 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; - } + if is_platform { + modifiers.platform = true; + continue; + } - let mut key_str = component.to_string(); + 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 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); - } + 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 - } - }); + // 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(), - })?; + let key = key.ok_or_else(|| InvalidKeystrokeError { + keystroke: source.to_owned(), + })?; - Ok(Keystroke { - modifiers, - key, - key_char, - }) - } + 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) - } + /// 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 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 - } + /// 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, - } - } + #[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 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 } - } - } + /// 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 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 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 - } - } + /// 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 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; - } + /// 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) - } - } + /// 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; - } + /// 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" - ) + !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) - } + 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) - } + 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 control key + #[serde(default)] + pub control: bool, - /// The alt key - /// Sometimes also known as the 'meta' key - #[serde(default)] - pub alt: 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 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 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, + /// 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 - } + /// 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 - } + /// 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 - } - } + #[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 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 no modifiers. + pub fn none() -> Modifiers { + Default::default() + } - /// Returns [`Modifiers`] with just the command key. - pub fn command() -> Modifiers { - Modifiers { - platform: true, - ..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() - } - } + /// 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() - } - } - } + #[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 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 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 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 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 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 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 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() - } - } + /// 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 - } + /// 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; + type Output = Self; - fn bitor(mut self, other: Self) -> Self::Output { - self |= other; - 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; - } + 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 - } + 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; - } + 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 - } + 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; - } + 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, + /// The capslock key is on + #[serde(default)] + pub on: bool, } impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } + fn as_keystroke(&self) -> &Keystroke { + self + } } impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } + 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('^')?; + 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, "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(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(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(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(()) + #[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" => '⌘', + 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) + 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-"); + 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(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 + #[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/src/platform/layer_shell.rs b/crates/gpui/src/platform/layer_shell.rs index 8be1b5fcdb..5fb8ec1c53 100644 --- a/crates/gpui/src/platform/layer_shell.rs +++ b/crates/gpui/src/platform/layer_shell.rs @@ -7,73 +7,73 @@ use crate::Pixels; /// a single layer is undefined. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum Layer { - /// The background layer, typically used for wallpapers. - Background, + /// The background layer, typically used for wallpapers. + Background, - /// The bottom layer. - Bottom, + /// The bottom layer. + Bottom, - /// The top layer, typically used for fullscreen windows. - Top, + /// The top layer, typically used for fullscreen windows. + Top, - /// The overlay layer, used for surfaces that should always be on top. - #[default] - Overlay, + /// 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; - } + /// 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, + /// 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 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, + /// 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, + /// 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 diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index 797e19ba23..df7ee1d997 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -1,7 +1,7 @@ //! Screen capture for Linux and Windows use crate::{ - DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, SourceMetadata, size, + DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + Size, SourceMetadata, size, }; use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; @@ -16,11 +16,11 @@ use std::sync::atomic::{self, AtomicBool}; /// won't return any results. #[allow(dead_code)] pub fn scap_screen_sources( - foreground_executor: &ForegroundExecutor, + 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) + 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 @@ -30,296 +30,296 @@ pub fn scap_screen_sources( /// single screen capture source for their selection. #[allow(dead_code)] pub(crate) fn start_scap_default_target_source( - foreground_executor: &ForegroundExecutor, + 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) + 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, + 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(); - }); + // 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 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(); + 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(); - } - } - }); + // 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) - } + 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, + // 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>>, + 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)) - }); + // 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(); - } - } - }); + 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 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 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, - }) + 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>, + 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(); + 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, + 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, - }) - } + 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 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)) + 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, + 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 + 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, + 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 + 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/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index ef662c6c48..d084e05e11 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -2,11 +2,11 @@ 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, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, }; /// TestDispatcher provides deterministic async execution for tests. @@ -15,147 +15,147 @@ use std::{ /// 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, + 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 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 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 scheduler(&self) -> &Arc { + &self.scheduler + } - pub fn session_id(&self) -> SessionId { - self.session_id - } + pub fn session_id(&self) -> SessionId { + self.session_id + } - pub fn drain_tasks(&self) { - self.scheduler.drain_tasks(); - } + 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(&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 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 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 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 run_until_parked(&self) { + while self.tick(false) {} + } - pub fn allow_parking(&self) { - self.scheduler.allow_parking(); - } + pub fn allow_parking(&self) { + self.scheduler.allow_parking(); + } - pub fn forbid_parking(&self) { - self.scheduler.forbid_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); - } + /// 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), - } - } + /// 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(), - } - } + 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 get_all_timings(&self) -> Vec { - Vec::new() - } + fn get_all_timings(&self) -> Vec { + Vec::new() + } - fn get_current_thread_timings(&self) -> crate::ThreadTaskTimings { - crate::ThreadTaskTimings { - thread_name: None, - thread_id: std::thread::current().id(), - timings: Vec::new(), - total_pushed: 0, - } - } + fn get_current_thread_timings(&self) -> crate::ThreadTaskTimings { + crate::ThreadTaskTimings { + thread_name: None, + thread_id: std::thread::current().id(), + timings: Vec::new(), + total_pushed: 0, + } + } - fn is_main_thread(&self) -> bool { - self.scheduler.is_main_thread() - } + fn is_main_thread(&self) -> bool { + self.scheduler.is_main_thread() + } - fn now(&self) -> Instant { - self.scheduler.clock().now() - } + 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(&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_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. \ + 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 as_test(&self) -> Option<&TestDispatcher> { + Some(self) + } - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - f(); - }); - } + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + f(); + }); + } } diff --git a/crates/gpui/src/platform/test/display.rs b/crates/gpui/src/platform/test/display.rs index c4adb014aa..dee9539f98 100644 --- a/crates/gpui/src/platform/test/display.rs +++ b/crates/gpui/src/platform/test/display.rs @@ -3,31 +3,31 @@ use anyhow::{Ok, Result}; #[derive(Debug)] pub(crate) struct TestDisplay { - id: DisplayId, - uuid: uuid::Uuid, - bounds: Bounds, + 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.))), - } - } + 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 id(&self) -> crate::DisplayId { + self.id + } - fn uuid(&self) -> Result { - Ok(self.uuid) - } + fn uuid(&self) -> Result { + Ok(self.uuid) + } - fn bounds(&self) -> crate::Bounds { - self.bounds - } + fn bounds(&self) -> crate::Bounds { + self.bounds + } } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index b3bee3769e..4f986bcc36 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,42 +1,42 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform, - PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, - size, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, + DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform, + PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, + size, }; use anyhow::Result; use collections::VecDeque; use futures::channel::oneshot; use parking_lot::Mutex; use std::{ - cell::RefCell, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::Arc, + 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, + 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>, - pub(crate) prompts: RefCell, - screen_capture_sources: RefCell>, - pub opened_url: RefCell>, - pub text_system: Arc, - pub expect_restart: RefCell>>>, - headless_renderer_factory: Option Option>>>, - weak: Weak, + 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>, + pub(crate) prompts: RefCell, + screen_capture_sources: RefCell>, + pub opened_url: RefCell>, + pub text_system: Arc, + pub expect_restart: RefCell>>>, + headless_renderer_factory: Option Option>>>, + weak: Weak, } #[derive(Clone)] @@ -47,470 +47,470 @@ pub struct TestScreenCaptureSource {} 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 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 - } + 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() - } + fn metadata(&self) -> Result { + TestScreenCaptureSource {}.metadata() + } } struct TestPrompt { - msg: String, - detail: Option, - answers: Vec, - tx: oneshot::Sender, + msg: String, + detail: Option, + answers: Vec, + tx: oneshot::Sender, } #[derive(Default)] pub(crate) struct TestPrompts { - multiple_choice: VecDeque, - new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, - paths: VecDeque<( - PathPromptOptions, - oneshot::Sender>>>, - )>, + multiple_choice: VecDeque, + new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, + paths: VecDeque<( + PathPromptOptions, + oneshot::Sender>>>, + )>, } impl TestPlatform { - pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { - Self::with_platform( - executor, - foreground_executor, - Arc::new(NoopTextSystem), - None, - ) - } + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + Self::with_platform( + executor, + foreground_executor, + Arc::new(NoopTextSystem), + None, + ) + } - 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_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, - 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(), - text_system, - headless_renderer_factory, - }) - } + 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, + 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(), + text_system, + headless_renderer_factory, + }) + } - 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(); - } + 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(); + } - 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(); - } + 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(); + } - pub(crate) fn did_prompt_for_paths(&self) -> bool { - !self.prompts.borrow().paths.is_empty() - } + pub(crate) fn did_prompt_for_paths(&self) -> bool { + !self.prompts.borrow().paths.is_empty() + } - #[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(); - } + #[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(); + } - pub(crate) fn has_pending_prompt(&self) -> bool { - !self.prompts.borrow().multiple_choice.is_empty() - } + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } - 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(), - )) - } + 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(), + )) + } - pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { - *self.screen_capture_sources.borrow_mut() = sources; - } + pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { + *self.screen_capture_sources.borrow_mut() = sources; + } - 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 - } + 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 + } - 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); + 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(); - } + 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(); + } - pub(crate) fn did_prompt_for_new_path(&self) -> bool { - !self.prompts.borrow().new_path.is_empty() - } + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + !self.prompts.borrow().new_path.is_empty() + } } impl Platform for TestPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } - fn text_system(&self) -> Arc { - self.text_system.clone() - } + fn text_system(&self) -> Arc { + self.text_system.clone() + } - fn keyboard_layout(&self) -> Box { - Box::new(TestKeyboardLayout) - } + fn keyboard_layout(&self) -> Box { + Box::new(TestKeyboardLayout) + } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } - fn on_keyboard_layout_change(&self, _: Box) {} + fn on_keyboard_layout_change(&self, _: Box) {} - fn on_thermal_state_change(&self, _: Box) {} + fn on_thermal_state_change(&self, _: Box) {} - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } - fn run(&self, _on_finish_launching: Box) { - unimplemented!() - } + fn run(&self, _on_finish_launching: Box) { + unimplemented!() + } - fn quit(&self) {} + fn quit(&self) {} - fn restart(&self, path: Option) { - if let Some(tx) = self.expect_restart.take() { - tx.send(path).unwrap(); - } - } + fn restart(&self, path: Option) { + if let Some(tx) = self.expect_restart.take() { + tx.send(path).unwrap(); + } + } - fn activate(&self, _ignoring_other_apps: bool) { - // - } + fn activate(&self, _ignoring_other_apps: bool) { + // + } - fn hide(&self) { - unimplemented!() - } + fn hide(&self) { + unimplemented!() + } - fn hide_other_apps(&self) { - unimplemented!() - } + fn hide_other_apps(&self) { + unimplemented!() + } - fn unhide_other_apps(&self) { - unimplemented!() - } + fn unhide_other_apps(&self) { + unimplemented!() + } - fn displays(&self) -> Vec> { - vec![self.active_display.clone()] - } + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } - fn primary_display(&self) -> Option> { - Some(self.active_display.clone()) - } + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } - fn is_screen_capture_supported(&self) -> bool { - true - } + 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 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 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 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 window_appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } - fn open_url(&self, url: &str) { - *self.opened_url.borrow_mut() = Some(url.to_string()) - } + fn open_url(&self, url: &str) { + *self.opened_url.borrow_mut() = Some(url.to_string()) + } - fn on_open_urls(&self, _callback: Box)>) { - unimplemented!() - } + fn on_open_urls(&self, _callback: Box)>) { + unimplemented!() + } - 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 - } + 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 + } - 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 - } + 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 + } - fn can_select_mixed_files_and_dirs(&self) -> bool { - true - } + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } - fn reveal_path(&self, _path: &std::path::Path) { - unimplemented!() - } + fn reveal_path(&self, _path: &std::path::Path) { + unimplemented!() + } - fn on_quit(&self, _callback: Box) {} + fn on_quit(&self, _callback: Box) {} - fn on_reopen(&self, _callback: Box) { - unimplemented!() - } + fn on_reopen(&self, _callback: Box) { + unimplemented!() + } - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + 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 add_recent_document(&self, _paths: &Path) {} - fn on_app_menu_action(&self, _callback: Box) {} + fn on_app_menu_action(&self, _callback: Box) {} - fn on_will_open_app_menu(&self, _callback: Box) {} + fn on_will_open_app_menu(&self, _callback: Box) {} - fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} - fn app_path(&self) -> Result { - unimplemented!() - } + fn app_path(&self) -> Result { + unimplemented!() + } - fn path_for_auxiliary_executable(&self, _name: &str) -> 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 set_cursor_style(&self, style: crate::CursorStyle) { + *self.active_cursor.lock() = style; + } - fn hide_cursor_until_mouse_moves(&self) {} + fn hide_cursor_until_mouse_moves(&self) {} - fn is_cursor_visible(&self) -> bool { - true - } + fn is_cursor_visible(&self) -> bool { + true + } - fn should_auto_hide_scrollbars(&self) -> bool { - false - } + fn should_auto_hide_scrollbars(&self) -> bool { + false + } - fn read_from_clipboard(&self) -> Option { - self.current_clipboard_item.lock().clone() - } + 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); - } + 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 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(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 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); - } + #[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 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 read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } - fn register_url_scheme(&self, _: &str) -> Task> { - unimplemented!() - } + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } - fn open_with_system(&self, _path: &Path) { - unimplemented!() - } + fn open_with_system(&self, _path: &Path) { + unimplemented!() + } } impl TestScreenCaptureSource { - /// Create a fake screen capture source, for testing. - pub fn new() -> Self { - Self {} - } + /// Create a fake screen capture source, for testing. + pub fn new() -> Self { + Self {} + } } struct TestKeyboardLayout; impl PlatformKeyboardLayout for TestKeyboardLayout { - fn id(&self) -> &str { - "zed.keyboard.example" - } + fn id(&self) -> &str { + "zed.keyboard.example" + } - fn name(&self) -> &str { - "zed.keyboard.example" - } + fn name(&self) -> &str { + "zed.keyboard.example" + } } diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 2b5399ca98..b508ab54a9 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,400 +1,400 @@ use crate::{ - AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, - DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, - PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TileId, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, + AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, + DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, + PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TileId, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, }; use collections::HashMap; use image::RgbaImage; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::{ - rc::{Rc, Weak}, - sync::{self, Arc}, + 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>, - input_handler: Option, - is_fullscreen: bool, + 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>, + input_handler: Option, + is_fullscreen: bool, } #[derive(Clone)] pub struct TestWindow(pub(crate) Rc>); impl HasWindowHandle for TestWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - unimplemented!("Test Windows are not backed by a real platform window") - } + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } } impl HasDisplayHandle for TestWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - unimplemented!("Test Windows are not backed by a real platform window") - } + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } } 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, - input_handler: None, - is_fullscreen: false, - }))) - } + 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, + input_handler: None, + is_fullscreen: false, + }))) + } - 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 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(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_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 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 + } } impl PlatformWindow for TestWindow { - fn bounds(&self) -> Bounds { - self.0.lock().bounds - } + fn bounds(&self) -> Bounds { + self.0.lock().bounds + } - fn window_bounds(&self) -> WindowBounds { - WindowBounds::Windowed(self.bounds()) - } + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } - fn is_maximized(&self) -> bool { - false - } + fn is_maximized(&self) -> bool { + false + } - fn content_size(&self) -> Size { - self.bounds().size - } + 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 resize(&mut self, size: Size) { + let mut lock = self.0.lock(); + lock.bounds.size = size; + } - fn scale_factor(&self) -> f32 { - 2.0 - } + fn scale_factor(&self) -> f32 { + 2.0 + } - fn appearance(&self) -> WindowAppearance { - WindowAppearance::Light - } + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } - fn display(&self) -> Option> { - Some(self.0.lock().display.clone()) - } + fn display(&self) -> Option> { + Some(self.0.lock().display.clone()) + } - fn mouse_position(&self) -> Point { - Point::default() - } + fn mouse_position(&self) -> Point { + Point::default() + } - fn modifiers(&self) -> crate::Modifiers { - crate::Modifiers::default() - } + fn modifiers(&self) -> crate::Modifiers { + crate::Modifiers::default() + } - fn capslock(&self) -> crate::Capslock { - crate::Capslock::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 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 take_input_handler(&mut self) -> Option { + self.0.lock().input_handler.take() + } - 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 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 activate(&self) { + self.0 + .lock() + .platform + .upgrade() + .unwrap() + .set_active_window(Some(self.clone())) + } - fn is_active(&self) -> bool { - false - } + fn is_active(&self) -> bool { + false + } - fn is_hovered(&self) -> bool { - false - } + fn is_hovered(&self) -> bool { + false + } - fn background_appearance(&self) -> WindowBackgroundAppearance { - WindowBackgroundAppearance::Opaque - } + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } - fn is_subpixel_rendering_supported(&self) -> bool { - false - } + 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_title(&mut self, title: &str) { + self.0.lock().title = Some(title.to_owned()); + } - fn set_app_id(&mut self, _app_id: &str) {} + fn set_app_id(&mut self, _app_id: &str) {} - fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} - fn set_edited(&mut self, edited: bool) { - self.0.lock().edited = edited; - } + 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 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 show_character_palette(&self) { + unimplemented!() + } - fn minimize(&self) { - unimplemented!() - } + fn minimize(&self) { + unimplemented!() + } - fn zoom(&self) { - unimplemented!() - } + fn zoom(&self) { + unimplemented!() + } - fn toggle_fullscreen(&self) { - let mut lock = self.0.lock(); - lock.is_fullscreen = !lock.is_fullscreen; - } + 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 is_fullscreen(&self) -> bool { + self.0.lock().is_fullscreen + } - fn on_request_frame(&self, _callback: Box) {} + fn on_request_frame(&self, _callback: Box) {} - fn on_input(&self, callback: Box DispatchEventResult>) { - self.0.lock().input_callback = Some(callback) - } + 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_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_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_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_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_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); + } - fn on_close(&self, _callback: Box) {} + 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_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) {} + fn on_appearance_changed(&self, _callback: Box) {} - fn draw(&self, _scene: &Scene) {} + fn draw(&self, _scene: &Scene) {} - fn sprite_atlas(&self) -> sync::Arc { - self.0.lock().sprite_atlas.clone() - } + 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 mut state = self.0.lock(); - let size = state.bounds.size; - if let Some(renderer) = &mut state.renderer { - let scale_factor = 2.0; - 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") - } - } + #[cfg(any(test, feature = "test-support"))] + fn render_to_image(&self, scene: &Scene) -> anyhow::Result { + let mut state = self.0.lock(); + let size = state.bounds.size; + if let Some(renderer) = &mut state.renderer { + let scale_factor = 2.0; + 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) - } + fn as_test(&mut self) -> Option<&mut TestWindow> { + Some(self) + } - #[cfg(target_os = "windows")] - fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { - unimplemented!() - } + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + unimplemented!() + } - fn show_window_menu(&self, _position: Point) { - unimplemented!() - } + fn show_window_menu(&self, _position: Point) { + unimplemented!() + } - fn start_window_move(&self) { - unimplemented!() - } + fn start_window_move(&self) { + unimplemented!() + } - fn update_ime_position(&self, _bounds: Bounds) {} + fn update_ime_position(&self, _bounds: Bounds) {} - fn gpu_specs(&self) -> Option { - None - } + fn gpu_specs(&self) -> Option { + None + } } pub(crate) struct TestAtlasState { - next_id: u32, - tiles: HashMap, + 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(), - })) - } + 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); + 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 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; + 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, - }, - }, - ); + 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])) - } + Ok(Some(state.tiles[key])) + } - fn remove(&self, key: &AtlasKey) { - let mut state = self.0.lock(); - state.tiles.remove(key); - } + fn remove(&self, key: &AtlasKey) { + let mut state = self.0.lock(); + state.tiles.remove(key); + } } diff --git a/crates/gpui/src/platform/visual_test.rs b/crates/gpui/src/platform/visual_test.rs index 3719a3ee24..f7929ab528 100644 --- a/crates/gpui/src/platform/visual_test.rs +++ b/crates/gpui/src/platform/visual_test.rs @@ -7,19 +7,19 @@ 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, + 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, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, }; /// A platform that combines real Mac rendering with controllable TestDispatcher. @@ -29,234 +29,234 @@ use std::{ /// - 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>, + 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()); + /// 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); + 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), - } - } + 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 - } + /// 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 background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } - fn text_system(&self) -> Arc { - self.platform.text_system() - } + 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 run(&self, _on_finish_launching: Box) { + panic!("VisualTestPlatform::run should not be called in tests") + } - fn quit(&self) {} + fn quit(&self) {} - fn restart(&self, _binary_path: Option) {} + fn restart(&self, _binary_path: Option) {} - fn activate(&self, _ignoring_other_apps: bool) {} + fn activate(&self, _ignoring_other_apps: bool) {} - fn hide(&self) {} + fn hide(&self) {} - fn hide_other_apps(&self) {} + fn hide_other_apps(&self) {} - fn unhide_other_apps(&self) {} + fn unhide_other_apps(&self) {} - fn displays(&self) -> Vec> { - self.platform.displays() - } + fn displays(&self) -> Vec> { + self.platform.displays() + } - fn primary_display(&self) -> Option> { - self.platform.primary_display() - } + fn primary_display(&self) -> Option> { + self.platform.primary_display() + } - fn active_window(&self) -> Option { - self.platform.active_window() - } + fn active_window(&self) -> Option { + self.platform.active_window() + } - fn window_stack(&self) -> Option> { - self.platform.window_stack() - } + fn window_stack(&self) -> Option> { + self.platform.window_stack() + } - fn is_screen_capture_supported(&self) -> bool { - self.platform.is_screen_capture_supported() - } + 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 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 open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Result> { + self.platform.open_window(handle, options) + } - fn window_appearance(&self) -> WindowAppearance { - self.platform.window_appearance() - } + fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } - fn open_url(&self, url: &str) { - self.platform.open_url(url) - } + fn open_url(&self, url: &str) { + self.platform.open_url(url) + } - fn on_open_urls(&self, _callback: Box)>) {} + fn on_open_urls(&self, _callback: Box)>) {} - fn register_url_scheme(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } + 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_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 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 can_select_mixed_files_and_dirs(&self) -> bool { + true + } - fn reveal_path(&self, path: &Path) { - self.platform.reveal_path(path) - } + 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 open_with_system(&self, path: &Path) { + self.platform.open_with_system(path) + } - fn on_quit(&self, _callback: Box) {} + fn on_quit(&self, _callback: Box) {} - fn on_reopen(&self, _callback: Box) {} + fn on_reopen(&self, _callback: Box) {} - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn get_menus(&self) -> Option> { - None - } + fn get_menus(&self) -> Option> { + None + } - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} - fn on_app_menu_action(&self, _callback: Box) {} + fn on_app_menu_action(&self, _callback: Box) {} - fn on_will_open_app_menu(&self, _callback: Box) {} + fn on_will_open_app_menu(&self, _callback: Box) {} - fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} - fn app_path(&self) -> Result { - self.platform.app_path() - } + 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 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 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 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 is_cursor_visible(&self) -> bool { + self.platform.is_cursor_visible() + } - fn should_auto_hide_scrollbars(&self) -> bool { - self.platform.should_auto_hide_scrollbars() - } + fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } - fn read_from_clipboard(&self) -> Option { - self.clipboard.lock().clone() - } + fn read_from_clipboard(&self) -> Option { + self.clipboard.lock().clone() + } - fn write_to_clipboard(&self, item: ClipboardItem) { - *self.clipboard.lock() = Some(item); - } + 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 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); - } + #[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 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 read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } - fn keyboard_layout(&self) -> Box { - self.platform.keyboard_layout() - } + fn keyboard_layout(&self) -> Box { + self.platform.keyboard_layout() + } - fn keyboard_mapper(&self) -> Rc { - self.platform.keyboard_mapper() - } + fn keyboard_mapper(&self) -> Rc { + self.platform.keyboard_mapper() + } - fn on_keyboard_layout_change(&self, _callback: Box) {} + fn on_keyboard_layout_change(&self, _callback: Box) {} - fn thermal_state(&self) -> super::ThermalState { - super::ThermalState::Nominal - } + fn thermal_state(&self) -> super::ThermalState { + super::ThermalState::Nominal + } - fn on_thermal_state_change(&self, _callback: Box) {} + fn on_thermal_state_change(&self, _callback: Box) {} } diff --git a/crates/gpui/src/platform_scheduler.rs b/crates/gpui/src/platform_scheduler.rs index 6a3929e91b..02bb0510cb 100644 --- a/crates/gpui/src/platform_scheduler.rs +++ b/crates/gpui/src/platform_scheduler.rs @@ -4,20 +4,20 @@ use chrono::{DateTime, Utc}; use futures::channel::oneshot; use scheduler::Instant; use scheduler::{ - Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer, - spawn_dedicated_thread, + 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, + any::Any, + future::Future, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicU16, Ordering}, + }, + time::Duration, }; /// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`]. @@ -25,417 +25,416 @@ use std::{ /// 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, + dispatcher: Arc, + clock: Arc, + next_session_id: AtomicU16, } impl PlatformScheduler { - pub fn new(dispatcher: Arc) -> Self { - Self { - dispatcher: dispatcher.clone(), - clock: Arc::new(PlatformClock { dispatcher }), - next_session_id: AtomicU16::new(0), - } - } + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher: dispatcher.clone(), + clock: Arc::new(PlatformClock { dispatcher }), + next_session_id: AtomicU16::new(0), + } + } - 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); - } - }) - } + 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)) - } + fn next_session_id(&self) -> SessionId { + SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst)) + } } impl Scheduler for PlatformScheduler { - fn block( - &self, - _session_id: Option, - #[cfg_attr(target_family = "wasm", allow(unused_mut))] mut future: Pin< - &mut dyn Future, - >, - #[cfg_attr(target_family = "wasm", allow(unused_variables))] timeout: Option, - ) -> bool { - #[cfg(target_family = "wasm")] - { - let _ = (&future, &timeout); - panic!("Cannot block on wasm") - } - #[cfg(not(target_family = "wasm"))] - { - 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; - } + fn block( + &self, + _session_id: Option, + #[cfg_attr(target_family = "wasm", allow(unused_mut))] mut future: Pin< + &mut dyn Future, + >, + #[cfg_attr(target_family = "wasm", allow(unused_variables))] timeout: Option, + ) -> bool { + #[cfg(target_family = "wasm")] + { + let _ = (&future, &timeout); + panic!("Cannot block on wasm") + } + #[cfg(not(target_family = "wasm"))] + { + 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) - }; + 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; - } - } - } - } + 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) { - self.dispatcher - .dispatch_on_main_thread(runnable, Priority::default()); - } + fn schedule_local(&self, _session_id: SessionId, runnable: Runnable) { + 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 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); - } + 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(); + #[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 }) - .spawn( - move |_| async move { - let _ = tx.send(()); - }, - move |runnable| { - dispatcher.dispatch_after(duration, runnable); - }, - ); - runnable.schedule(); + // 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 }) + .spawn( + move |_| async move { + let _ = tx.send(()); + }, + move |runnable| { + dispatcher.dispatch_after(duration, runnable); + }, + ); + runnable.schedule(); - Timer::new(rx) - } + Timer::new(rx) + } - fn clock(&self) -> Arc { - self.clock.clone() - } + 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 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 - } + fn as_test(&self) -> Option<&TestScheduler> { + None + } } /// A production clock that uses the platform dispatcher's time. struct PlatformClock { - dispatcher: Arc, + dispatcher: Arc, } impl Clock for PlatformClock { - fn utc_now(&self) -> DateTime { - Utc::now() - } + fn utc_now(&self) -> DateTime { + Utc::now() + } - fn now(&self) -> Instant { - self.dispatcher.now() - } + fn now(&self) -> Instant { + self.dispatcher.now() + } } #[cfg(all(test, not(target_family = "wasm")))] mod tests { - use super::*; - use crate::{RunnableVariant, ThreadTaskTimings}; - use scheduler::BackgroundExecutor; - use std::time::Instant as StdInstant; + use super::*; + use crate::{RunnableVariant, ThreadTaskTimings}; + 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; + // `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 get_all_timings(&self) -> Vec { - Vec::new() - } - fn get_current_thread_timings(&self) -> ThreadTaskTimings { - ThreadTaskTimings { - thread_name: None, - thread_id: std::thread::current().id(), - timings: Vec::new(), - total_pushed: 0, - } - } - 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"); - } - } + impl PlatformDispatcher for SmokeDispatcher { + fn get_all_timings(&self) -> Vec { + Vec::new() + } + fn get_current_thread_timings(&self) -> ThreadTaskTimings { + ThreadTaskTimings { + thread_name: None, + thread_id: std::thread::current().id(), + timings: Vec::new(), + total_pushed: 0, + } + } + 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 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_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; + #[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); - } + 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; + #[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 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 (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"); - }) - }; + 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"); + // 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); + // 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" - ); - } + // 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; + #[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()); - } - } - } + // 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 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 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" - ); + 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); + // 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" - ); - } + // 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; + #[test] + fn spawn_dedicated_detached_child_outlives_root() { + use std::sync::mpsc; - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + 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::(); + // `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. - }); + 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); + 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" - ); + // 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"); + // 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" - ); - } + 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/src/prelude.rs b/crates/gpui/src/prelude.rs index b5185a25e8..78c55e6b70 100644 --- a/crates/gpui/src/prelude.rs +++ b/crates/gpui/src/prelude.rs @@ -3,7 +3,7 @@ //! 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, + AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement, + ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage, + TaskExt as _, VisualContext, util::FluentBuilder, }; diff --git a/crates/gpui/src/profiler.rs b/crates/gpui/src/profiler.rs index 009afb9099..852ebfa061 100644 --- a/crates/gpui/src/profiler.rs +++ b/crates/gpui/src/profiler.rs @@ -1,13 +1,13 @@ use scheduler::Instant; use std::{ - cell::LazyCell, - collections::{HashMap, VecDeque}, - hash::{DefaultHasher, Hash, Hasher}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread::ThreadId, + cell::LazyCell, + collections::{HashMap, VecDeque}, + hash::{DefaultHasher, Hash, Hasher}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::ThreadId, }; use serde::{Deserialize, Serialize}; @@ -17,231 +17,231 @@ use crate::SharedString; #[doc(hidden)] #[derive(Debug, Copy, Clone)] pub struct TaskTiming { - pub location: &'static core::panic::Location<'static>, - pub start: Instant, - pub end: Option, + pub location: &'static core::panic::Location<'static>, + pub start: Instant, + pub end: Option, } #[doc(hidden)] #[derive(Debug, Clone)] pub struct ThreadTaskTimings { - pub thread_name: Option, - pub thread_id: ThreadId, - pub timings: Vec, - pub total_pushed: u64, + pub thread_name: Option, + pub thread_id: ThreadId, + pub timings: Vec, + pub total_pushed: u64, } impl ThreadTaskTimings { - /// Convert global thread timings into their structured format. - pub fn convert(timings: &[GlobalThreadTimings]) -> Vec { - timings - .iter() - .filter_map(|t| match t.timings.upgrade() { - Some(timings) => Some((t.thread_id, timings)), - _ => None, - }) - .map(|(thread_id, timings)| { - let timings = timings.lock(); - let thread_name = timings.thread_name.clone(); - let total_pushed = timings.total_pushed; - let timings = &timings.timings; + /// Convert global thread timings into their structured format. + pub fn convert(timings: &[GlobalThreadTimings]) -> Vec { + timings + .iter() + .filter_map(|t| match t.timings.upgrade() { + Some(timings) => Some((t.thread_id, timings)), + _ => None, + }) + .map(|(thread_id, timings)| { + let timings = timings.lock(); + let thread_name = timings.thread_name.clone(); + let total_pushed = timings.total_pushed; + let timings = &timings.timings; - let mut vec = Vec::with_capacity(timings.len()); - let (s1, s2) = timings.as_slices(); - vec.extend_from_slice(s1); - vec.extend_from_slice(s2); + let mut vec = Vec::with_capacity(timings.len()); + let (s1, s2) = timings.as_slices(); + vec.extend_from_slice(s1); + vec.extend_from_slice(s2); - ThreadTaskTimings { - thread_name, - thread_id, - timings: vec, - total_pushed, - } - }) - .collect() - } + ThreadTaskTimings { + thread_name, + thread_id, + timings: vec, + total_pushed, + } + }) + .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, + /// 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(), - } - } + 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, + /// 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 - .unwrap_or_else(|| Instant::now()) - .duration_since(timing.start) - .as_nanos(); - SerializedTaskTiming { - location: timing.location.into(), - start, - duration, - } - }) - .collect::>(); + /// 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 + .unwrap_or_else(|| Instant::now()) + .duration_since(timing.start) + .as_nanos(); + SerializedTaskTiming { + location: timing.location.into(), + start, + duration, + } + }) + .collect::>(); - serialized - } + serialized + } } /// 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, + /// 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); + /// 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(); + 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, - } - } + 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, + /// 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, + startup_time: Instant, + cursors: HashMap, } impl ProfilingCollector { - pub fn new(startup_time: Instant) -> Self { - Self { - startup_time, - cursors: HashMap::default(), - } - } + pub fn new(startup_time: Instant) -> Self { + Self { + startup_time, + cursors: HashMap::default(), + } + } - pub fn startup_time(&self) -> Instant { - self.startup_time - } + 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()); + 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(); + 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 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 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())..] + }; - // Don't emit the last entry if it's still in-progress (end: None). - let incomplete_at_end = slice.last().is_some_and(|t| t.end.is_none()); - if incomplete_at_end { - slice = &slice[..slice.len() - 1]; - } + // Don't emit the last entry if it's still in-progress (end: None). + let incomplete_at_end = slice.last().is_some_and(|t| t.end.is_none()); + if incomplete_at_end { + slice = &slice[..slice.len() - 1]; + } - let cursor_advance = if incomplete_at_end { - thread.total_pushed.saturating_sub(1) - } else { - thread.total_pushed - }; + let cursor_advance = if incomplete_at_end { + thread.total_pushed.saturating_sub(1) + } else { + thread.total_pushed + }; - self.cursors.insert(thread.thread_id, cursor_advance); + self.cursors.insert(thread.thread_id, cursor_advance); - if slice.is_empty() { - continue; - } + if slice.is_empty() { + continue; + } - let new_timings = SerializedTaskTiming::convert(self.startup_time, slice); + let new_timings = SerializedTaskTiming::convert(self.startup_time, slice); - deltas.push(ThreadTimingsDelta { - thread_id: hashed_id, - thread_name: thread.thread_name, - new_timings, - }); - } + deltas.push(ThreadTimingsDelta { + thread_id: hashed_id, + thread_name: thread.thread_name, + new_timings, + }); + } - deltas - } + deltas + } - pub fn reset(&mut self) { - self.cursors.clear(); - } + pub fn reset(&mut self) { + self.cursors.clear(); + } } // Allow 16MiB of task timing entries. @@ -257,111 +257,111 @@ pub type GuardedTaskTimings = spin::Mutex; #[doc(hidden)] pub struct GlobalThreadTimings { - pub thread_id: ThreadId, - pub timings: std::sync::Weak, + pub thread_id: ThreadId, + pub timings: std::sync::Weak, } #[doc(hidden)] pub static GLOBAL_THREAD_TIMINGS: spin::Mutex> = - spin::Mutex::new(Vec::new()); + spin::Mutex::new(Vec::new()); 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)); + #[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); - } + { + let timings = Arc::downgrade(&timings); + let global_timings = GlobalThreadTimings { + thread_id: std::thread::current().id(), + timings, + }; + GLOBAL_THREAD_TIMINGS.lock().push(global_timings); + } - timings - }); + timings + }); } #[doc(hidden)] pub struct ThreadTimings { - pub thread_name: Option, - pub thread_id: ThreadId, - pub timings: TaskTimings, - pub total_pushed: u64, + pub thread_name: Option, + pub thread_id: ThreadId, + pub timings: TaskTimings, + pub total_pushed: u64, } impl ThreadTimings { - pub fn new(thread_name: Option, thread_id: ThreadId) -> Self { - ThreadTimings { - thread_name, - thread_id, - timings: TaskTimings::new(), - total_pushed: 0, - } - } + pub fn new(thread_name: Option, thread_id: ThreadId) -> Self { + ThreadTimings { + thread_name, + thread_id, + timings: TaskTimings::new(), + total_pushed: 0, + } + } - /// If this task is the same as the last task, update the end time of the last task. - /// - /// Otherwise, add the new task timing to the list. - pub fn add_task_timing(&mut self, timing: TaskTiming) { - if let Some(last_timing) = self.timings.back_mut() - && last_timing.location == timing.location - && last_timing.start == timing.start - { - last_timing.end = timing.end; - } else { - while self.timings.len() + 1 > MAX_TASK_TIMINGS { - // This should only ever pop one element because it matches the insertion below. - self.timings.pop_front(); - } - self.timings.push_back(timing); - self.total_pushed += 1; - } - } + /// If this task is the same as the last task, update the end time of the last task. + /// + /// Otherwise, add the new task timing to the list. + pub fn add_task_timing(&mut self, timing: TaskTiming) { + if let Some(last_timing) = self.timings.back_mut() + && last_timing.location == timing.location + && last_timing.start == timing.start + { + last_timing.end = timing.end; + } else { + while self.timings.len() + 1 > MAX_TASK_TIMINGS { + // This should only ever pop one element because it matches the insertion below. + self.timings.pop_front(); + } + self.timings.push_back(timing); + self.total_pushed += 1; + } + } - pub fn get_thread_task_timings(&self) -> ThreadTaskTimings { - ThreadTaskTimings { - thread_name: self.thread_name.clone(), - thread_id: self.thread_id, - timings: self.timings.iter().cloned().collect(), - total_pushed: self.total_pushed, - } - } + pub fn get_thread_task_timings(&self) -> ThreadTaskTimings { + ThreadTaskTimings { + thread_name: self.thread_name.clone(), + thread_id: self.thread_id, + timings: self.timings.iter().cloned().collect(), + total_pushed: self.total_pushed, + } + } } impl Drop for ThreadTimings { - fn drop(&mut self) { - let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock(); + 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); - } + 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 add_task_timing(timing: TaskTiming) { - if !PROFILER_ENABLED.load(Ordering::Acquire) { - return; - } - THREAD_TIMINGS.with(|timings| { - timings.lock().add_task_timing(timing); - }); + if !PROFILER_ENABLED.load(Ordering::Acquire) { + return; + } + THREAD_TIMINGS.with(|timings| { + timings.lock().add_task_timing(timing); + }); } #[doc(hidden)] pub fn get_current_thread_task_timings() -> ThreadTaskTimings { - THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings()) + THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings()) } static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false); @@ -372,19 +372,19 @@ static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false); /// no-op and the existing per-thread buffers are cleared so stale data isn't /// reported after a later re-enable. Calls with the current value are a no-op. pub fn set_enabled(enabled: bool) -> bool { - if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled { - return false; - } + if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled { + return false; + } - if !enabled { - for global in GLOBAL_THREAD_TIMINGS.lock().iter() { - if let Some(timings) = global.timings.upgrade() { - let mut timings = timings.lock(); - timings.timings.clear(); - timings.timings.shrink_to_fit(); - timings.total_pushed = 0; - } - } - } - true + if !enabled { + for global in GLOBAL_THREAD_TIMINGS.lock().iter() { + if let Some(timings) = global.timings.upgrade() { + let mut timings = timings.lock(); + timings.timings.clear(); + timings.timings.shrink_to_fit(); + timings.total_pushed = 0; + } + } + } + true } diff --git a/crates/gpui/src/queue.rs b/crates/gpui/src/queue.rs index 6e7cf2445e..db39428ff0 100644 --- a/crates/gpui/src/queue.rs +++ b/crates/gpui/src/queue.rs @@ -1,8 +1,8 @@ use std::{ - collections::VecDeque, - fmt, - iter::FusedIterator, - sync::{Arc, atomic::AtomicUsize}, + collections::VecDeque, + fmt, + iter::FusedIterator, + sync::{Arc, atomic::AtomicUsize}, }; use rand::{Rng, SeedableRng, rngs::SmallRng}; @@ -10,184 +10,184 @@ use rand::{Rng, SeedableRng, rngs::SmallRng}; use crate::Priority; struct PriorityQueues { - high_priority: VecDeque, - medium_priority: VecDeque, - low_priority: VecDeque, + 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() - } + 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, + 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)); - } + 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(()) - } + 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)); - } + 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(()) - } + 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 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(); + 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); - } + 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); - } + while queues.is_empty() { + self.condvar.wait(&mut queues); + } - Ok(queues) - } + Ok(queues) + } - fn try_recv<'a>( - &'a self, - ) -> Result>>, RecvError> { - let mut queues = self.queues.lock(); + 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); - } + 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)) - } - } + 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(); - }; + 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); - } + 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)) - } - } + if queues.is_empty() { + Ok(None) + } else { + Ok(Some(queues)) + } + } } #[doc(hidden)] pub struct PriorityQueueSender { - state: Arc>, + state: Arc>, } impl PriorityQueueSender { - fn new(state: Arc>) -> Self { - Self { state } - } + 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 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(()) - } + 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); - } + 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, + 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, - } - } + 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() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("SendError").field(&self.0).finish() + } } #[derive(Debug)] @@ -196,229 +196,229 @@ 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); + 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 sender = PriorityQueueSender::new(Arc::clone(&state)); - let receiver = PriorityQueueReceiver { - state, - rand: SmallRng::seed_from_u64(0), - disconnected: false, - }; + let receiver = PriorityQueueReceiver { + state, + rand: SmallRng::seed_from_u64(0), + disconnected: false, + }; - (sender, receiver) - } + (sender, receiver) + } - /// 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) - } + /// 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; + 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 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; + 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.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.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()); - } - } + 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) - } + 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()) - } + /// 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 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) - } + /// 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; + #[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 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; //% + 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.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.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()); - } - } + 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) - } + Ok(None) + } } impl Drop for PriorityQueueReceiver { - fn drop(&mut self) { - self.state - .receiver_count - .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); - } + 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; + type Item = T; - fn next(&mut self) -> Option { - self.0.pop().ok() - } + fn next(&mut self) -> Option { + self.0.pop().ok() + } } impl FusedIterator for Iter {} #[doc(hidden)] pub struct TryIter { - receiver: PriorityQueueReceiver, - ended: bool, + receiver: PriorityQueueReceiver, + ended: bool, } impl Iterator for TryIter { - type Item = Result; + type Item = Result; - fn next(&mut self) -> Option { - if self.ended { - return None; - } + fn next(&mut self) -> Option { + if self.ended { + return None; + } - let res = self.receiver.try_pop(); - self.ended = res.is_err(); + let res = self.receiver.try_pop(); + self.ended = res.is_err(); - res.transpose() - } + res.transpose() + } } impl FusedIterator for TryIter {} #[cfg(test)] mod tests { - use collections::HashSet; + use collections::HashSet; - use super::*; + 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(); + #[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); + drop(tx); - assert_eq!( - rx.iter().collect::>(), - [30, 31, 20, 21, 10].into_iter().collect::>() - ) - } + 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(); - } + #[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); - } + 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/src/scene.rs b/crates/gpui/src/scene.rs index 93a4299e9a..72d5c39e33 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -5,15 +5,15 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ - AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, - Point, Radians, ScaledFilter, ScaledPixels, Size, bounds_tree::BoundsTree, point, + AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, + Point, Radians, ScaledFilter, ScaledPixels, Size, bounds_tree::BoundsTree, point, }; use smallvec::SmallVec; use std::{ - fmt::Debug, - iter::Peekable, - ops::{Add, Range, Sub}, - slice, + fmt::Debug, + iter::Peekable, + ops::{Add, Range, Sub}, + slice, }; #[allow(non_camel_case_types, unused)] @@ -26,642 +26,642 @@ pub type DrawOrder = u32; #[derive(Default)] #[expect(missing_docs)] pub struct Scene { - 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, - pub backdrop_filters: Vec, - pub filter_boundaries: Vec, + 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, + pub backdrop_filters: Vec, + pub filter_boundaries: Vec, } #[expect(missing_docs)] impl Scene { - pub fn clear(&mut self) { - 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(); - self.backdrop_filters.clear(); - self.filter_boundaries.clear(); - } + pub fn clear(&mut self) { + 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(); + self.backdrop_filters.clear(); + self.filter_boundaries.clear(); + } - pub fn len(&self) -> usize { - self.paint_operations.len() - } + pub fn len(&self) -> usize { + self.paint_operations.len() + } - 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 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 pop_layer(&mut self) { + self.layer_stack.pop(); + self.paint_operations.push(PaintOperation::EndLayer); + } - /// Raise the draw-order floor so every primitive inserted afterwards sorts above everything - /// inserted before. Called before painting deferred draws so overlays (tooltips, popovers, - /// drag images) sort above the main scene — and a deferred backdrop's order can't fall inside - /// a content-filter (`filter`) order range left behind by the main scene. - pub fn raise_order_floor(&mut self) { - let floor = self.primitive_bounds.max_order() + 1; - self.primitive_bounds.set_order_floor(floor); - } + /// Raise the draw-order floor so every primitive inserted afterwards sorts above everything + /// inserted before. Called before painting deferred draws so overlays (tooltips, popovers, + /// drag images) sort above the main scene — and a deferred backdrop's order can't fall inside + /// a content-filter (`filter`) order range left behind by the main scene. + pub fn raise_order_floor(&mut self) { + let floor = self.primitive_bounds.max_order() + 1; + self.primitive_bounds.set_order_floor(floor); + } - pub fn insert_primitive(&mut self, primitive: impl Into) { - let mut primitive = primitive.into(); - let clipped_bounds = primitive - .bounds() - .intersect(&primitive.content_mask().bounds); + pub fn insert_primitive(&mut self, primitive: impl Into) { + let mut primitive = primitive.into(); + let clipped_bounds = primitive + .bounds() + .intersect(&primitive.content_mask().bounds); - // Content-filter boundaries must always be inserted as matched pairs — dropping one - // (e.g. for an empty clipped region) would orphan its partner and corrupt the renderer's - // target stack. Each marker takes an order strictly above ALL prior content, so the start - // sorts after everything painted before it and the element's own children (which overlap - // the marker bounds) sort strictly above the start. This keeps a marker's order range from - // colliding with unrelated non-overlapping content that reuses low orderings (e.g. a - // background grid), which would otherwise sweep that content into the group. Content - // painted *after* the group is held above it by raising the order floor when the end - // marker is inserted (see below) — otherwise a later non-overlapping sibling could reuse a - // low order that lands inside the start..end range and be swept into the group. - let is_filter_boundary = matches!(primitive, Primitive::FilterBoundary(_)); + // Content-filter boundaries must always be inserted as matched pairs — dropping one + // (e.g. for an empty clipped region) would orphan its partner and corrupt the renderer's + // target stack. Each marker takes an order strictly above ALL prior content, so the start + // sorts after everything painted before it and the element's own children (which overlap + // the marker bounds) sort strictly above the start. This keeps a marker's order range from + // colliding with unrelated non-overlapping content that reuses low orderings (e.g. a + // background grid), which would otherwise sweep that content into the group. Content + // painted *after* the group is held above it by raising the order floor when the end + // marker is inserted (see below) — otherwise a later non-overlapping sibling could reuse a + // low order that lands inside the start..end range and be swept into the group. + let is_filter_boundary = matches!(primitive, Primitive::FilterBoundary(_)); - if clipped_bounds.is_empty() && !is_filter_boundary { - return; - } + if clipped_bounds.is_empty() && !is_filter_boundary { + return; + } - let order = if is_filter_boundary { - let order_bounds = if clipped_bounds.is_empty() { - *primitive.bounds() - } else { - clipped_bounds - }; - self.primitive_bounds.insert_above_all(order_bounds) - } else { - 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()); - } - Primitive::BackdropFilter(filter) => { - filter.order = order; - self.backdrop_filters.push(filter.clone()); - } - Primitive::FilterBoundary(boundary) => { - boundary.order = order; - if !boundary.is_start { - // A closed content-filter group is a draw-order barrier: everything painted - // afterwards must sort above the group's end marker so it can't fall back - // inside the group's order range (subsequent non-overlapping content otherwise - // reuses a low order). Mirrors the floor raised before deferred draws in - // `raise_order_floor`. - self.primitive_bounds.set_order_floor(order + 1); - } - self.filter_boundaries.push(boundary.clone()); - } - } - self.paint_operations - .push(PaintOperation::Primitive(primitive)); - } + let order = if is_filter_boundary { + let order_bounds = if clipped_bounds.is_empty() { + *primitive.bounds() + } else { + clipped_bounds + }; + self.primitive_bounds.insert_above_all(order_bounds) + } else { + 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()); + } + Primitive::BackdropFilter(filter) => { + filter.order = order; + self.backdrop_filters.push(filter.clone()); + } + Primitive::FilterBoundary(boundary) => { + boundary.order = order; + if !boundary.is_start { + // A closed content-filter group is a draw-order barrier: everything painted + // afterwards must sort above the group's end marker so it can't fall back + // inside the group's order range (subsequent non-overlapping content otherwise + // reuses a low order). Mirrors the floor raised before deferred draws in + // `raise_order_floor`. + self.primitive_bounds.set_order_floor(order + 1); + } + self.filter_boundaries.push(boundary.clone()); + } + } + self.paint_operations + .push(PaintOperation::Primitive(primitive)); + } - pub fn replay(&mut self, range: Range, prev_scene: &Scene) { - for operation in &prev_scene.paint_operations[range] { - match operation { - PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), - PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), - PaintOperation::EndLayer => self.pop_layer(), - } - } - } + pub fn replay(&mut self, range: Range, prev_scene: &Scene) { + for operation in &prev_scene.paint_operations[range] { + match operation { + PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), + 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); - self.backdrop_filters.sort_by_key(|filter| filter.order); - // Markers normally get distinct, monotonically-increasing orders (children overlap - // their group bounds and so sort strictly between the start and end). The `!is_start` - // tiebreak only matters for a degenerate empty group whose start and end tie: it keeps - // the start (false = 0) ahead of the end (true = 1) so the pair stays well-formed. - self.filter_boundaries - .sort_by_key(|boundary| (boundary.order, !boundary.is_start)); - } + 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); + self.backdrop_filters.sort_by_key(|filter| filter.order); + // Markers normally get distinct, monotonically-increasing orders (children overlap + // their group bounds and so sort strictly between the start and end). The `!is_start` + // tiebreak only matters for a degenerate empty group whose start and end tie: it keeps + // the start (false = 0) ahead of the end (true = 1) so the pair stays well-formed. + self.filter_boundaries + .sort_by_key(|boundary| (boundary.order, !boundary.is_start)); + } - #[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(), - backdrop_filters_start: 0, - backdrop_filters_iter: self.backdrop_filters.iter().peekable(), - filter_boundaries_start: 0, - filter_boundaries_iter: self.filter_boundaries.iter().peekable(), - } - } + #[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(), + backdrop_filters_start: 0, + backdrop_filters_iter: self.backdrop_filters.iter().peekable(), + filter_boundaries_start: 0, + filter_boundaries_iter: self.filter_boundaries.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) + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) )] pub(crate) enum PrimitiveKind { - // Lowest discriminant: at an equal order, a content-filter group-start is emitted before - // the group's own content so the renderer redirects rendering before any child draws. - FilterBoundaryStart, - Shadow, - #[default] - Quad, - Path, - Underline, - MonochromeSprite, - SubpixelSprite, - PolychromeSprite, - Surface, - BackdropFilter, - // Highest discriminant: at an equal order, a group-end is emitted after the group's content - // so the renderer composites the filtered group only once every child has been drawn. - FilterBoundaryEnd, + // Lowest discriminant: at an equal order, a content-filter group-start is emitted before + // the group's own content so the renderer redirects rendering before any child draws. + FilterBoundaryStart, + Shadow, + #[default] + Quad, + Path, + Underline, + MonochromeSprite, + SubpixelSprite, + PolychromeSprite, + Surface, + BackdropFilter, + // Highest discriminant: at an equal order, a group-end is emitted after the group's content + // so the renderer composites the filtered group only once every child has been drawn. + FilterBoundaryEnd, } pub(crate) enum PaintOperation { - Primitive(Primitive), - StartLayer(Bounds), - EndLayer, + 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), - BackdropFilter(BackdropFilter), - FilterBoundary(FilterBoundary), + Shadow(Shadow), + Quad(Quad), + Path(Path), + Underline(Underline), + MonochromeSprite(MonochromeSprite), + SubpixelSprite(SubpixelSprite), + PolychromeSprite(PolychromeSprite), + Surface(PaintSurface), + BackdropFilter(BackdropFilter), + FilterBoundary(FilterBoundary), } #[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, - Primitive::BackdropFilter(filter) => &filter.bounds, - Primitive::FilterBoundary(boundary) => &boundary.bounds, - } - } + 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, + Primitive::BackdropFilter(filter) => &filter.bounds, + Primitive::FilterBoundary(boundary) => &boundary.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, - Primitive::BackdropFilter(filter) => &filter.content_mask, - Primitive::FilterBoundary(boundary) => &boundary.content_mask, - } - } + 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, + Primitive::BackdropFilter(filter) => &filter.content_mask, + Primitive::FilterBoundary(boundary) => &boundary.content_mask, + } + } } #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) + 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>, - backdrop_filters_start: usize, - backdrop_filters_iter: Peekable>, - filter_boundaries_start: usize, - filter_boundaries_iter: Peekable>, + 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>, + backdrop_filters_start: usize, + backdrop_filters_iter: Peekable>, + filter_boundaries_start: usize, + filter_boundaries_iter: Peekable>, } impl<'a> Iterator for BatchIterator<'a> { - type Item = PrimitiveBatch; + 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, - ), - ( - self.backdrop_filters_iter.peek().map(|f| f.order), - PrimitiveKind::BackdropFilter, - ), - ( - self.filter_boundaries_iter.peek().map(|b| b.order), - // The same vec yields both start and end markers; the discriminant decides - // where the next marker sorts relative to draw batches at an equal order - // (start before content, end after). - match self.filter_boundaries_iter.peek() { - Some(boundary) if boundary.is_start => PrimitiveKind::FilterBoundaryStart, - _ => PrimitiveKind::FilterBoundaryEnd, - }, - ), - ]; - orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); + 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, + ), + ( + self.backdrop_filters_iter.peek().map(|f| f.order), + PrimitiveKind::BackdropFilter, + ), + ( + self.filter_boundaries_iter.peek().map(|b| b.order), + // The same vec yields both start and end markers; the discriminant decides + // where the next marker sorts relative to draw batches at an equal order + // (start before content, end after). + match self.filter_boundaries_iter.peek() { + Some(boundary) if boundary.is_start => PrimitiveKind::FilterBoundaryStart, + _ => PrimitiveKind::FilterBoundaryEnd, + }, + ), + ]; + 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; - }; + 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)) - } - PrimitiveKind::BackdropFilter => { - let backdrop_filters_start = self.backdrop_filters_start; - let mut backdrop_filters_end = backdrop_filters_start + 1; - self.backdrop_filters_iter.next(); - while self - .backdrop_filters_iter - .next_if(|filter| (filter.order, batch_kind) < max_order_and_kind) - .is_some() - { - backdrop_filters_end += 1; - } - self.backdrop_filters_start = backdrop_filters_end; - Some(PrimitiveBatch::BackdropFilters( - backdrop_filters_start..backdrop_filters_end, - )) - } - // Boundaries are emitted one at a time (never merged) so the renderer can switch - // render targets at exactly the right point in the batch stream. - PrimitiveKind::FilterBoundaryStart | PrimitiveKind::FilterBoundaryEnd => { - let index = self.filter_boundaries_start; - self.filter_boundaries_iter.next(); - self.filter_boundaries_start = index + 1; - Some(PrimitiveBatch::FilterBoundary(index)) - } - } - } + 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)) + } + PrimitiveKind::BackdropFilter => { + let backdrop_filters_start = self.backdrop_filters_start; + let mut backdrop_filters_end = backdrop_filters_start + 1; + self.backdrop_filters_iter.next(); + while self + .backdrop_filters_iter + .next_if(|filter| (filter.order, batch_kind) < max_order_and_kind) + .is_some() + { + backdrop_filters_end += 1; + } + self.backdrop_filters_start = backdrop_filters_end; + Some(PrimitiveBatch::BackdropFilters( + backdrop_filters_start..backdrop_filters_end, + )) + } + // Boundaries are emitted one at a time (never merged) so the renderer can switch + // render targets at exactly the right point in the batch stream. + PrimitiveKind::FilterBoundaryStart | PrimitiveKind::FilterBoundaryEnd => { + let index = self.filter_boundaries_start; + self.filter_boundaries_iter.next(); + self.filter_boundaries_start = index + 1; + Some(PrimitiveBatch::FilterBoundary(index)) + } + } + } } #[derive(Debug)] #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) + 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), - BackdropFilters(Range), - /// A single content-filter group boundary; index into [`Scene::filter_boundaries`]. Read - /// `is_start` to tell whether this opens the group (switch render target) or closes it - /// (filter the offscreen target and composite it back). - FilterBoundary(usize), + 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), + BackdropFilters(Range), + /// A single content-filter group boundary; index into [`Scene::filter_boundaries`]. Read + /// `is_start` to tell whether this opens the group (switch render target) or closes it + /// (filter the offscreen target and composite it back). + FilterBoundary(usize), } #[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, + 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) - } + 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: u32, + 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: u32, } impl From for Primitive { - fn from(underline: Underline) -> Self { - Primitive::Underline(underline) - } + 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 + 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) - } + fn from(shadow: Shadow) -> Self { + Primitive::Shadow(shadow) + } } /// A backdrop filter blurs (and may otherwise filter) the content already rendered behind @@ -670,25 +670,25 @@ impl From for Primitive { #[derive(Default, Debug, Clone)] #[expect(missing_docs)] pub struct BackdropFilter { - pub order: DrawOrder, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub corner_radii: Corners, - /// The filter chain applied to the backdrop, in scene (device-pixel) space. Identity filters - /// are dropped at paint time, so a `BackdropFilter` is only emitted when this is non-empty. - /// - /// Inline capacity is 4: a `SmallVec<[ScaledFilter; 4]>` is the same size as capacity 1 here - /// (the heap repr already occupies that space), so chains up to 4 filters avoid allocating - /// at no extra struct size. - pub filters: SmallVec<[ScaledFilter; 4]>, - /// Element opacity captured at paint time, multiplied into the composited result. - pub opacity: f32, + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub corner_radii: Corners, + /// The filter chain applied to the backdrop, in scene (device-pixel) space. Identity filters + /// are dropped at paint time, so a `BackdropFilter` is only emitted when this is non-empty. + /// + /// Inline capacity is 4: a `SmallVec<[ScaledFilter; 4]>` is the same size as capacity 1 here + /// (the heap repr already occupies that space), so chains up to 4 filters avoid allocating + /// at no extra struct size. + pub filters: SmallVec<[ScaledFilter; 4]>, + /// Element opacity captured at paint time, multiplied into the composited result. + pub opacity: f32, } impl From for Primitive { - fn from(filter: BackdropFilter) -> Self { - Primitive::BackdropFilter(filter) - } + fn from(filter: BackdropFilter) -> Self { + Primitive::BackdropFilter(filter) + } } /// The start or end marker of a content-filter (`filter`) isolation group. The element's @@ -698,215 +698,215 @@ impl From for Primitive { #[derive(Debug, Clone)] #[expect(missing_docs)] pub struct FilterBoundary { - pub order: DrawOrder, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub corner_radii: Corners, - /// The filter chain applied to the isolated group, in scene (device-pixel) space. Identity - /// filters are dropped at paint time, so a `FilterBoundary` is only emitted when non-empty. - /// Inline capacity 4 (same struct size as 1 here — see [`BackdropFilter::filters`]). - pub filters: SmallVec<[ScaledFilter; 4]>, - pub opacity: f32, - /// `true` for the start marker (opens the group), `false` for the end marker (closes it). - pub is_start: bool, + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub corner_radii: Corners, + /// The filter chain applied to the isolated group, in scene (device-pixel) space. Identity + /// filters are dropped at paint time, so a `FilterBoundary` is only emitted when non-empty. + /// Inline capacity 4 (same struct size as 1 here — see [`BackdropFilter::filters`]). + pub filters: SmallVec<[ScaledFilter; 4]>, + pub opacity: f32, + /// `true` for the start marker (opens the group), `false` for the end marker (closes it). + pub is_start: bool, } impl From for Primitive { - fn from(boundary: FilterBoundary) -> Self { - Primitive::FilterBoundary(boundary) - } + fn from(boundary: FilterBoundary) -> Self { + Primitive::FilterBoundary(boundary) + } } /// 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 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], + /// 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], - } - } + /// 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], - }) - } + /// 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], - }) - } + /// 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], - }) - } + /// 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], - ], - } - } + /// 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()) - } + /// 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() - } + 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, + 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) - } + 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, + 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) - } + 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: bool, - pub opacity: f32, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub corner_radii: Corners, - pub tile: AtlasTile, + pub order: DrawOrder, + pub pad: u32, + pub grayscale: bool, + 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) - } + 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, - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub texture: std::sync::Arc, - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub texture_size: Size, + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + #[cfg(target_os = "macos")] + pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub texture: std::sync::Arc, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub texture_size: Size, } impl From for Primitive { - fn from(surface: PaintSurface) -> Self { - Primitive::Surface(surface) - } + fn from(surface: PaintSurface) -> Self { + Primitive::Surface(surface) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] @@ -917,331 +917,331 @@ pub struct PathId(pub usize); #[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, + 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, - } - } + /// 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, - } - } + /// 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; - } + /// 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 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.)), - ); - } + /// 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; - } + 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(), - }); + /// 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(), - }); - } + 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, + 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) - } + #[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) - } + 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

, + 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), - } - } + 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), + } + } } #[cfg(test)] mod tests { - use super::*; - use crate::{Point, Size}; + use super::*; + use crate::{Point, Size}; - fn sp(value: f32) -> ScaledPixels { - ScaledPixels(value) - } + fn sp(value: f32) -> ScaledPixels { + ScaledPixels(value) + } - /// All test primitives cover the same region so the bounds tree assigns strictly - /// increasing orders in insertion order — making the expected batch order deterministic. - fn full_bounds() -> Bounds { - Bounds { - origin: Point { - x: sp(0.0), - y: sp(0.0), - }, - size: Size { - width: sp(100.0), - height: sp(100.0), - }, - } - } + /// All test primitives cover the same region so the bounds tree assigns strictly + /// increasing orders in insertion order — making the expected batch order deterministic. + fn full_bounds() -> Bounds { + Bounds { + origin: Point { + x: sp(0.0), + y: sp(0.0), + }, + size: Size { + width: sp(100.0), + height: sp(100.0), + }, + } + } - fn mask() -> ContentMask { - ContentMask { - bounds: full_bounds(), - } - } + fn mask() -> ContentMask { + ContentMask { + bounds: full_bounds(), + } + } - fn quad() -> Quad { - Quad { - bounds: full_bounds(), - content_mask: mask(), - ..Default::default() - } - } + fn quad() -> Quad { + Quad { + bounds: full_bounds(), + content_mask: mask(), + ..Default::default() + } + } - /// A 100x100 quad whose bounds don't overlap `full_bounds()` (used to exercise the - /// order-reuse path: non-overlapping content reuses low draw-orders). - fn detached_quad() -> Quad { - let bounds = Bounds { - origin: Point { - x: sp(200.0), - y: sp(200.0), - }, - size: Size { - width: sp(100.0), - height: sp(100.0), - }, - }; - Quad { - bounds, - content_mask: ContentMask { bounds }, - ..Default::default() - } - } + /// A 100x100 quad whose bounds don't overlap `full_bounds()` (used to exercise the + /// order-reuse path: non-overlapping content reuses low draw-orders). + fn detached_quad() -> Quad { + let bounds = Bounds { + origin: Point { + x: sp(200.0), + y: sp(200.0), + }, + size: Size { + width: sp(100.0), + height: sp(100.0), + }, + }; + Quad { + bounds, + content_mask: ContentMask { bounds }, + ..Default::default() + } + } - fn boundary(is_start: bool) -> FilterBoundary { - FilterBoundary { - order: 0, - bounds: full_bounds(), - content_mask: mask(), - corner_radii: Corners::default(), - filters: smallvec::smallvec![ScaledFilter::Blur(sp(8.0))], - opacity: 1.0, - is_start, - } - } + fn boundary(is_start: bool) -> FilterBoundary { + FilterBoundary { + order: 0, + bounds: full_bounds(), + content_mask: mask(), + corner_radii: Corners::default(), + filters: smallvec::smallvec![ScaledFilter::Blur(sp(8.0))], + opacity: 1.0, + is_start, + } + } - fn backdrop() -> BackdropFilter { - BackdropFilter { - bounds: full_bounds(), - content_mask: mask(), - corner_radii: Corners::default(), - filters: smallvec::smallvec![ScaledFilter::Blur(sp(20.0))], - opacity: 1.0, - ..Default::default() - } - } + fn backdrop() -> BackdropFilter { + BackdropFilter { + bounds: full_bounds(), + content_mask: mask(), + corner_radii: Corners::default(), + filters: smallvec::smallvec![ScaledFilter::Blur(sp(20.0))], + opacity: 1.0, + ..Default::default() + } + } - fn batch_kinds(scene: &mut Scene) -> Vec<&'static str> { - scene.finish(); - scene - .batches() - .map(|batch| match batch { - PrimitiveBatch::Quads(_) => "quad", - PrimitiveBatch::BackdropFilters(_) => "backdrop", - PrimitiveBatch::FilterBoundary(ix) => { - if scene.filter_boundaries[ix].is_start { - "start" - } else { - "end" - } - } - _ => "other", - }) - .collect() - } + fn batch_kinds(scene: &mut Scene) -> Vec<&'static str> { + scene.finish(); + scene + .batches() + .map(|batch| match batch { + PrimitiveBatch::Quads(_) => "quad", + PrimitiveBatch::BackdropFilters(_) => "backdrop", + PrimitiveBatch::FilterBoundary(ix) => { + if scene.filter_boundaries[ix].is_start { + "start" + } else { + "end" + } + } + _ => "other", + }) + .collect() + } - #[test] - fn content_filter_group_brackets_its_children() { - let mut scene = Scene::default(); - // Background painted before the filtered element. - scene.insert_primitive(quad()); - // A content-filtered element: start marker, its child, end marker. - scene.insert_primitive(boundary(true)); - scene.insert_primitive(quad()); - scene.insert_primitive(boundary(false)); + #[test] + fn content_filter_group_brackets_its_children() { + let mut scene = Scene::default(); + // Background painted before the filtered element. + scene.insert_primitive(quad()); + // A content-filtered element: start marker, its child, end marker. + scene.insert_primitive(boundary(true)); + scene.insert_primitive(quad()); + scene.insert_primitive(boundary(false)); - // The start must precede the group's child and the end must follow it, so the - // renderer can redirect rendering for exactly the group's span. - assert_eq!( - batch_kinds(&mut scene), - vec!["quad", "start", "quad", "end"] - ); - } + // The start must precede the group's child and the end must follow it, so the + // renderer can redirect rendering for exactly the group's span. + assert_eq!( + batch_kinds(&mut scene), + vec!["quad", "start", "quad", "end"] + ); + } - // Note: this validates only the *scene ordering* of nested filter boundaries (start/child/ - // end interleaving), not that a renderer actually isolates both levels — that depends on the - // backend's group-texture pool (see MAX_FILTER_DEPTH) and is exercised by the `blur` example. - #[test] - fn nested_content_filters_emit_well_nested_ordering() { - let mut scene = Scene::default(); - scene.insert_primitive(boundary(true)); // outer start - scene.insert_primitive(quad()); // outer child - scene.insert_primitive(boundary(true)); // inner start - scene.insert_primitive(quad()); // inner child - scene.insert_primitive(boundary(false)); // inner end - scene.insert_primitive(boundary(false)); // outer end + // Note: this validates only the *scene ordering* of nested filter boundaries (start/child/ + // end interleaving), not that a renderer actually isolates both levels — that depends on the + // backend's group-texture pool (see MAX_FILTER_DEPTH) and is exercised by the `blur` example. + #[test] + fn nested_content_filters_emit_well_nested_ordering() { + let mut scene = Scene::default(); + scene.insert_primitive(boundary(true)); // outer start + scene.insert_primitive(quad()); // outer child + scene.insert_primitive(boundary(true)); // inner start + scene.insert_primitive(quad()); // inner child + scene.insert_primitive(boundary(false)); // inner end + scene.insert_primitive(boundary(false)); // outer end - assert_eq!( - batch_kinds(&mut scene), - vec!["start", "quad", "start", "quad", "end", "end"] - ); - } + assert_eq!( + batch_kinds(&mut scene), + vec!["start", "quad", "start", "quad", "end", "end"] + ); + } - #[test] - fn content_after_a_filter_group_sorts_above_it() { - let mut scene = Scene::default(); - // A content-filtered element: start marker, its child, end marker. - scene.insert_primitive(boundary(true)); - scene.insert_primitive(quad()); - scene.insert_primitive(boundary(false)); - // A sibling painted after the group that does NOT overlap it. Without the close-time - // order-floor it would reuse the lowest order, tie with the start marker, and be swept - // into the group (start, quad, quad, end); it must instead sort after the end marker. - scene.insert_primitive(detached_quad()); + #[test] + fn content_after_a_filter_group_sorts_above_it() { + let mut scene = Scene::default(); + // A content-filtered element: start marker, its child, end marker. + scene.insert_primitive(boundary(true)); + scene.insert_primitive(quad()); + scene.insert_primitive(boundary(false)); + // A sibling painted after the group that does NOT overlap it. Without the close-time + // order-floor it would reuse the lowest order, tie with the start marker, and be swept + // into the group (start, quad, quad, end); it must instead sort after the end marker. + scene.insert_primitive(detached_quad()); - assert_eq!( - batch_kinds(&mut scene), - vec!["start", "quad", "end", "quad"] - ); - } + assert_eq!( + batch_kinds(&mut scene), + vec!["start", "quad", "end", "quad"] + ); + } - #[test] - fn backdrop_filter_sorts_before_a_later_overlapping_quad() { - let mut scene = Scene::default(); - // Content behind the frosted panel. - scene.insert_primitive(quad()); - // The panel: its backdrop snapshot, then its (translucent) background quad on top. - scene.insert_primitive(backdrop()); - scene.insert_primitive(quad()); + #[test] + fn backdrop_filter_sorts_before_a_later_overlapping_quad() { + let mut scene = Scene::default(); + // Content behind the frosted panel. + scene.insert_primitive(quad()); + // The panel: its backdrop snapshot, then its (translucent) background quad on top. + scene.insert_primitive(backdrop()); + scene.insert_primitive(quad()); - assert_eq!(batch_kinds(&mut scene), vec!["quad", "backdrop", "quad"]); - } + assert_eq!(batch_kinds(&mut scene), vec!["quad", "backdrop", "quad"]); + } } diff --git a/crates/gpui/src/shared_uri.rs b/crates/gpui/src/shared_uri.rs index e257aaf08d..76b1973e5f 100644 --- a/crates/gpui/src/shared_uri.rs +++ b/crates/gpui/src/shared_uri.rs @@ -7,19 +7,19 @@ use crate::SharedString; 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) - } + 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()) - } + 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()) - } + fn from(value: T) -> Self { + Self(value.into()) + } } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 0ae46cdade..02e867cb5b 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -1,15 +1,15 @@ use std::{ - hash::{Hash, Hasher}, - iter, mem, - ops::Range, + hash::{Hash, Hasher}, + iter, mem, + ops::Range, }; use crate::{ - AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, - CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, - FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, - PointRefinement, Rgba, ScaledPixels, SharedString, Size, SizeRefinement, Styled, TextRun, - Window, black, phi, point, quad, rems, size, + AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, + CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, + FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, + PointRefinement, Rgba, ScaledPixels, SharedString, Size, SizeRefinement, Styled, TextRun, + Window, black, phi, point, quad, rems, size, }; use collections::HashSet; use refineable::Refineable; @@ -27,338 +27,338 @@ 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, + /// 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; + /// 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), - ) - }; + 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, + } + } + 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, + } + } 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, - }, - } - } + 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, + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize, )] pub enum TemplateColumnMinSize { - /// The column size may be 0 - #[default] - Zero, - /// The column size can be determined by the min content - MinContent, - /// The column size can be determined by the max content - MaxContent, + /// The column size may be 0 + #[default] + Zero, + /// The column size can be determined by the min content + MinContent, + /// The column 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, + 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: TemplateColumnMinSize, + /// How this template directive should be repeated + pub repeat: u16, + /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation + pub min_size: TemplateColumnMinSize, } /// 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, + /// What layout strategy should be used? + pub display: Display, - /// Should the element be painted on screen? - pub visibility: Visibility, + /// 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 axis indicated by the mouse wheel. - /// - /// This means that: - /// - The mouse wheel alone will only ever scroll the Y axis. - /// - Holding `Shift` and using the mouse wheel will scroll 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, + // 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 axis indicated by the mouse wheel. + /// + /// This means that: + /// - The mouse wheel alone will only ever scroll the Y axis. + /// - Holding `Shift` and using the mouse wheel will scroll 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, + // 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, + // 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, + // 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, + // 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, + // 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 fill color of this element + pub background: Option, - /// The border color of this element - pub border_color: Option, + /// The border color of this element + pub border_color: Option, - /// The border style of this element - pub border_style: BorderStyle, + /// The border style of this element + pub border_style: BorderStyle, - /// The radius of the corners of this element - #[refineable] - pub corner_radii: Corners, + /// The radius of the corners of this element + #[refineable] + pub corner_radii: Corners, - /// Box shadow of the element - pub box_shadow: Vec, + /// Box shadow of the element + pub box_shadow: Vec, - /// Filters applied to this element's own content and children (CSS `filter`). - pub filter: Vec, + /// Filters applied to this element's own content and children (CSS `filter`). + pub filter: Vec, - /// Filters applied to the content rendered behind this element (CSS `backdrop-filter`). - pub backdrop_filter: Vec, + /// Filters applied to the content rendered behind this element (CSS `backdrop-filter`). + pub backdrop_filter: Vec, - /// The text style of this element - #[refineable] - pub text: TextStyleRefinement, + /// 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 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 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 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 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, + /// 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 + #[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, + /// 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 - } + 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 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 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, + /// 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, } /// A graphical filter that can be applied either to an element's own content @@ -366,30 +366,30 @@ pub struct BoxShadow { /// it (via [`Styled::backdrop_filter`], like CSS `backdrop-filter`). #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum Filter { - /// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur()`. - Blur(Pixels), + /// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur()`. + Blur(Pixels), } impl Filter { - /// Whether this filter has no visible effect, so painting can skip it entirely (and the - /// element can avoid the offscreen isolation pass when *all* of its filters are identities). - /// - /// Each variant declares its own no-op case here rather than the pipeline special-casing - /// blur — adding a filter that this returns `true` for is silently dropped before it ever - /// reaches the renderer. - pub fn is_identity(&self) -> bool { - match self { - Filter::Blur(radius) => *radius <= Pixels::ZERO, - } - } + /// Whether this filter has no visible effect, so painting can skip it entirely (and the + /// element can avoid the offscreen isolation pass when *all* of its filters are identities). + /// + /// Each variant declares its own no-op case here rather than the pipeline special-casing + /// blur — adding a filter that this returns `true` for is silently dropped before it ever + /// reaches the renderer. + pub fn is_identity(&self) -> bool { + match self { + Filter::Blur(radius) => *radius <= Pixels::ZERO, + } + } - /// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the - /// renderer, scaling any pixel magnitudes by `factor` (the window scale factor). - pub fn scale(&self, factor: f32) -> ScaledFilter { - match self { - Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)), - } - } + /// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the + /// renderer, scaling any pixel magnitudes by `factor` (the window scale factor). + pub fn scale(&self, factor: f32) -> ScaledFilter { + match self { + Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)), + } + } } /// The scene-space (device-pixel) form of a [`Filter`], carried on the scene primitives that the @@ -401,44 +401,44 @@ impl Filter { /// implement-or-decline decision per backend instead of silently rendering nothing. #[derive(Clone, Copy, Debug, PartialEq)] pub enum ScaledFilter { - /// A gaussian blur with the given radius, in scaled (device) pixels. - Blur(ScaledPixels), + /// A gaussian blur with the given radius, in scaled (device) pixels. + Blur(ScaledPixels), } /// 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, + /// 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 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), } /// 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, + /// Align the text to the left of the element + #[default] + Left, - /// Center the text within the element - Center, + /// Center the text within the element + Center, - /// Align the text to the right of the element - Right, + /// Align the text to the right of the element + Right, } /// Case mapping applied at layout time while keeping **UTF-8 byte lengths** unchanged. @@ -456,169 +456,169 @@ pub enum TextAlign { /// [tw-cap]: https://tailwindcss.com/docs/text-transform #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub enum TextTransform { - /// Do not transform text. - #[default] - None, - /// Uppercase text (Unicode, byte-length preserving — see [`TextTransform`]). - Uppercase, - /// Lowercase text (Unicode, byte-length preserving — see [`TextTransform`]). - Lowercase, - /// `text-transform: capitalize` semantics (Tailwind class `capitalize`): per Unicode word, only - /// the first alphabetic character is mapped to uppercase; the rest of the string is unchanged. - Capitalize, + /// Do not transform text. + #[default] + None, + /// Uppercase text (Unicode, byte-length preserving — see [`TextTransform`]). + Uppercase, + /// Lowercase text (Unicode, byte-length preserving — see [`TextTransform`]). + Lowercase, + /// `text-transform: capitalize` semantics (Tailwind class `capitalize`): per Unicode word, only + /// the first alphabetic character is mapped to uppercase; the rest of the string is unchanged. + Capitalize, } /// 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 color of the text + pub color: Hsla, - /// The font family to use - pub font_family: SharedString, + /// The font family to use + pub font_family: SharedString, - /// The font features to use - pub font_features: FontFeatures, + /// The font features to use + pub font_features: FontFeatures, - /// The fallback fonts to use - pub font_fallbacks: Option, + /// The fallback fonts to use + pub font_fallbacks: Option, - /// The font size to use, in pixels or rems. - pub font_size: AbsoluteLength, + /// 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 line height to use, in pixels or fractions + pub line_height: DefiniteLength, - /// The font weight, e.g. bold - pub font_weight: FontWeight, + /// The font weight, e.g. bold + pub font_weight: FontWeight, - /// The font style, e.g. italic - pub font_style: FontStyle, + /// The font style, e.g. italic + pub font_style: FontStyle, - /// The background color of the text - pub background_color: 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 underline: Option, - /// The strikethrough style of the text - pub strikethrough: Option, + /// The strikethrough style of the text + pub strikethrough: Option, - /// How to handle whitespace in the text - pub white_space: WhiteSpace, + /// 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, + /// 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, + /// 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, + /// The number of lines to display before truncating the text + pub line_clamp: Option, - /// Letter spacing added between characters, in pixels (positive widens, negative tightens). - /// - /// The platform text stack may clamp values outside the range it supports. - pub letter_spacing: Option, + /// Letter spacing added between characters, in pixels (positive widens, negative tightens). + /// + /// The platform text stack may clamp values outside the range it supports. + pub letter_spacing: Option, - /// Case transformation applied at layout time. - pub text_transform: Option, + /// Case transformation applied at layout time. + pub text_transform: 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, - letter_spacing: None, - text_transform: None, - } - } + 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, + letter_spacing: None, + text_transform: 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; - } + /// 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(color) = style.color { + self.color = self.color.blend(color); + } - if let Some(factor) = style.fade_out { - self.color.fade_out(factor); - } + 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(background_color) = style.background_color { + self.background_color = Some(background_color); + } - if let Some(underline) = style.underline { - self.underline = Some(underline); - } + if let Some(underline) = style.underline { + self.underline = Some(underline); + } - if let Some(strikethrough) = style.strikethrough { - self.strikethrough = Some(strikethrough); - } + if let Some(strikethrough) = style.strikethrough { + self.strikethrough = Some(strikethrough); + } - self - } + 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, - } - } + /// 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() - } + /// 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, - letter_spacing: self.letter_spacing, - } - } + /// 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, + letter_spacing: self.letter_spacing, + } + } } /// A highlight style to apply, similar to a `TextStyle` except @@ -630,476 +630,476 @@ impl TextStyle { /// [`crate::StyledText::with_default_highlights`]). #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct HighlightStyle { - /// The color of the text - pub color: Option, + /// The color of the text + pub color: Option, - /// The font weight, e.g. bold - pub font_weight: Option, + /// The font weight, e.g. bold + pub font_weight: Option, - /// The font style, e.g. italic - pub font_style: Option, + /// The font style, e.g. italic + pub font_style: Option, - /// The background color of the text - pub background_color: 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 underline: Option, - /// The underline style of the text - pub strikethrough: 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, + /// 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(), - )); - } + 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())) - } + /// 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 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, - ) -> Option> { - match self.overflow { - Point { - x: Overflow::Visible, - y: Overflow::Visible, - } => None, - _ => { - let mut min = bounds.origin; - let mut max = bounds.bottom_right(); + /// 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, + ) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let mut min = bounds.origin; + let mut max = bounds.bottom_right(); - if self - .border_color - .is_some_and(|color| !color.is_transparent()) - { - min.x += self.border_widths.left.to_pixels(rem_size); - max.x -= self.border_widths.right.to_pixels(rem_size); - min.y += self.border_widths.top.to_pixels(rem_size); - max.y -= self.border_widths.bottom.to_pixels(rem_size); - } + if self + .border_color + .is_some_and(|color| !color.is_transparent()) + { + min.x += self.border_widths.left.to_pixels(rem_size); + max.x -= self.border_widths.right.to_pixels(rem_size); + min.y += self.border_widths.top.to_pixels(rem_size); + max.y -= self.border_widths.bottom.to_pixels(rem_size); + } - let bounds = match ( - self.overflow.x == Overflow::Visible, - 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 bounds = match ( + self.overflow.x == Overflow::Visible, + 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), + }; - Some(ContentMask { bounds }) - } - } - } + Some(ContentMask { bounds }) + } + } + } - /// 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) - } + /// 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())); - } + #[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 corner_radii = self - .corner_radii - .to_pixels(rem_size) - .clamp_radii_for_quad_size(bounds.size); + let rem_size = window.rem_size(); + let corner_radii = self + .corner_radii + .to_pixels(rem_size) + .clamp_radii_for_quad_size(bounds.size); - window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow); + window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow); - // Blur the content behind this element before its (typically translucent) background - // is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`). - if !self.backdrop_filter.is_empty() { - window.paint_backdrop_filter(bounds, corner_radii, &self.backdrop_filter); - } + // Blur the content behind this element before its (typically translucent) background + // is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`). + if !self.backdrop_filter.is_empty() { + window.paint_backdrop_filter(bounds, corner_radii, &self.backdrop_filter); + } - // The element's own box — background, inset shadows, children, and border — painted as a - // unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element - // and its children together as one group; without a filter it paints directly. - let paint_box = |window: &mut Window, cx: &mut App| { - 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, + // The element's own box — background, inset shadows, children, and border — painted as a + // unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element + // and its children together as one group; without a filter it paints directly. + let paint_box = |window: &mut Window, cx: &mut App| { + 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, - )); - } + 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); + window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow); - continuation(window, cx); + continuation(window, cx); - if self.is_border_visible() { - let border_widths = self.border_widths.to_pixels(rem_size); - 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, - )); - } - }; + if self.is_border_visible() { + let border_widths = self.border_widths.to_pixels(rem_size); + 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, + )); + } + }; - if self.filter.is_empty() { - paint_box(window, cx); - } else { - window.with_filter_layer(bounds, corner_radii, &self.filter, |window| { - paint_box(window, cx); - }); - } + if self.filter.is_empty() { + paint_box(window, cx); + } else { + window.with_filter_layer(bounds, corner_radii, &self.filter, |window| { + paint_box(window, cx); + }); + } - #[cfg(debug_assertions)] - if self.debug_below { - cx.remove_global::(); - } - } + #[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()) - } + 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(), - filter: Default::default(), - backdrop_filter: Default::default(), - text: TextStyleRefinement::default(), - mouse_cursor: None, - opacity: None, - grid_rows: None, - grid_cols: None, - grid_location: None, + 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(), + filter: Default::default(), + backdrop_filter: 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, - } - } + #[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, + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, )] pub struct UnderlineStyle { - /// The thickness of the underline. - pub thickness: Pixels, + /// The thickness of the underline. + pub thickness: Pixels, - /// The color of the underline. - pub color: Option, + /// The color of the underline. + pub color: Option, - /// Whether the underline should be wavy, like in a spell checker. - pub wavy: bool, + /// 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, + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, )] pub struct StrikethroughStyle { - /// The thickness of the strikethrough. - pub thickness: Pixels, + /// The thickness of the strikethrough. + pub thickness: Pixels, - /// The color of the strikethrough. - pub color: Option, + /// 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), + /// 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), - } - } + /// 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()) - } + fn default() -> Self { + Self::Color(Background::default()) + } } impl From for Fill { - fn from(color: Hsla) -> Self { - Self::Color(color.into()) - } + fn from(color: Hsla) -> Self { + Self::Color(color.into()) + } } impl From for Fill { - fn from(color: Rgba) -> Self { - Self::Color(color.into()) - } + fn from(color: Rgba) -> Self { + Self::Color(color.into()) + } } impl From for Fill { - fn from(background: Background) -> Self { - Self::Color(background) - } + fn from(background: Background) -> Self { + Self::Color(background) + } } impl From for HighlightStyle { - fn from(other: TextStyle) -> Self { - Self::from(&other) - } + 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, - } - } + 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), - } - } + /// 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() - } - } + 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() - } - } + 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() - } - } + 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() - } - } + 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)>, + 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 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)); - } + 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 - }) + 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. @@ -1110,26 +1110,26 @@ pub fn combine_highlights( #[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, + /// 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 @@ -1160,33 +1160,33 @@ pub type JustifySelf = AlignItems; #[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, + /// 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 @@ -1202,15 +1202,15 @@ pub type JustifyContent = AlignContent; #[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, + /// 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. @@ -1221,13 +1221,13 @@ pub enum Display { #[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, + /// 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. @@ -1244,23 +1244,23 @@ pub enum FlexWrap { #[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, + /// 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 @@ -1279,20 +1279,20 @@ pub enum FlexDirection { #[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 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. @@ -1307,294 +1307,294 @@ pub enum Overflow { #[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, + /// 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::FlexStart, - AlignItems::FlexEnd => Self::FlexEnd, - AlignItems::Center => Self::Center, - AlignItems::Baseline => Self::Baseline, - AlignItems::Stretch => Self::Stretch, - } - } + fn from(value: AlignItems) -> Self { + match value { + AlignItems::Start => Self::Start, + AlignItems::End => Self::End, + AlignItems::FlexStart => Self::FlexStart, + AlignItems::FlexEnd => Self::FlexEnd, + 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::FlexStart, - AlignContent::FlexEnd => Self::FlexEnd, - AlignContent::Center => Self::Center, - AlignContent::Stretch => Self::Stretch, - AlignContent::SpaceBetween => Self::SpaceBetween, - AlignContent::SpaceEvenly => Self::SpaceEvenly, - AlignContent::SpaceAround => Self::SpaceAround, - } - } + fn from(value: AlignContent) -> Self { + match value { + AlignContent::Start => Self::Start, + AlignContent::End => Self::End, + AlignContent::FlexStart => Self::FlexStart, + AlignContent::FlexEnd => Self::FlexEnd, + AlignContent::Center => Self::Center, + AlignContent::Stretch => Self::Stretch, + AlignContent::SpaceBetween => Self::SpaceBetween, + AlignContent::SpaceEvenly => Self::SpaceEvenly, + AlignContent::SpaceAround => Self::SpaceAround, + } + } } 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, - } - } + 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, - } - } + 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, - } - } + 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, - } - } + 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, - } - } + 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 crate::{blue, green, px, red, yellow}; - use super::*; + use super::*; - #[test] - 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." - ); + #[test] + 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 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_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 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 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 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 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" - ); - } + 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" + ); + } - #[test] - 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() - } - ) - ] - ); - } + #[test] + 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() + } + ) + ] + ); + } - #[test] - 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)); + #[test] + 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(AbsoluteLength::from(px(20.0))), + style.text_style().unwrap().font_size + ); - assert_eq!( - Some(FontWeight::SEMIBOLD), - style.text_style().unwrap().font_weight - ); - } + assert_eq!( + Some(FontWeight::SEMIBOLD), + style.text_style().unwrap().font_weight + ); + } } diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 6bd0cf88ed..71c483941d 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,14 +1,14 @@ use crate::{ - self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle, - DefiniteLength, Display, Fill, Filter, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle, - FontWeight, GridPlacement, GridTemplate, Hsla, JustifyContent, Length, Pixels, SharedString, - StrikethroughStyle, StyleRefinement, TemplateColumnMinSize, TextAlign, TextOverflow, - TextStyleRefinement, TextTransform, UnderlineStyle, WhiteSpace, px, relative, rems, + self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle, + DefiniteLength, Display, Fill, Filter, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle, + FontWeight, GridPlacement, GridTemplate, Hsla, JustifyContent, Length, Pixels, SharedString, + StrikethroughStyle, StyleRefinement, TemplateColumnMinSize, TextAlign, TextOverflow, + TextStyleRefinement, TextTransform, 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, + 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("…"); @@ -16,906 +16,906 @@ const ELLIPSIS: SharedString = SharedString::new_static("…"); /// 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 + 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!(); - - /// Blur this element's own content and children, like CSS `filter: blur()`. - /// - /// This isolates the element's subtree, blurs it as a group, and composites the - /// result back. To blur the content *behind* the element instead (frosted glass), - /// use [`Styled::backdrop_blur`]. - /// - /// *Appends* to the element's filter chain, so it composes with other convenience - /// setters (`.blur(8.).()`). To replace the whole chain, use - /// [`Styled::filter`]. - fn blur(mut self, radius: impl Into) -> Self { - self.style() - .filter - .get_or_insert_with(Vec::new) - .push(Filter::Blur(radius.into())); - self - } - - /// Set (replacing any existing) the full list of filters applied to this element's own - /// content, like CSS `filter`. To add a single filter to the chain instead, use the - /// convenience setters such as [`Styled::blur`]. - fn filter(mut self, filters: impl Into>) -> Self { - self.style().filter = Some(filters.into()); - self - } - - /// Blur the content rendered behind this element — a frosted-glass effect — like CSS - /// `backdrop-filter: blur()`. Typically paired with a translucent [`Styled::bg`] - /// so the background tints the blurred backdrop. - /// - /// *Appends* to the element's backdrop-filter chain. To replace the whole chain, use - /// [`Styled::backdrop_filter`]. - fn backdrop_blur(mut self, radius: impl Into) -> Self { - self.style() - .backdrop_filter - .get_or_insert_with(Vec::new) - .push(Filter::Blur(radius.into())); - self - } - - /// Set (replacing any existing) the full list of filters applied to the content behind this - /// element, like CSS `backdrop-filter`. To add a single filter to the chain instead, use the - /// convenience setters such as [`Styled::backdrop_blur`]. - fn backdrop_filter(mut self, filters: impl Into>) -> Self { - self.style().backdrop_filter = Some(filters.into()); - self - } - - /// 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 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 letter spacing for text in this element and its children. - fn letter_spacing(mut self, spacing: impl Into) -> Self { - self.text_style().letter_spacing = Some(spacing.into()); - self - } - - /// Sets the case transformation for text in this element and its children. - fn text_transform(mut self, transform: TextTransform) -> Self { - self.text_style().text_transform = Some(transform); - self - } - - /// 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 - } - - /// 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 element to allow a flex item to grow to fill any available space. - /// [Docs](https://tailwindcss.com/docs/flex-grow) - fn flex_grow(mut self) -> Self { - self.style().flex_grow = Some(1.); - self - } - - /// Sets the element to prevent a flex item from growing. - /// [Docs](https://tailwindcss.com/docs/flex-grow#dont-grow) - fn flex_grow_0(mut self) -> Self { - self.style().flex_grow = Some(0.); - self - } - - /// Sets the element to allow a flex item to shrink if needed. - /// [Docs](https://tailwindcss.com/docs/flex-shrink) - fn flex_shrink(mut self) -> Self { - self.style().flex_shrink = Some(1.); - self - } - - /// Sets the element to prevent a flex item from shrinking. - /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) - fn flex_shrink_0(mut self) -> Self { - self.style().flex_shrink = Some(0.); - 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: TemplateColumnMinSize::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: TemplateColumnMinSize::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: TemplateColumnMinSize::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: TemplateColumnMinSize::Zero, - }); - 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 - } + /// 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!(); + + /// Blur this element's own content and children, like CSS `filter: blur()`. + /// + /// This isolates the element's subtree, blurs it as a group, and composites the + /// result back. To blur the content *behind* the element instead (frosted glass), + /// use [`Styled::backdrop_blur`]. + /// + /// *Appends* to the element's filter chain, so it composes with other convenience + /// setters (`.blur(8.).()`). To replace the whole chain, use + /// [`Styled::filter`]. + fn blur(mut self, radius: impl Into) -> Self { + self.style() + .filter + .get_or_insert_with(Vec::new) + .push(Filter::Blur(radius.into())); + self + } + + /// Set (replacing any existing) the full list of filters applied to this element's own + /// content, like CSS `filter`. To add a single filter to the chain instead, use the + /// convenience setters such as [`Styled::blur`]. + fn filter(mut self, filters: impl Into>) -> Self { + self.style().filter = Some(filters.into()); + self + } + + /// Blur the content rendered behind this element — a frosted-glass effect — like CSS + /// `backdrop-filter: blur()`. Typically paired with a translucent [`Styled::bg`] + /// so the background tints the blurred backdrop. + /// + /// *Appends* to the element's backdrop-filter chain. To replace the whole chain, use + /// [`Styled::backdrop_filter`]. + fn backdrop_blur(mut self, radius: impl Into) -> Self { + self.style() + .backdrop_filter + .get_or_insert_with(Vec::new) + .push(Filter::Blur(radius.into())); + self + } + + /// Set (replacing any existing) the full list of filters applied to the content behind this + /// element, like CSS `backdrop-filter`. To add a single filter to the chain instead, use the + /// convenience setters such as [`Styled::backdrop_blur`]. + fn backdrop_filter(mut self, filters: impl Into>) -> Self { + self.style().backdrop_filter = Some(filters.into()); + self + } + + /// 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 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 letter spacing for text in this element and its children. + fn letter_spacing(mut self, spacing: impl Into) -> Self { + self.text_style().letter_spacing = Some(spacing.into()); + self + } + + /// Sets the case transformation for text in this element and its children. + fn text_transform(mut self, transform: TextTransform) -> Self { + self.text_style().text_transform = Some(transform); + self + } + + /// 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 + } + + /// 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 element to allow a flex item to grow to fill any available space. + /// [Docs](https://tailwindcss.com/docs/flex-grow) + fn flex_grow(mut self) -> Self { + self.style().flex_grow = Some(1.); + self + } + + /// Sets the element to prevent a flex item from growing. + /// [Docs](https://tailwindcss.com/docs/flex-grow#dont-grow) + fn flex_grow_0(mut self) -> Self { + self.style().flex_grow = Some(0.); + self + } + + /// Sets the element to allow a flex item to shrink if needed. + /// [Docs](https://tailwindcss.com/docs/flex-shrink) + fn flex_shrink(mut self) -> Self { + self.style().flex_shrink = Some(1.); + self + } + + /// Sets the element to prevent a flex item from shrinking. + /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) + fn flex_shrink_0(mut self) -> Self { + self.style().flex_shrink = Some(0.); + 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: TemplateColumnMinSize::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: TemplateColumnMinSize::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: TemplateColumnMinSize::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: TemplateColumnMinSize::Zero, + }); + 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/src/subscription.rs b/crates/gpui/src/subscription.rs index b0c55a3966..2f8d4b6641 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -1,351 +1,351 @@ use collections::BTreeMap; use gpui_util::post_inc; use std::{ - cell::{Cell, RefCell}, - fmt::Debug, - rc::Rc, + cell::{Cell, RefCell}, + fmt::Debug, + rc::Rc, }; pub(crate) struct SubscriberSet( - Rc>>, + Rc>>, ); impl Clone for SubscriberSet { - fn clone(&self) -> Self { - SubscriberSet(self.0.clone()) - } + fn clone(&self) -> Self { + SubscriberSet(self.0.clone()) + } } struct SubscriberSetState { - subscribers: BTreeMap>>>, - next_subscriber_id: usize, + subscribers: BTreeMap>>>, + next_subscriber_id: usize, } struct Subscriber { - active: Rc>, - dropped: Rc>, - callback: Callback, + active: Rc>, + dropped: Rc>, + callback: Callback, } impl SubscriberSet where - EmitterKey: 'static + Ord + Clone + Debug, - Callback: 'static, + EmitterKey: 'static + Ord + Clone + Debug, + Callback: 'static, { - pub fn new() -> Self { - Self(Rc::new(RefCell::new(SubscriberSetState { - subscribers: Default::default(), - next_subscriber_id: 0, - }))) - } + 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(); + /// 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 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; - }; + 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)) - } + 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 - } - }) - } + 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; - }; + /// 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(); + 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); - } + // 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)); - } - } + 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>, + 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)), - } - } + /// 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(); - } + /// 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(); - } - })), - } - } + /// 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(); - } - } + 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() - } + 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}; + use super::*; + use crate::{Global, TestApp}; - #[test] - fn test_unsubscribe_during_callback_with_insert() { - struct TestGlobal; - impl Global for TestGlobal {} + #[test] + fn test_unsubscribe_during_callback_with_insert() { + struct TestGlobal; + impl Global for TestGlobal {} - let mut app = TestApp::new(); - app.set_global(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 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(); + 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 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(); - }) - } - })); + // 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); + // 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"); - } + // 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 {} + #[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 mut app = TestApp::new(); + app.set_global(TestGlobal); - let observer_b_count = Rc::new(Cell::new(0usize)); - let sub_b: Rc>> = Default::default(); + 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 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); - }) - } - })); + // 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" - ); - } + 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 {} + #[test] + fn test_self_drop_during_callback() { + struct TestGlobal; + impl Global for TestGlobal {} - let mut app = TestApp::new(); - app.set_global(TestGlobal); + let mut app = TestApp::new(); + app.set_global(TestGlobal); - let count = Rc::new(Cell::new(0usize)); - let sub: Rc>> = Default::default(); + 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(); - }) - } - })); + *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); - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(count.get(), 1, "should not fire after self-drop"); - } + 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 {} + #[test] + fn test_subscription_drop() { + struct TestGlobal; + impl Global for TestGlobal {} - let mut app = TestApp::new(); - app.set_global(TestGlobal); + let mut app = TestApp::new(); + app.set_global(TestGlobal); - let count = Rc::new(Cell::new(0usize)); + 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); - }) - } - }); + let subscription = app.update({ + let count = count.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + count.set(count.get() + 1); + }) + } + }); - drop(subscription); + drop(subscription); - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(count.get(), 0, "should not fire after drop"); - } + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(count.get(), 0, "should not fire after drop"); + } } diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index 3b5062f55d..51e0f2273d 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -1,13 +1,13 @@ use crate::{ - AssetSource, DevicePixels, IsZero, RenderImage, Result, SharedString, Size, - swap_rgba_pa_to_bgra, + 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}, + hash::Hash, + sync::{Arc, LazyLock, OnceLock}, }; #[cfg(target_os = "macos")] @@ -18,63 +18,63 @@ 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", + "Noto Color Emoji", + "Emoji One", + "Twitter Color Emoji", + "JoyPixels", ]; #[cfg(not(any( - target_os = "macos", - target_os = "windows", - target_os = "linux", - target_os = "freebsd", + 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)) + 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) + 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], + 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, - }; + 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; - }; + let Some(id) = db.query(&query) else { + continue; + }; - if fonts.contains(&id) || !font_has_char(db, id, ch) { - continue; - } + if fonts.contains(&id) || !font_has_char(db, id, ch) { + continue; + } - return Some(id); - } + return Some(id); + } - None + None } /// When rendering SVGs, we render them at twice the size to get a higher-quality result. @@ -83,334 +83,334 @@ 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, + 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>, + asset_source: Arc, + usvg_options: Arc>, } /// The size in which to render the SVG. pub enum SvgSize { - /// An absolute size in device pixels. - Size(Size), - /// A scaling factor to apply to the size provided by the SVG. - ScaleFactor(f32), + /// An absolute size in device pixels. + Size(Size), + /// A scaling factor to apply to the size provided by the SVG. + ScaleFactor(f32), } 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) - }); + /// 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()); + // 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); - } - } + 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), - } - } + 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), + } + } - /// Renders the given bytes into an image buffer. - pub fn render_single_frame( - &self, - bytes: &[u8], - scale_factor: f32, - ) -> Result, usvg::Error> { - self.render_pixmap( - bytes, - SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR), - ) - .map(|pixmap| { - let mut buffer = - image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) - .unwrap(); + /// Renders the given bytes into an image buffer. + pub fn render_single_frame( + &self, + bytes: &[u8], + scale_factor: f32, + ) -> Result, usvg::Error> { + self.render_pixmap( + bytes, + SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR), + ) + .map(|pixmap| { + 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); - } + 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 = SMOOTH_SVG_SCALE_FACTOR; - Arc::new(image) - }) - } + let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)])); + image.scale_factor = SMOOTH_SVG_SCALE_FACTOR; + Arc::new(image) + }) + } - 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"); + 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))?; + 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::>(); + // 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))) - }; + 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) - } - } + 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)?; - let svg_size = tree.size(); - let scale = match size { - SvgSize::Size(size) => size.width.0 as f32 / svg_size.width(), - SvgSize::ScaleFactor(scale) => scale, - }; + fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; + let svg_size = tree.size(); + let scale = match size { + SvgSize::Size(size) => size.width.0 as f32 / svg_size.width(), + SvgSize::ScaleFactor(scale) => scale, + }; - // Render the SVG to a pixmap with the specified width and height. - let mut pixmap = resvg::tiny_skia::Pixmap::new( - (svg_size.width() * scale) as u32, - (svg_size.height() * scale) as u32, - ) - .ok_or(usvg::Error::InvalidSize)?; + // Render the SVG to a pixmap with the specified width and height. + let mut pixmap = resvg::tiny_skia::Pixmap::new( + (svg_size.width() * scale) as u32, + (svg_size.height() * scale) as u32, + ) + .ok_or(usvg::Error::InvalidSize)?; - let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); + let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); - resvg::render(&tree, transform, &mut pixmap.as_mut()); + resvg::render(&tree, transform, &mut pixmap.as_mut()); - Ok(pixmap) - } + 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}"), - } - } + 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}; + 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"), - ]; + 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), - _ => {} - } - } - } + 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}; + 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"); + 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"); - 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 - } + 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 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 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); + #[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, - ]; + 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" - ); - } - } + 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(); + #[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(); + 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, '│')); - } + 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); + #[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 - ); - } + 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/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index a205005963..dafda5547a 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -9,26 +9,26 @@ 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, + current_path: TabStopPath, + pub(crate) insertion_history: Vec, + by_id: FxHashMap, + order: SumTree, } #[derive(Debug, Clone)] pub enum TabStopOperation { - Insert(FocusHandle), - Group(TabIndex), - GroupEnd, + Insert(FocusHandle), + Group(TabIndex), + GroupEnd, } impl TabStopOperation { - fn focus_handle(&self) -> Option<&FocusHandle> { - match self { - TabStopOperation::Insert(focus_handle) => Some(focus_handle), - _ => None, - } - } + fn focus_handle(&self) -> Option<&FocusHandle> { + match self { + TabStopOperation::Insert(focus_handle) => Some(focus_handle), + _ => None, + } + } } type TabIndex = isize; @@ -38,574 +38,574 @@ 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, + /// 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, + /// 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)) - } + 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)) - } + 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(()), - } - } + 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 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 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 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 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)); - } - }; + 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); + 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) - } - } + 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(); - } + 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() - } + 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)); - } - }; + 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); + 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) - } - } + 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(); - } + 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() - } + 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 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 fn paint_index(&self) -> usize { + self.insertion_history.len() + } - 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 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) - } + 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 sum_tree::SeekTarget; - use crate::tab_stop::{TabStopNode, TabStopPath}; + use crate::tab_stop::{TabStopNode, TabStopPath}; - #[derive(Clone, Debug)] - pub struct TabStopOrderNodeSummary { - max_index: usize, - max_path: TabStopPath, - pub tab_stops: usize, - } + #[derive(Clone, Debug)] + pub struct TabStopOrderNodeSummary { + max_index: usize, + max_path: TabStopPath, + pub tab_stops: usize, + } - pub type TabStopCount = 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, - } - } + 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; - } - } + 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; + impl sum_tree::KeyedItem for TabStopNode { + type Key = Self; - fn key(&self) -> Self::Key { - self.clone() - } - } + fn key(&self) -> Self::Key { + self.clone() + } + } - impl sum_tree::Item for TabStopNode { - type Summary = TabStopOrderNodeSummary; + 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 }, - } - } - } + 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 - } + 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; - } - } + 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() - } + 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(); - } - } + 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, - ), - ) - } - } + 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 itertools::Itertools as _; - use crate::{FocusHandle, FocusId, FocusMap, TabStopMap}; - use std::sync::Arc; + 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(); + #[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), - ]; + 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(), - ]; + 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); - } + 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::>() - ); + 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(),); + // 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()) - ); + 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()) - ); - } + // 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(); + #[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 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); - } + // 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)>, - } + #[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(), - } - } + 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_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_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 - } + #[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 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(); + 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); + // 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() - ); + // 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); + // 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 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_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_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_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_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_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() { + 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(); - } + #[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/src/taffy.rs b/crates/gpui/src/taffy.rs index 4844748d6c..70260bfc7f 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -1,357 +1,357 @@ 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, - }, + 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 stacksafe::{StackSafe, stacksafe}; 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, + TaffyTree, TraversePartialTree as _, + geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, + prelude::{max_content, min_content}, + style::AvailableSpace as TaffyAvailableSpace, + tree::NodeId, }; type NodeMeasureFn = StackSafe< - Box< - dyn FnMut( - Size>, - Size, - &mut Window, - &mut App, - ) -> Size, - >, + Box< + dyn FnMut( + Size>, + Size, + &mut Window, + &mut App, + ) -> Size, + >, >; struct NodeContext { - measure: NodeMeasureFn, + 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, + 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 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 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); + 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() - } - } + 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); + 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); - self.taffy - .new_leaf_with_context( - taffy_style, - NodeContext { - measure: StackSafe::new(Box::new(measure)), - }, - ) - .expect(EXPECT_MESSAGE) - .into() - } + self.taffy + .new_leaf_with_context( + taffy_style, + NodeContext { + measure: StackSafe::new(Box::new(measure)), + }, + ) + .expect(EXPECT_MESSAGE) + .into() + } - // Used to understand performance - #[allow(dead_code)] - fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { - let mut count = 0; + // 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; + 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))? - } + // Count all of this child's children. + count += self.count_all_children(LayoutId(child))? + } - Ok(count) - } + 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) - ); + // 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; + 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))?); - } + 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) - } + 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(); + // 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))); + for child in self.taffy.children(parent.0)? { + edges.push((parent, LayoutId(child))); - edges.extend(self.get_edges(LayoutId(child))?); - } + edges.extend(self.get_edges(LayoutId(child))?); + } - Ok(edges) - } + Ok(edges) + } - #[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)?); + #[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)); - // } - // + // 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), - ); - } - } + 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 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), - ); + 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(); - }; + 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 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 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); - } + 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. + // 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; - } + 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 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_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 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 - } + 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 @@ -360,392 +360,392 @@ impl TaffyLayoutEngine { 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) } - } + 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); - } + 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) - } + fn from(node_id: NodeId) -> Self { + Self(node_id) + } } impl From for NodeId { - fn from(layout_id: LayoutId) -> NodeId { - layout_id.0 - } + 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)) + 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, + 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), - } + 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; + 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_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_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::TemplateColumnMinSize::Zero => { - vec![repeat( - template.repeat, - vec![minmax(length(0.0_f32), fr(1.0_f32))], - )] - } - // grid-template-*: repeat(, minmax(min-content, 1fr)); - crate::TemplateColumnMinSize::MinContent => { - vec![repeat( - template.repeat, - vec![minmax(min_content(), fr(1.0_f32))], - )] - } - // grid-template-*: repeat(, minmax(0, max-content)) - crate::TemplateColumnMinSize::MaxContent => { - vec![repeat( - template.repeat, - vec![minmax(length(0.0_f32), max_content())], - )] - } - } - }) - .unwrap_or_default() - } + fn to_grid_repeat( + unit: &Option, + ) -> Vec> { + unit.map(|template| { + match template.min_size { + // grid-template-*: repeat(, minmax(0, 1fr)); + crate::TemplateColumnMinSize::Zero => { + vec![repeat( + template.repeat, + vec![minmax(length(0.0_f32), fr(1.0_f32))], + )] + } + // grid-template-*: repeat(, minmax(min-content, 1fr)); + crate::TemplateColumnMinSize::MinContent => { + vec![repeat( + template.repeat, + vec![minmax(min_content(), fr(1.0_f32))], + )] + } + // grid-template-*: repeat(, minmax(0, max-content)) + crate::TemplateColumnMinSize::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() - } - } + 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) - } + 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(), - } - } + 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(), - } - } + 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) - } - } - } + 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) - } - } - } + 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), - } - } + 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)) - } + 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)) - } + 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)) - } + 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, + T: Into, + T2: Clone + Debug + Default + PartialEq, { - fn from(point: TaffyPoint) -> Point { - Point { - x: point.x.into(), - y: point.y.into(), - } - } + 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, + T: Into + Clone + Debug + Default + PartialEq, { - fn from(val: Point) -> Self { - TaffyPoint { - x: val.x.into(), - y: val.y.into(), - } - } + 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, + 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), - } - } + 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, + 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), - } - } + 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, + T: Into, + U: Clone + Debug + Default + PartialEq, { - fn from(taffy_size: TaffySize) -> Self { - Size { - width: taffy_size.width.into(), - height: taffy_size.height.into(), - } - } + 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, + T: Into + Clone + Debug + Default + PartialEq, { - fn from(size: Size) -> Self { - TaffySize { - width: size.width.into(), - height: size.height.into(), - } - } + 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, + /// 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, - } - } + /// 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, - } - } + 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, - } - } + 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) - } + 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), - } - } + fn from(size: Size) -> Self { + Size { + width: AvailableSpace::Definite(size.width), + height: AvailableSpace::Definite(size.height), + } + } } #[cfg(test)] mod tests { - use super::*; + 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); + #[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) - ); - } + 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/src/test.rs b/crates/gpui/src/test.rs index a93424edea..8f421b321a 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -29,9 +29,9 @@ 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, + env, + panic::{self, RefUnwindSafe, UnwindSafe}, + pin::Pin, }; /// Strategy injected into `#[gpui::property_test]` tests to control the seed @@ -41,10 +41,10 @@ use std::{ /// 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(), - } + 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 @@ -55,14 +55,14 @@ pub fn seed_strategy() -> impl Strategy { /// 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, + 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 + 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 @@ -72,149 +72,149 @@ pub fn apply_seed_to_proptest_config( /// 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>, + 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 - }); + 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), - } + 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, + 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); + 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(); - }); + 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); - } - } - } - } - } + 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], + 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 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 env_num = env::var("SEED") + .map(|seed| seed.parse().expect("invalid SEED variable as integer")) + .ok(); - let empty_range = || 0..0; + 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() - }; + 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 `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 - }; + // 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) + 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, + rx: Pin>>, + _subscription: Subscription, } impl futures::Stream for Observation { - type Item = T; + 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) - } + 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); + 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 } + Observation { rx, _subscription } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 2cfa061321..f8c85b48e8 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -13,8 +13,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ - Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, - StrikethroughStyle, TextRenderingMode, UnderlineStyle, px, + Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, + StrikethroughStyle, TextRenderingMode, UnderlineStyle, px, }; use anyhow::{Context as _, anyhow}; use collections::FxHashMap; @@ -24,12 +24,12 @@ 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, + borrow::Cow, + cmp, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut, Range}, + sync::Arc, }; /// An opaque identifier for a specific font. @@ -49,867 +49,867 @@ 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]>, + 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 - ], - } - } + /// 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(); - names.dedup(); - names - } + /// 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(); + 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) - } + /// 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}")), - } - } + /// 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 - } - } + 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() - } + /// 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; - } - } + /// 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(", ") - ); - } + panic!( + "failed to resolve font '{}' or any of the fallbacks: {}", + font.family, + self.fallback_font_stack + .iter() + .map(|fallback| &fallback.family) + .join(", ") + ); + } - /// 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 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 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; + /// 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) - } + 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, - letter_spacing: None, - }], - ) - .width - } + // 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, + letter_spacing: None, + }], + ) + .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 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 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 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) - } + /// 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 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 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 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 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 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 - } + /// 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(); + 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) - } - } + 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())); + /// 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(), - } - } + 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) - } - } + /// 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) - } + 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 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) - } + /// 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) + } } #[cfg(test)] impl TextSystem { - /// Reach the platform shaper from crate tests (e.g. `line_wrapper`) without a [`WindowTextSystem`]. - pub(crate) fn platform_text_system_for_tests(&self) -> Arc { - self.platform_text_system.clone() - } + /// Reach the platform shaper from crate tests (e.g. `line_wrapper`) without a [`WindowTextSystem`]. + pub(crate) fn platform_text_system_for_tests(&self) -> Arc { + self.platform_text_system.clone() + } } /// The GPUI text layout subsystem. #[derive(Deref)] pub struct WindowTextSystem { - line_layout_cache: LineLayoutCache, - #[deref] - text_system: Arc, + 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, - } - } + /// 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 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 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) - } + 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" - ); + /// 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 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); + let layout = self.layout_line(&text, font_size, runs, force_width); - ShapedLine { - layout, - text, - decoration_runs, - } - } + 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, - }); - } + /// 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 - }, - ); + 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(""); + // 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, - } - } + 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, - /// where each run dictates the length of utf8 characters in `text` that it styles. - /// The length (utf8 characters) of last item in `runs` is semantically ignored as it - /// represents the "rest" of the `text`. - /// - /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. - /// - /// If the text provided is SharedString and does not contain new-lines, - /// it will be used as-is without additional allocations. - /// If the text provided is not a SharedString or contains new-lines, new SharedStrings - /// will be allocated for each substring between new-line characters (minimum of 1). - pub fn shape_text + Into>( - &self, - text: S, - 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(); + /// 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, + /// where each run dictates the length of utf8 characters in `text` that it styles. + /// The length (utf8 characters) of last item in `runs` is semantically ignored as it + /// represents the "rest" of the `text`. + /// + /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. + /// + /// If the text provided is SharedString and does not contain new-lines, + /// it will be used as-is without additional allocations. + /// If the text provided is not a SharedString or contains new-lines, new SharedStrings + /// will be allocated for each substring between new-line characters (minimum of 1). + pub fn shape_text + Into>( + &self, + text: S, + 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 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 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 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 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 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); - let letter_spacing = run.letter_spacing; - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && font_run.letter_spacing == letter_spacing - && !decoration_changed - { - font_run.len += run_len_within_line; - } else { - font_runs.push(FontRun { - len: run_len_within_line, - font_id, - letter_spacing, - }); - } + let font_id = self.resolve_font(&run.font); + let letter_spacing = run.letter_spacing; + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && font_run.letter_spacing == letter_spacing + && !decoration_changed + { + font_run.len += run_len_within_line; + } else { + font_runs.push(FontRun { + len: run_len_within_line, + font_id, + letter_spacing, + }); + } - // 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; - } + // 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(); + 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, - }); + 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(); - } - } - }; + // Skip `\n` character. + if let Some(run) = runs.peek_mut() { + run.len -= 1; + if run.len == 0 { + runs.next(); + } + } + }; - let mut split_lines = text.as_ref().split('\n'); + let mut split_lines = text.as_ref().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.as_ref().len(); - process_line(text.into(), 0, end); - } + // 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.as_ref().len(); + process_line(text.into(), 0, end); + } - self.font_runs_pool.lock().push(font_runs); + self.font_runs_pool.lock().push(font_runs); - Ok(lines) - } + Ok(lines) + } - pub(crate) fn finish_frame(&self) { - self.line_layout_cache.finish_frame() - } + 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(); + /// 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 - }; + 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); - let letter_spacing = run.letter_spacing; - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && font_run.letter_spacing == letter_spacing - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - letter_spacing, - }); - } - } + let font_id = self.resolve_font(&run.font); + let letter_spacing = run.letter_spacing; + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && font_run.letter_spacing == letter_spacing + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + letter_spacing, + }); + } + } - let layout = self.line_layout_cache.layout_line( - &SharedString::new(text), - font_size, - &font_runs, - force_width, - ); + 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); + self.font_runs_pool.lock().push(font_runs); - layout - } + 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, - letter_spacing: None, - }], - None, - ) - .width - } + /// 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, + letter_spacing: None, + }], + 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') - } + /// 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(); + /// 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 - }; + 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); - let letter_spacing = run.letter_spacing; - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && font_run.letter_spacing == letter_spacing - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - letter_spacing, - }); - } - } + let font_id = self.resolve_font(&run.font); + let letter_spacing = run.letter_spacing; + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && font_run.letter_spacing == letter_spacing + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + letter_spacing, + }); + } + } - let layout = self.line_layout_cache.try_layout_line_by_hash( - text_hash, - text_len, - font_size, - &font_runs, - force_width, - ); + 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); + self.font_runs_pool.lock().push(font_runs); - layout - } + 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(); + /// 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 - }; + 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); - let letter_spacing = run.letter_spacing; - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && font_run.letter_spacing == letter_spacing - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - letter_spacing, - }); - } - } + let font_id = self.resolve_font(&run.font); + let letter_spacing = run.letter_spacing; + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && font_run.letter_spacing == letter_spacing + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + letter_spacing, + }); + } + } - let layout = self.line_layout_cache.layout_line_by_hash( - text_hash, - text_len, - font_size, - &font_runs, - force_width, - materialize_text, - ); + 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); + self.font_runs_pool.lock().push(font_runs); - layout - } + layout + } } #[derive(Hash, Eq, PartialEq)] struct FontIdWithSize { - font_id: FontId, - font_size: Pixels, + 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, + wrapper: Option, + text_system: Arc, } impl Drop for LineWrapperHandle { - fn drop(&mut self) { - let mut state = self.text_system.wrapper_pool.lock(); - let mut wrapper = self.wrapper.take().unwrap(); - wrapper.set_letter_spacing(None); - state - .get_mut(&FontIdWithSize { - font_id: wrapper.font_id, - font_size: wrapper.font_size, - }) - .unwrap() - .push(wrapper); - } + fn drop(&mut self) { + let mut state = self.text_system.wrapper_pool.lock(); + let mut wrapper = self.wrapper.take().unwrap(); + wrapper.set_letter_spacing(None); + state + .get_mut(&FontIdWithSize { + font_id: wrapper.font_id, + font_size: wrapper.font_size, + }) + .unwrap() + .push(wrapper); + } } impl Deref for LineWrapperHandle { - type Target = LineWrapper; + type Target = LineWrapper; - fn deref(&self) -> &Self::Target { - self.wrapper.as_ref().unwrap() - } + 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() - } + 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, @@ -919,127 +919,127 @@ impl DerefMut for LineWrapperHandle { pub struct FontWeight(pub f32); impl Display for FontWeight { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } + 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) - } + fn from(weight: f32) -> Self { + FontWeight(weight) + } } impl Default for FontWeight { - #[inline] - fn default() -> FontWeight { - FontWeight::NORMAL - } + #[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())); - } + 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); + /// 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, - ]; + /// 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 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)" - }) - } + 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, + /// 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) - } + 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, - /// Letter spacing applied between glyphs, in pixels. - pub letter_spacing: Option, + /// 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, + /// Letter spacing applied between glyphs, in pixels. + pub letter_spacing: 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 - } + 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`]. @@ -1055,186 +1055,186 @@ pub struct GlyphId(pub u32); #[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, + 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); - } + 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 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 font features to use. + pub features: FontFeatures, - /// The fallbacks fonts to use. - pub fallbacks: Option, + /// The fallbacks fonts to use. + pub fallbacks: Option, - /// The font weight. - pub weight: FontWeight, + /// The font weight. + pub weight: FontWeight, - /// The font style. - pub style: FontStyle, + /// The font style. + pub style: FontStyle, } impl Default for Font { - fn default() -> Self { - font(".SystemUIFont") - } + 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, - } + 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 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 - } + /// 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 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 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 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 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 position of the underline. + pub underline_position: f32, - /// The suggested thickness of the underline. - pub underline_thickness: 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 capital letter measured from the baseline of the font. + pub cap_height: f32, - /// The height of a lowercase x. - pub x_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, + /// 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 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 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 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 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 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 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 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) - } + /// 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, - } + // 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, + 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, - } + // 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/src/text_system/font_fallbacks.rs b/crates/gpui/src/text_system/font_fallbacks.rs index 63dc89ba41..cf26c277be 100644 --- a/crates/gpui/src/text_system/font_fallbacks.rs +++ b/crates/gpui/src/text_system/font_fallbacks.rs @@ -9,13 +9,13 @@ use serde::{Deserialize, Serialize}; pub struct FontFallbacks(pub Arc>); impl FontFallbacks { - /// Get the fallback fonts family names - pub fn fallback_list(&self) -> &[String] { - self.0.as_slice() - } + /// 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)) - } + /// 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/src/text_system/font_features.rs b/crates/gpui/src/text_system/font_features.rs index c1ab72b417..8be7a2c167 100644 --- a/crates/gpui/src/text_system/font_features.rs +++ b/crates/gpui/src/text_system/font_features.rs @@ -8,147 +8,147 @@ use schemars::{JsonSchema, json_schema}; pub struct FontFeatures(pub Arc>); impl FontFeatures { - /// Disables `calt`. - pub fn disable_ligatures() -> Self { - Self(Arc::new(vec![("calt".into(), 0)])) - } + /// 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() - } + /// 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) - } + /// 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); - } + 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() - } + debug.finish() + } } #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(untagged)] enum FeatureValue { - Bool(bool), - Number(serde_json::Number), + 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; + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + use std::fmt; - struct FontFeaturesVisitor; + struct FontFeaturesVisitor; - impl<'de> Visitor<'de> for FontFeaturesVisitor { - type Value = FontFeatures; + 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 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(); + 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; - } - } - } - } - } + 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))) - } - } + Ok(FontFeatures(Arc::new(feature_list))) + } + } - let features = deserializer.deserialize_map(FontFeaturesVisitor)?; - Ok(features) - } + 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; + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; - let mut map = serializer.serialize_map(None)?; + let mut map = serializer.serialize_map(None)?; - for (tag, value) in self.tag_value_list() { - map.serialize_entry(tag, value)?; - } + for (tag, value) in self.tag_value_list() { + map.serialize_entry(tag, value)?; + } - map.end() - } + map.end() + } } impl JsonSchema for FontFeatures { - fn schema_name() -> Cow<'static, str> { - "FontFeatures".into() - } + 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 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()) + tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric()) } diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index cb66359206..2237fdd8e5 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -1,7 +1,7 @@ use crate::{ - App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result, - ShapedGlyph, ShapedRun, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, - WrapBoundary, WrappedLineLayout, black, fill, point, px, size, + App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result, + ShapedGlyph, ShapedRun, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, + WrapBoundary, WrappedLineLayout, black, fill, point, px, size, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -13,1003 +13,1003 @@ use std::sync::Arc; /// 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, + /// 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 length of the run in utf-8 bytes. + pub len: u32, - /// The color for this run - pub color: Hsla, + /// The color for this run + pub color: Hsla, - /// The background color for this run - pub background_color: Option, + /// The background color for this run + pub background_color: Option, - /// The underline style for this run - pub underline: Option, + /// The underline style for this run + pub underline: Option, - /// The strikethrough style for this run - pub strikethrough: 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]>, + #[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 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 - } + /// 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 - } + /// 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, - )?; + /// 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(()) - } + 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, - )?; + /// 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(()) - } + 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 x_offset = self.layout.x_for_index(byte_index); + /// 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 x_offset = self.layout.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(); + // 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.layout.runs { - let split_pos = run.glyphs.partition_point(|g| g.index < byte_index); + for run in &self.layout.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 > 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, - }); - } - } + 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, + }); + } + } - // 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; + // 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; + 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, - }); - } + 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; - } + 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..]) - }; + // 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_width = x_offset; - let right_width = self.layout.width - left_width; + let left_width = x_offset; + let right_width = self.layout.width - left_width; - let left = ShapedLine { - layout: Arc::new(LineLayout { - font_size: self.layout.font_size, - width: left_width, - ascent: self.layout.ascent, - descent: self.layout.descent, - runs: left_runs, - len: byte_index, - }), - text: left_text, - decoration_runs: left_decorations, - }; + let left = ShapedLine { + layout: Arc::new(LineLayout { + font_size: self.layout.font_size, + width: left_width, + ascent: self.layout.ascent, + descent: self.layout.descent, + runs: left_runs, + len: byte_index, + }), + text: left_text, + decoration_runs: left_decorations, + }; - let right = ShapedLine { - layout: Arc::new(LineLayout { - font_size: self.layout.font_size, - width: right_width, - ascent: self.layout.ascent, - descent: self.layout.descent, - runs: right_runs, - len: self.layout.len - byte_index, - }), - text: right_text, - decoration_runs: right_decorations, - }; + let right = ShapedLine { + layout: Arc::new(LineLayout { + font_size: self.layout.font_size, + width: right_width, + ascent: self.layout.ascent, + descent: self.layout.descent, + runs: right_runs, + len: self.layout.len - byte_index, + }), + text: right_text, + decoration_runs: right_decorations, + }; - (left, right) - } + (left, right) + } } /// 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, + #[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() - } + /// 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 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, - )?; + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + )?; - Ok(()) - } + 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 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, - )?; + paint_line_background( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + )?; - Ok(()) - } + 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, + 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; + 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; - } + 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; - } - } + 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; + 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(); + 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(); - } + // 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, - }, - )); - } + 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(); - } - } + 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 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, - ); - } + 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 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 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; - } + 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 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, - ); - } + 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(()) - }) + 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, + 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; + 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; + 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; - } - } + 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; + 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(); + 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(); - } + // 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(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, - )); - } - } - } + 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; - } + 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, - )); - } + 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(()) - }) + Ok(()) + }) } fn aligned_origin_x( - origin: Point, - align_width: Pixels, - last_glyph_x: Pixels, - align: &TextAlign, - layout: &LineLayout, - wrap_boundary: Option<&&WrapBoundary>, + 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 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; + 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, - } + 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}; + use super::*; + use crate::{FontId, GlyphId}; - /// 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(); + /// 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()), - } - } + 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, - &[], - ); + #[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); + 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}"); - } + 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 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); - } + // 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(), - }; + #[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)); + // 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)); + // 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)); + // 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() - ); - } + // 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, - }; + #[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 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); + 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); + // 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); - } + // 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/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 83765038ee..54d87bde5c 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -3,10 +3,10 @@ use collections::FxHashMap; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; use std::{ - borrow::Borrow, - hash::{Hash, Hasher}, - ops::Range, - sync::Arc, + borrow::Borrow, + hash::{Hash, Hasher}, + ops::Range, + sync::Arc, }; use super::LineWrapper; @@ -14,769 +14,769 @@ 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, + /// 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, + /// 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 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 position of this glyph in its containing line. + pub position: Point, - /// The index of this glyph in the original text. - pub index: usize, + /// The index of this glyph in the original text. + pub index: usize, - /// Whether this glyph is an emoji - pub is_emoji: bool, + /// 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) - } - } + /// 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.); + /// 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; - } - } + 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; - } - } + if self.len == 1 { + if x > self.width / 2. { + return 1; + } else { + return 0; + } + } - self.len - } + 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 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 - } + /// 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 + } - 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(); + 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; - } + 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; - } - } + // 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); - } + 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; + 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 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; - } + 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 - } + 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 line layout, pre-wrapping. + pub unwrapped_layout: Arc, - /// The boundaries at which the line was wrapped - pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + /// 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, + /// 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, + /// 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 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 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 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 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 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 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 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 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 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) - } + /// 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; + 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_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 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()) - } - } - } + 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)); - } - } + /// 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 - } + None + } } pub(crate) struct LineLayoutCache { - previous_frame: Mutex, - current_frame: RwLock, - platform_text_system: Arc, + 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>, + 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>, + // 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, + 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 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 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(); + 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_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_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_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()); - } - } + 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 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(); + 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(); - } + 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; + 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 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 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); + 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 - } - } + 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; + 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 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); + 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); - } + 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 - } - } + 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, - }; + /// 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 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()); - } + 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 - } + 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, - }; + /// 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(); - } + // 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); + 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; - } - } + // 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); + 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); - } + 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 - } + 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 @@ -785,312 +785,312 @@ impl LineLayoutCache { // 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.); + 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; + 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); - } - } - } + 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)] #[expect(missing_docs)] pub struct FontRun { - pub len: usize, - pub font_id: FontId, - pub letter_spacing: Option, + pub len: usize, + pub font_id: FontId, + pub letter_spacing: Option, } impl Hash for FontRun { - fn hash(&self, state: &mut H) { - self.len.hash(state); - self.font_id.hash(state); - self.letter_spacing - .map(Pixels::as_f32) - .map(|value| { - if value == 0.0 { - 0.0f32.to_bits() - } else { - value.to_bits() - } - }) - .hash(state); - } + fn hash(&self, state: &mut H) { + self.len.hash(state); + self.font_id.hash(state); + self.letter_spacing + .map(Pixels::as_f32) + .map(|value| { + if value == 0.0 { + 0.0f32.to_bits() + } else { + value.to_bits() + } + }) + .hash(state); + } } trait AsCacheKeyRef { - fn as_cache_key_ref(&self) -> CacheKeyRef<'_>; + 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, + 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, + 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, + 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, + 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() - } + 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 - } + 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); - } + 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 - } + 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); - } + 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) - } + 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, - } - } + 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()) - } + 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); - } + 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 - } + fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { + self.as_ref() as &dyn AsCacheKeyRef + } } impl AsCacheKeyRef for CacheKeyRef<'_> { - fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { - *self - } + fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { + *self + } } #[cfg(test)] mod tests { - use super::*; - use crate::GlyphId; + 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 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 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() - } + 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)]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 8., 16.]); - } + 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) - ]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0.]); - } + 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)]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0., 8.]); - } + 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 - ]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0., 0., 8.]); - } + 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 - ]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0.5, 8., 16.]); - } + 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)]); + #[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); + apply_force_width_to_layout(&mut layout, cell_width); - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0.5, 0.5]); - } + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0.5, 0.5]); + } } diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 1eec3cff1a..b83c0bbcce 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -5,398 +5,398 @@ 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 start. + Start, + /// Truncate text from the end. + End, } /// 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, - letter_spacing: Option, - cached_ascii_char_widths: [Option; 128], - cached_other_char_widths: HashMap, + text_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: Pixels, + letter_spacing: Option, + 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; + /// 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, - letter_spacing: None, - cached_ascii_char_widths: [None; 128], - cached_other_char_widths: HashMap::default(), - } - } + pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc) -> Self { + Self { + text_system, + font_id, + font_size, + letter_spacing: None, + cached_ascii_char_widths: [None; 128], + cached_other_char_widths: HashMap::default(), + } + } - pub(crate) fn set_letter_spacing(&mut self, letter_spacing: Option) { - self.letter_spacing = letter_spacing; - } + pub(crate) fn set_letter_spacing(&mut self, letter_spacing: Option) { + self.letter_spacing = letter_spacing; + } - /// 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 previous_text_character = false; - 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 mut item_had_spacing = false; - let item_width = match candidate { - WrapBoundaryCandidate::Char { character: c } => { - if c == '\n' { - previous_text_character = false; - continue; - } + /// 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 previous_text_character = false; + 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 mut item_had_spacing = false; + let item_width = match candidate { + WrapBoundaryCandidate::Char { character: c } => { + if c == '\n' { + previous_text_character = false; + 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 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); - } + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } - new_prev_c = c; + new_prev_c = c; - let width = self.width_for_char(c); - let spacing = if previous_text_character { - self.letter_spacing.unwrap_or_default() - } else { - px(0.) - }; - item_had_spacing = previous_text_character; - previous_text_character = true; - width + spacing - } - WrapBoundaryCandidate::Element { - width: element_width, - .. - } => { - if prev_c == ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } + let width = self.width_for_char(c); + let spacing = if previous_text_character { + self.letter_spacing.unwrap_or_default() + } else { + px(0.) + }; + item_had_spacing = previous_text_character; + previous_text_character = true; + width + spacing + } + 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); - } + if first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } - element_width - } - }; + 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), - ); - } + 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; - width -= self.letter_spacing.unwrap_or_default(); - last_candidate_ix = 0; - } else { - last_wrap_ix = ix; - width = if item_had_spacing { - item_width - self.letter_spacing.unwrap_or_default() - } else { - item_width - }; - } + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + width -= self.letter_spacing.unwrap_or_default(); + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = if item_had_spacing { + item_width - self.letter_spacing.unwrap_or_default() + } else { + item_width + }; + } - if let Some(indent) = indent { - width += self.width_for_char(' ') * indent as f32; - } + if let Some(indent) = indent { + width += self.width_for_char(' ') * indent as f32; + } - return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); - } + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } - prev_c = new_prev_c; - } + prev_c = new_prev_c; + } - None - }) - } + 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 = self.width_for_text(truncation_affix) - + if truncation_affix.is_empty() { - px(0.) - } else { - self.letter_spacing.unwrap_or_default() - }; - let mut truncate_ix = 0; + /// 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 = self.width_for_text(truncation_affix) + + if truncation_affix.is_empty() { + px(0.) + } else { + self.letter_spacing.unwrap_or_default() + }; + let mut truncate_ix = 0; - match truncate_from { - TruncateFrom::Start => { - let mut previous_text_character = false; - for (ix, c) in line.char_indices().rev() { - if width + suffix_width < truncate_width { - truncate_ix = ix; - } + match truncate_from { + TruncateFrom::Start => { + let mut previous_text_character = false; + for (ix, c) in line.char_indices().rev() { + if width + suffix_width < truncate_width { + truncate_ix = ix; + } - let char_width = - self.width_for_char_with_spacing(c, &mut previous_text_character); - width += char_width; + let char_width = + self.width_for_char_with_spacing(c, &mut previous_text_character); + width += char_width; - if width.floor() > truncate_width { - return Some(truncate_ix); - } - } - } - TruncateFrom::End => { - let mut previous_text_character = false; - for (ix, c) in line.char_indices() { - if width + suffix_width < truncate_width { - truncate_ix = ix; - } + if width.floor() > truncate_width { + return Some(truncate_ix); + } + } + } + TruncateFrom::End => { + let mut previous_text_character = false; + for (ix, c) in line.char_indices() { + if width + suffix_width < truncate_width { + truncate_ix = ix; + } - let char_width = - self.width_for_char_with_spacing(c, &mut previous_text_character); - width += char_width; + let char_width = + self.width_for_char_with_spacing(c, &mut previous_text_character); + width += char_width; - if width.floor() > truncate_width { - return Some(truncate_ix); - } - } - } - } + if width.floor() > truncate_width { + return Some(truncate_ix); + } + } + } + } - None - } + None + } - /// 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 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()) - )), - }; - 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 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 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()) + )), + }; + 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, - ); - } + /// 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, + ); + } - let affix_width = self.width_for_text(truncation_affix) - + if truncation_affix.is_empty() { - px(0.) - } else { - self.letter_spacing.unwrap_or_default() - }; + let affix_width = self.width_for_text(truncation_affix) + + if truncation_affix.is_empty() { + px(0.) + } else { + self.letter_spacing.unwrap_or_default() + }; - 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; - let mut previous_text_character = false; + 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; + let mut previous_text_character = false; - 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)); - } + 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; - previous_text_character = false; - continue; - } + // 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; + previous_text_character = false; + continue; + } - let char_width = self.width_for_char_with_spacing(c, &mut previous_text_character); + let char_width = self.width_for_char_with_spacing(c, &mut previous_text_character); - 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 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); - } + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } - width += char_width; + 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 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; - width -= self.letter_spacing.unwrap_or_default(); - last_candidate_ix = 0; - } else { - last_wrap_ix = ix; - width = self.width_for_char(c); - } + if last_candidate_ix > last_wrap_ix { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + width -= self.letter_spacing.unwrap_or_default(); + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = self.width_for_char(c); + } - if let Some(ind) = indent { - width += self.width_for_char(' ') * ind as f32; - } + 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(); - } + 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)); - } - } + 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; - } + prev_c = c; + } - // Text fits within max_lines without truncation. - (text, Cow::Borrowed(runs)) - } + // 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() || + /// 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 @@ -425,1025 +425,1025 @@ impl LineWrapper { matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') || // `⋯` character is special used in Zed, to keep this at the end of the line. matches!(c, '⋯') - } + } - #[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 - } - } + #[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 width_for_char_with_spacing( - &mut self, - c: char, - previous_text_character: &mut bool, - ) -> Pixels { - let width = self.width_for_char(c); - let spacing = if *previous_text_character { - self.letter_spacing.unwrap_or_default() - } else { - px(0.) - }; - *previous_text_character = true; - width + spacing - } + fn width_for_char_with_spacing( + &mut self, + c: char, + previous_text_character: &mut bool, + ) -> Pixels { + let width = self.width_for_char(c); + let spacing = if *previous_text_character { + self.letter_spacing.unwrap_or_default() + } else { + px(0.) + }; + *previous_text_character = true; + width + spacing + } - fn width_for_text(&mut self, text: &str) -> Pixels { - let mut previous_text_character = false; - text.chars() - .map(|character| { - self.width_for_char_with_spacing(character, &mut previous_text_character) - }) - .sum() - } + fn width_for_text(&mut self, text: &str) -> Pixels { + let mut previous_text_character = false; + text.chars() + .map(|character| { + self.width_for_char_with_spacing(character, &mut previous_text_character) + }) + .sum() + } } fn update_runs_after_truncation( - result: &str, - ellipsis: &str, - runs: &mut Vec, - truncate_from: TruncateFrom, + 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; - } - } - } - } + 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; + } + } + } + } } /// 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, - }, + /// 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 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 } - } + /// 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 } - } - }) - } + 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 }, + 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, - } - } + 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, + /// 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 } - } + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } } #[cfg(test)] mod tests { - use super::*; - use crate::{ - Font, FontFeatures, FontRun, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, - TextRun, font, - }; - #[cfg(target_os = "macos")] - use crate::{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_tracking_changes_measured_width() { - let dispatcher = TestDispatcher::new(1); - let cx = TestAppContext::build(dispatcher, None); - let base = TextRun { - len: 4, - font: font(".ZedMono"), - color: Hsla::default(), - background_color: None, - underline: None, - strikethrough: None, - ..Default::default() - }; - let font_id = cx.text_system().resolve_font(&base.font); - let platform = cx.text_system().platform_text_system_for_tests(); - let no_tracking = platform.layout_line( - "TEST", - px(16.), - &[FontRun { - len: 4, - font_id, - letter_spacing: None, - }], - ); - let wide = platform.layout_line( - "TEST", - px(16.), - &[FontRun { - len: 4, - font_id, - letter_spacing: Some(px(2.0)), - }], - ); - let tight = platform.layout_line( - "TEST", - px(16.), - &[FontRun { - len: 4, - font_id, - letter_spacing: Some(px(-0.5)), - }], - ); - - assert!(wide.width > no_tracking.width); - assert!(tight.width <= no_tracking.width); - } - - #[test] - fn test_tracking_changes_truncation_width() { - let mut wrapper = build_wrapper(); - let no_tracking = wrapper.width_for_text("TEST…"); - wrapper.set_letter_spacing(Some(px(2.0))); - let wide = wrapper.width_for_text("TEST…"); - - assert!(wide > no_tracking); - } - - #[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] - 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"); - - // 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("()[]{}<>"); - } - - // 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 = SharedString::from("aa bbb cccc ddddd eeee"); - 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_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 - ); - } + use super::*; + use crate::{ + Font, FontFeatures, FontRun, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, + TextRun, font, + }; + #[cfg(target_os = "macos")] + use crate::{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_tracking_changes_measured_width() { + let dispatcher = TestDispatcher::new(1); + let cx = TestAppContext::build(dispatcher, None); + let base = TextRun { + len: 4, + font: font(".ZedMono"), + color: Hsla::default(), + background_color: None, + underline: None, + strikethrough: None, + ..Default::default() + }; + let font_id = cx.text_system().resolve_font(&base.font); + let platform = cx.text_system().platform_text_system_for_tests(); + let no_tracking = platform.layout_line( + "TEST", + px(16.), + &[FontRun { + len: 4, + font_id, + letter_spacing: None, + }], + ); + let wide = platform.layout_line( + "TEST", + px(16.), + &[FontRun { + len: 4, + font_id, + letter_spacing: Some(px(2.0)), + }], + ); + let tight = platform.layout_line( + "TEST", + px(16.), + &[FontRun { + len: 4, + font_id, + letter_spacing: Some(px(-0.5)), + }], + ); + + assert!(wide.width > no_tracking.width); + assert!(tight.width <= no_tracking.width); + } + + #[test] + fn test_tracking_changes_truncation_width() { + let mut wrapper = build_wrapper(); + let no_tracking = wrapper.width_for_text("TEST…"); + wrapper.set_letter_spacing(Some(px(2.0))); + let wide = wrapper.width_for_text("TEST…"); + + assert!(wide > no_tracking); + } + + #[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] + 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"); + + // 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("()[]{}<>"); + } + + // 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 = SharedString::from("aa bbb cccc ddddd eeee"); + 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_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/src/timeline/clip.rs b/crates/gpui/src/timeline/clip.rs index ee52238d92..94930254c1 100644 --- a/crates/gpui/src/timeline/clip.rs +++ b/crates/gpui/src/timeline/clip.rs @@ -19,13 +19,13 @@ use std::sync::Arc; use crate::{ - App, BorderStyle, Bounds, Hsla, PathBuilder, Pixels, SharedString, Window, canvas, div, fill, - hsla, outline, point, prelude::*, px, + App, BorderStyle, Bounds, Hsla, PathBuilder, Pixels, SharedString, Window, canvas, div, fill, + hsla, outline, point, prelude::*, px, }; use super::{ - data::ClipId, - time::{Frame, FrameRange}, + data::ClipId, + time::{Frame, FrameRange}, }; /// Screen-space width of each trim-handle hit zone, in pixels. @@ -44,13 +44,13 @@ const PX_PER_FRAME_FALLBACK: f32 = 1.0; /// What the clip content area should show, per track kind and zoom. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClipContent { - /// Just the body color and label (also the fallback when zoomed out). - #[default] - None, - /// Video thumbnails — painted via [`ClipDecorator::paint_thumbnail`]. - Thumbnails, - /// Audio waveform — painted via [`ClipDecorator::paint_waveform`]. - Waveform, + /// Just the body color and label (also the fallback when zoomed out). + #[default] + None, + /// Video thumbnails — painted via [`ClipDecorator::paint_thumbnail`]. + Thumbnails, + /// Audio waveform — painted via [`ClipDecorator::paint_waveform`]. + Waveform, } /// Paints rich clip content (thumbnails, waveforms) into the clip body. @@ -66,37 +66,37 @@ pub enum ClipContent { /// /// The trait is object-safe and used behind `Arc`. pub trait ClipDecorator: 'static { - /// Paints a video thumbnail filmstrip for `clip` covering - /// `visible_range` (the part of the clip currently on screen) into - /// `bounds`. - /// - /// `media_in` semantics matter here: the strip starts at - /// [`ClipData::media_in`](super::ClipData::media_in), so frame - /// `visible_range.start` of the *timeline* maps to source frame - /// `media_in + (visible_range.start - clip.range().start)`. - fn paint_thumbnail( - &mut self, - _window: &mut Window, - _clip: ClipId, - _visible_range: FrameRange, - _bounds: Bounds, - ) { - // no-op by default - } + /// Paints a video thumbnail filmstrip for `clip` covering + /// `visible_range` (the part of the clip currently on screen) into + /// `bounds`. + /// + /// `media_in` semantics matter here: the strip starts at + /// [`ClipData::media_in`](super::ClipData::media_in), so frame + /// `visible_range.start` of the *timeline* maps to source frame + /// `media_in + (visible_range.start - clip.range().start)`. + fn paint_thumbnail( + &mut self, + _window: &mut Window, + _clip: ClipId, + _visible_range: FrameRange, + _bounds: Bounds, + ) { + // no-op by default + } - /// Paints an audio waveform for `clip` covering `visible_range` into - /// `bounds`. Same media-time mapping as - /// [`ClipDecorator::paint_thumbnail`]. Typically drawn with - /// [`PathBuilder`](crate::PathBuilder) from cached peak data. - fn paint_waveform( - &mut self, - _window: &mut Window, - _clip: ClipId, - _visible_range: FrameRange, - _bounds: Bounds, - ) { - // no-op by default - } + /// Paints an audio waveform for `clip` covering `visible_range` into + /// `bounds`. Same media-time mapping as + /// [`ClipDecorator::paint_thumbnail`]. Typically drawn with + /// [`PathBuilder`](crate::PathBuilder) from cached peak data. + fn paint_waveform( + &mut self, + _window: &mut Window, + _clip: ClipId, + _visible_range: FrameRange, + _bounds: Bounds, + ) { + // no-op by default + } } /// The default decorator: paints no thumbnails and no waveforms. @@ -120,228 +120,238 @@ impl ClipDecorator for NoopClipDecorator {} /// constant for the limitation. #[derive(IntoElement)] pub struct ClipElement { - id: ClipId, - label: SharedString, - color: Hsla, - selected: bool, - enabled: bool, - locked: bool, - in_transition: Option, - out_transition: Option, - content: ClipContent, - decorator: Arc>, + id: ClipId, + label: SharedString, + color: Hsla, + selected: bool, + enabled: bool, + locked: bool, + in_transition: Option, + out_transition: Option, + content: ClipContent, + decorator: Arc>, } impl ClipElement { - /// Creates a clip element. - /// - /// * `id` / `label` / `color` — from the clip's - /// [`ClipData`](super::ClipData); `color` is the clip color or the - /// track-kind default resolved by the caller. - /// * `in_transition` / `out_transition` — the frame ranges (in - /// *clip-local* time) of the head/tail transition wedges, if any. - /// * `decorator` — shared decorator instance; see [`ClipDecorator`]. - #[allow(clippy::too_many_arguments)] - pub fn new( - id: ClipId, - label: SharedString, - color: Hsla, - in_transition: Option, - out_transition: Option, - decorator: Arc>, - ) -> Self { - ClipElement { - id, - label, - color, - selected: false, - enabled: true, - locked: false, - in_transition, - out_transition, - content: ClipContent::None, - decorator, - } - } + /// Creates a clip element. + /// + /// * `id` / `label` / `color` — from the clip's + /// [`ClipData`](super::ClipData); `color` is the clip color or the + /// track-kind default resolved by the caller. + /// * `in_transition` / `out_transition` — the frame ranges (in + /// *clip-local* time) of the head/tail transition wedges, if any. + /// * `decorator` — shared decorator instance; see [`ClipDecorator`]. + #[allow(clippy::too_many_arguments)] + pub fn new( + id: ClipId, + label: SharedString, + color: Hsla, + in_transition: Option, + out_transition: Option, + decorator: Arc>, + ) -> Self { + ClipElement { + id, + label, + color, + selected: false, + enabled: true, + locked: false, + in_transition, + out_transition, + content: ClipContent::None, + decorator, + } + } - /// Builder: render in the selected state (selection outline). - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } + /// Builder: render in the selected state (selection outline). + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } - /// Builder: render in the disabled state (dimmed, no snapping target). - pub fn enabled(mut self, enabled: bool) -> Self { - self.enabled = enabled; - self - } + /// Builder: render in the disabled state (dimmed, no snapping target). + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } - /// Builder: render as belonging to a locked track (no trim handles, no - /// drag cursor). - pub fn locked(mut self, locked: bool) -> Self { - self.locked = locked; - self - } + /// Builder: render as belonging to a locked track (no trim handles, no + /// drag cursor). + pub fn locked(mut self, locked: bool) -> Self { + self.locked = locked; + self + } - /// Builder: what content the decorator should paint inside the body. - pub fn content(mut self, content: ClipContent) -> Self { - self.content = content; - self - } + /// Builder: what content the decorator should paint inside the body. + pub fn content(mut self, content: ClipContent) -> Self { + self.content = content; + self + } - /// The clip this element renders. - pub fn clip_id(&self) -> ClipId { - self.id - } + /// The clip this element renders. + pub fn clip_id(&self) -> ClipId { + self.id + } - /// The trim-handle hit zone width, in pixels. - pub fn trim_handle_width(&self) -> Pixels { - crate::px(TRIM_HANDLE_WIDTH) - } + /// The trim-handle hit zone width, in pixels. + pub fn trim_handle_width(&self) -> Pixels { + crate::px(TRIM_HANDLE_WIDTH) + } - // TODO(implementor): register the trim-handle hit zones when the element - // grows interactive handles; they are currently part of the view's - // interaction layer. + // TODO(implementor): register the trim-handle hit zones when the element + // grows interactive handles; they are currently part of the view's + // interaction layer. } /// Paint-time snapshot of a [`ClipElement`], captured by the canvas prepaint /// callback and consumed by the paint callback. struct ClipPaint { - id: ClipId, - color: Hsla, - selected: bool, - enabled: bool, - locked: bool, - in_transition: Option, - out_transition: Option, - content: ClipContent, - decorator: Arc>, + id: ClipId, + color: Hsla, + selected: bool, + enabled: bool, + locked: bool, + in_transition: Option, + out_transition: Option, + content: ClipContent, + decorator: Arc>, } impl RenderOnce for ClipElement { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let ClipElement { - id, - label, - color, - selected, - enabled, - locked, - in_transition, - out_transition, - content, - decorator, - .. - } = self; + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let ClipElement { + id, + label, + color, + selected, + enabled, + locked, + in_transition, + out_transition, + content, + decorator, + .. + } = self; - // The clip body is custom-painted (body quad, transition wedges, - // state overlays, decorator content); the label rides in a styled - // overlay div so it inherits the usual text elision. - div() - .relative() - .size_full() - .child(canvas( - move |_bounds, _window, _cx| ClipPaint { - id, - color, - selected, - enabled, - locked, - in_transition, - out_transition, - content, - decorator, - }, - |bounds, paint, window, _cx| { - // Body quad, dimmed when the clip is disabled. - let body_color = if paint.enabled { - paint.color - } else { - Hsla { - h: paint.color.h, - s: paint.color.s, - l: paint.color.l, - a: paint.color.a * 0.5, - } - }; - window.paint_quad(fill(bounds, body_color)); + // The clip body is custom-painted (body quad, transition wedges, + // state overlays, decorator content); the label rides in a styled + // overlay div so it inherits the usual text elision. + div() + .relative() + .size_full() + .child( + canvas( + move |_bounds, _window, _cx| ClipPaint { + id, + color, + selected, + enabled, + locked, + in_transition, + out_transition, + content, + decorator, + }, + |bounds, paint, window, _cx| { + // Body quad, dimmed when the clip is disabled. + let body_color = if paint.enabled { + paint.color + } else { + Hsla { + h: paint.color.h, + s: paint.color.s, + l: paint.color.l, + a: paint.color.a * 0.5, + } + }; + window.paint_quad(fill(bounds, body_color)); - // Transition wedges: triangles tapering into the clip - // from each edge, capped at 40% of the body width so tiny - // clips don't vanish. - let max_wedge = bounds.size.width.0 * 0.4; - if let Some(range) = paint.in_transition { - let w = (range.len().0 as f32 * PX_PER_FRAME_FALLBACK).min(max_wedge); - if w > 0.0 { - let mut path = PathBuilder::fill(); - path.move_to(point(bounds.left(), bounds.top())); - path.line_to(point(bounds.left() + px(w), bounds.top())); - path.line_to(point(bounds.left(), bounds.bottom())); - path.close(); - window.paint_path(path.build().expect("wedge path is valid"), paint.color); - } - } - if let Some(range) = paint.out_transition { - let w = (range.len().0 as f32 * PX_PER_FRAME_FALLBACK).min(max_wedge); - if w > 0.0 { - let mut path = PathBuilder::fill(); - path.move_to(point(bounds.right(), bounds.top())); - path.line_to(point(bounds.right() - px(w), bounds.top())); - path.line_to(point(bounds.right(), bounds.bottom())); - path.close(); - window.paint_path(path.build().expect("wedge path is valid"), paint.color); - } - } + // Transition wedges: triangles tapering into the clip + // from each edge, capped at 40% of the body width so tiny + // clips don't vanish. + let max_wedge = bounds.size.width.0 * 0.4; + if let Some(range) = paint.in_transition { + let w = (range.len().0 as f32 * PX_PER_FRAME_FALLBACK).min(max_wedge); + if w > 0.0 { + let mut path = PathBuilder::fill(); + path.move_to(point(bounds.left(), bounds.top())); + path.line_to(point(bounds.left() + px(w), bounds.top())); + path.line_to(point(bounds.left(), bounds.bottom())); + path.close(); + window.paint_path( + path.build().expect("wedge path is valid"), + paint.color, + ); + } + } + if let Some(range) = paint.out_transition { + let w = (range.len().0 as f32 * PX_PER_FRAME_FALLBACK).min(max_wedge); + if w > 0.0 { + let mut path = PathBuilder::fill(); + path.move_to(point(bounds.right(), bounds.top())); + path.line_to(point(bounds.right() - px(w), bounds.top())); + path.line_to(point(bounds.right(), bounds.bottom())); + path.close(); + window.paint_path( + path.build().expect("wedge path is valid"), + paint.color, + ); + } + } - // State overlays, in back-to-front order. - if !paint.enabled { - window.paint_quad(fill(bounds, hsla(0., 0., 0.05, 0.55))); - } - if paint.locked { - window.paint_quad(fill(bounds, hsla(0., 0., 0.1, 0.25))); - } + // State overlays, in back-to-front order. + if !paint.enabled { + window.paint_quad(fill(bounds, hsla(0., 0., 0.05, 0.55))); + } + if paint.locked { + window.paint_quad(fill(bounds, hsla(0., 0., 0.1, 0.25))); + } - // Rich content via the decorator, scoped to the frames - // visible inside this body. - let visible_range = FrameRange::new( - Frame::ZERO, - Frame((bounds.size.width.0 / PX_PER_FRAME_FALLBACK) as i64), - ); - match paint.content { - ClipContent::Thumbnails => paint - .decorator - .write() - .expect("clip decorator lock is not poisoned") - .paint_thumbnail(window, paint.id, visible_range, bounds), - ClipContent::Waveform => paint - .decorator - .write() - .expect("clip decorator lock is not poisoned") - .paint_waveform(window, paint.id, visible_range, bounds), - ClipContent::None => {} - } + // Rich content via the decorator, scoped to the frames + // visible inside this body. + let visible_range = FrameRange::new( + Frame::ZERO, + Frame((bounds.size.width.0 / PX_PER_FRAME_FALLBACK) as i64), + ); + match paint.content { + ClipContent::Thumbnails => paint + .decorator + .write() + .expect("clip decorator lock is not poisoned") + .paint_thumbnail(window, paint.id, visible_range, bounds), + ClipContent::Waveform => paint + .decorator + .write() + .expect("clip decorator lock is not poisoned") + .paint_waveform(window, paint.id, visible_range, bounds), + ClipContent::None => {} + } - // Selection outline on top of everything. - if paint.selected { - window.paint_quad( - outline(bounds, hsla(0.6, 0.8, 0.6, 1.), BorderStyle::Solid), - ); - } - }, - ) - .size_full()) - .child( - div() - .absolute() - .left(px(4.)) - .top(px(4.)) - .right(px(4.)) - .overflow_hidden() - .whitespace_nowrap() - .text_ellipsis() - .text_size(px(11.)) - .text_color(hsla(0., 0., 1., 0.92)) - .child(label), - ) - } + // Selection outline on top of everything. + if paint.selected { + window.paint_quad(outline( + bounds, + hsla(0.6, 0.8, 0.6, 1.), + BorderStyle::Solid, + )); + } + }, + ) + .size_full(), + ) + .child( + div() + .absolute() + .left(px(4.)) + .top(px(4.)) + .right(px(4.)) + .overflow_hidden() + .whitespace_nowrap() + .text_ellipsis() + .text_size(px(11.)) + .text_color(hsla(0., 0., 1., 0.92)) + .child(label), + ) + } } diff --git a/crates/gpui/src/timeline/data.rs b/crates/gpui/src/timeline/data.rs index d32ffa00a4..313bca5ca6 100644 --- a/crates/gpui/src/timeline/data.rs +++ b/crates/gpui/src/timeline/data.rs @@ -57,14 +57,14 @@ pub struct ClipId(pub u64); /// which clip decorations (waveform vs. thumbnails) are offered. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum TrackKind { - /// Video track. Stacks visually above/below siblings by compositing - /// order; upper tracks occlude lower ones. - #[default] - Video, - /// Audio track. Contributes to the mix; subject to mute/solo. - Audio, - /// Subtitle / caption track. - Subtitle, + /// Video track. Stacks visually above/below siblings by compositing + /// order; upper tracks occlude lower ones. + #[default] + Video, + /// Audio track. Contributes to the mix; subject to mute/solo. + Audio, + /// Subtitle / caption track. + Subtitle, } /// A single clip on a track. @@ -77,70 +77,70 @@ pub enum TrackKind { /// expensive (thumbnails, waveforms) belongs behind the /// [`ClipDecorator`](super::ClipDecorator) cache hooks, not here. pub trait ClipData { - /// The clip's stable, unique [`ClipId`]. See its docs for the stability - /// requirements. - fn id(&self) -> ClipId; + /// The clip's stable, unique [`ClipId`]. See its docs for the stability + /// requirements. + fn id(&self) -> ClipId; - /// The clip's occupied range in sequence time, `[start, end)`. - /// - /// Must have non-zero length for a real clip. Gaps between clips are not - /// represented (Oak's `GapBlock` is implicit here). - fn range(&self) -> FrameRange; + /// The clip's occupied range in sequence time, `[start, end)`. + /// + /// Must have non-zero length for a real clip. Gaps between clips are not + /// represented (Oak's `GapBlock` is implicit here). + fn range(&self) -> FrameRange; - /// The offset into the source media, in frames, at which this clip - /// starts playing. - /// - /// A clip created from frame 100 of a source file reports `Frame(100)`. - /// Trimming the clip's left edge by `n` frames increases this by `n`. - /// The widget displays this nowhere directly but forwards it in trim - /// requests' docs and uses it for thumbnail/waveform alignment via the - /// decorator hooks. - fn media_in(&self) -> Frame; + /// The offset into the source media, in frames, at which this clip + /// starts playing. + /// + /// A clip created from frame 100 of a source file reports `Frame(100)`. + /// Trimming the clip's left edge by `n` frames increases this by `n`. + /// The widget displays this nowhere directly but forwards it in trim + /// requests' docs and uses it for thumbnail/waveform alignment via the + /// decorator hooks. + fn media_in(&self) -> Frame; - /// Short label shown on the clip body (typically the source file name). - fn label(&self) -> SharedString; + /// Short label shown on the clip body (typically the source file name). + fn label(&self) -> SharedString; - /// Base color of the clip body. The widget derives hover/selected/ - /// disabled shades from it. `None` falls back to the track-kind default. - fn color(&self) -> Option { - None - } + /// Base color of the clip body. The widget derives hover/selected/ + /// disabled shades from it. `None` falls back to the track-kind default. + fn color(&self) -> Option { + None + } - /// Clips that must move and trim together with this one. - /// - /// This is Oak's *linked clips* concept: audio and video clips recorded - /// from the same source are linked, so trimming the video's head trims - /// the audio identically. The widget expands every move/trim request to - /// cover the transitive link group before emitting it — see - /// [`TimelineEvent::ClipMoveRequested`](super::TimelineEvent::ClipMoveRequested). - /// - /// Must not contain `self.id()`. May be empty (the common case). - fn linked_ids(&self) -> Vec { - Vec::new() - } + /// Clips that must move and trim together with this one. + /// + /// This is Oak's *linked clips* concept: audio and video clips recorded + /// from the same source are linked, so trimming the video's head trims + /// the audio identically. The widget expands every move/trim request to + /// cover the transitive link group before emitting it — see + /// [`TimelineEvent::ClipMoveRequested`](super::TimelineEvent::ClipMoveRequested). + /// + /// Must not contain `self.id()`. May be empty (the common case). + fn linked_ids(&self) -> Vec { + Vec::new() + } - /// Duration of the clip's **in transition** (Oak: the `TransitionBlock` - /// attached to the clip's head), if any, in frames. - /// - /// Rendered as a wedge at the clip's left edge. The transition itself is - /// edited elsewhere; the timeline only displays it. - fn in_transition(&self) -> Option { - None - } + /// Duration of the clip's **in transition** (Oak: the `TransitionBlock` + /// attached to the clip's head), if any, in frames. + /// + /// Rendered as a wedge at the clip's left edge. The transition itself is + /// edited elsewhere; the timeline only displays it. + fn in_transition(&self) -> Option { + None + } - /// Duration of the clip's **out transition** (Oak: the `TransitionBlock` - /// attached to the clip's tail), if any, in frames. - fn out_transition(&self) -> Option { - None - } + /// Duration of the clip's **out transition** (Oak: the `TransitionBlock` + /// attached to the clip's tail), if any, in frames. + fn out_transition(&self) -> Option { + None + } - /// Whether the clip is enabled (not disabled/bypassed). - /// - /// Disabled clips render dimmed and are skipped by snapping; the flag - /// itself is toggled through the app's engine, not the timeline. - fn is_enabled(&self) -> bool { - true - } + /// Whether the clip is enabled (not disabled/bypassed). + /// + /// Disabled clips render dimmed and are skipped by snapping; the flag + /// itself is toggled through the app's engine, not the timeline. + fn is_enabled(&self) -> bool { + true + } } /// A single track (row) of the timeline. @@ -148,67 +148,67 @@ pub trait ClipData { /// Corresponds to an Oak track of one of the `k_video` / `k_audio` / /// `k_subtitle` types. pub trait TrackData { - /// The clip type carried by this track. - type Clip: ClipData; + /// The clip type carried by this track. + type Clip: ClipData; - /// What kind of content this track holds. - fn kind(&self) -> TrackKind; + /// What kind of content this track holds. + fn kind(&self) -> TrackKind; - /// Display name for the track header (e.g. `V1`, `Music`). - fn name(&self) -> SharedString; + /// Display name for the track header (e.g. `V1`, `Music`). + fn name(&self) -> SharedString; - /// Whether the track is locked. Locked tracks render normally but reject - /// all edit gestures (no moves, no trims, no drops); the widget checks - /// this before emitting any edit request. - fn is_locked(&self) -> bool { - false - } + /// Whether the track is locked. Locked tracks render normally but reject + /// all edit gestures (no moves, no trims, no drops); the widget checks + /// this before emitting any edit request. + fn is_locked(&self) -> bool { + false + } - /// Whether the track is muted (audio) — silenced in playback. - /// - /// Meaningful for [`TrackKind::Audio`]; the header only shows the mute - /// button there. - fn is_muted(&self) -> bool { - false - } + /// Whether the track is muted (audio) — silenced in playback. + /// + /// Meaningful for [`TrackKind::Audio`]; the header only shows the mute + /// button there. + fn is_muted(&self) -> bool { + false + } - /// Whether the track is soloed (audio) — all non-solo tracks are - /// temporarily silenced. - fn is_solo(&self) -> bool { - false - } + /// Whether the track is soloed (audio) — all non-solo tracks are + /// temporarily silenced. + fn is_solo(&self) -> bool { + false + } - /// Whether the track is visible (video) — Oak's *show* flag. - /// - /// Meaningful for [`TrackKind::Video`] and [`TrackKind::Subtitle`]. - fn is_visible(&self) -> bool { - true - } + /// Whether the track is visible (video) — Oak's *show* flag. + /// + /// Meaningful for [`TrackKind::Video`] and [`TrackKind::Subtitle`]. + fn is_visible(&self) -> bool { + true + } - /// The track's row height in the clip area. - /// - /// This is view state that Oak persists per sequence; it changes via - /// [`TimelineEvent::TrackHeightChanged`](super::TimelineEvent::TrackHeightChanged) - /// and must be written back into the model there. - fn height(&self) -> Pixels; + /// The track's row height in the clip area. + /// + /// This is view state that Oak persists per sequence; it changes via + /// [`TimelineEvent::TrackHeightChanged`](super::TimelineEvent::TrackHeightChanged) + /// and must be written back into the model there. + fn height(&self) -> Pixels; - /// The clips on this track, in ascending frame order, non-overlapping. - /// - /// Returned as a slice so the widget can binary-search by frame. If your - /// model cannot produce a contiguous slice, collect into a buffer you - /// own and return that. - fn clips(&self) -> &[Self::Clip]; + /// The clips on this track, in ascending frame order, non-overlapping. + /// + /// Returned as a slice so the widget can binary-search by frame. If your + /// model cannot produce a contiguous slice, collect into a buffer you + /// own and return that. + fn clips(&self) -> &[Self::Clip]; } /// A marker on the sequence ruler (chapter marks, annotations). #[derive(Debug, Clone, PartialEq)] pub struct Marker { - /// Where the marker sits, in sequence frames. - pub frame: Frame, - /// Label shown in the marker tooltip / ruler. - pub label: SharedString, - /// Optional marker color; defaults to the theme's accent. - pub color: Option, + /// Where the marker sits, in sequence frames. + pub frame: Frame, + /// Label shown in the marker tooltip / ruler. + pub label: SharedString, + /// Optional marker color; defaults to the theme's accent. + pub color: Option, } /// The root data source the timeline widget reads from. @@ -226,31 +226,31 @@ pub struct Marker { /// widget requests arrives as a [`TimelineEvent`](super::TimelineEvent) that /// the facade turns into an undoable engine command. pub trait TimelineDataSource: 'static { - /// The track type returned by [`TimelineDataSource::track`]. - type Track: TrackData; + /// The track type returned by [`TimelineDataSource::track`]. + type Track: TrackData; - /// The sequence's frame rate (e.g. [`FrameRate::NTSC_2997`]). - /// - /// Assumed constant for the lifetime of the sequence; changing it - /// requires rebuilding the view. - fn frame_rate(&self) -> FrameRate; + /// The sequence's frame rate (e.g. [`FrameRate::NTSC_2997`]). + /// + /// Assumed constant for the lifetime of the sequence; changing it + /// requires rebuilding the view. + fn frame_rate(&self) -> FrameRate; - /// Total length of the sequence in frames — the position just past the - /// last frame of content. Playhead and scroll are clamped to this. - fn sequence_length(&self) -> Frame; + /// Total length of the sequence in frames — the position just past the + /// last frame of content. Playhead and scroll are clamped to this. + fn sequence_length(&self) -> Frame; - /// Number of tracks. Indices are stable within a single notification - /// cycle. - fn track_count(&self) -> usize; + /// Number of tracks. Indices are stable within a single notification + /// cycle. + fn track_count(&self) -> usize; - /// The track at `index`, or `None` if out of range. - /// - /// Returns by value so implementations can hand out lightweight - /// snapshot views of their internal track storage. - fn track(&self, index: usize) -> Option; + /// The track at `index`, or `None` if out of range. + /// + /// Returns by value so implementations can hand out lightweight + /// snapshot views of their internal track storage. + fn track(&self, index: usize) -> Option; - /// All sequence markers, in ascending frame order. - fn markers(&self) -> Vec { - Vec::new() - } + /// All sequence markers, in ascending frame order. + fn markers(&self) -> Vec { + Vec::new() + } } diff --git a/crates/gpui/src/timeline/mod.rs b/crates/gpui/src/timeline/mod.rs index 6f780c967d..91a4a32528 100644 --- a/crates/gpui/src/timeline/mod.rs +++ b/crates/gpui/src/timeline/mod.rs @@ -63,8 +63,8 @@ pub mod playhead; pub mod ruler; pub mod state; pub mod time; -pub mod track_header; pub mod timeline_view; +pub mod track_header; pub use clip::*; pub use data::*; @@ -72,5 +72,5 @@ pub use playhead::*; pub use ruler::*; pub use state::*; pub use time::*; -pub use track_header::*; pub use timeline_view::*; +pub use track_header::*; diff --git a/crates/gpui/src/timeline/playhead.rs b/crates/gpui/src/timeline/playhead.rs index 320cb69cb4..7045e93195 100644 --- a/crates/gpui/src/timeline/playhead.rs +++ b/crates/gpui/src/timeline/playhead.rs @@ -10,7 +10,7 @@ use std::time::Instant; use crate::{ - App, Bounds, Hsla, PathBuilder, Pixels, Window, canvas, fill, point, px, size, prelude::*, + App, Bounds, Hsla, PathBuilder, Pixels, Window, canvas, fill, point, prelude::*, px, size, }; use super::time::{Frame, FrameRate}; @@ -22,58 +22,56 @@ use super::time::{Frame, FrameRate}; /// so the element itself does no time math. #[derive(IntoElement)] pub struct PlayheadElement { - x: Pixels, - color: Hsla, + x: Pixels, + color: Hsla, } impl PlayheadElement { - /// Creates the playhead element at screen position `x`. - pub fn new(x: Pixels, color: Hsla) -> Self { - PlayheadElement { x, color } - } + /// Creates the playhead element at screen position `x`. + pub fn new(x: Pixels, color: Hsla) -> Self { + PlayheadElement { x, color } + } - /// Screen x of the line. - pub fn x(&self) -> Pixels { - self.x - } + /// Screen x of the line. + pub fn x(&self) -> Pixels { + self.x + } } impl RenderOnce for PlayheadElement { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let x = self.x; - let color = self.color; + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let x = self.x; + let color = self.color; - canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, cx| { - let _ = cx; - let x = bounds.left() + x; + canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, cx| { + let _ = cx; + let x = bounds.left() + x; - // The playhead line, full height of the element. The width is - // 1 px and the line is centered on `x`. - window.paint_quad(fill( - Bounds { - origin: point(x, bounds.top()), - size: size(px(1.0), bounds.size.height), - }, - color, - )); + // The playhead line, full height of the element. The width is + // 1 px and the line is centered on `x`. + window.paint_quad(fill( + Bounds { + origin: point(x, bounds.top()), + size: size(px(1.0), bounds.size.height), + }, + color, + )); - // A small downward-pointing grab handle at the top of the - // line, marking where the user can drag to seek. - let handle_height = 8.0; - let mut path = PathBuilder::fill(); - path.move_to(point(px(x.0 - 5.0), bounds.top())); - path.line_to(point(px(x.0 + 6.0), bounds.top())); - path.line_to(point(px(x.0 + 0.5), bounds.top() + px(handle_height))); - path.close(); - let path = path - .build() - .expect("playhead handle path always builds"); - window.paint_path(path, color); - }, - ) - } + // A small downward-pointing grab handle at the top of the + // line, marking where the user can drag to seek. + let handle_height = 8.0; + let mut path = PathBuilder::fill(); + path.move_to(point(px(x.0 - 5.0), bounds.top())); + path.line_to(point(px(x.0 + 6.0), bounds.top())); + path.line_to(point(px(x.0 + 0.5), bounds.top() + px(handle_height))); + path.close(); + let path = path.build().expect("playhead handle path always builds"); + window.paint_path(path, color); + }, + ) + } } /// Drives playhead advancement while the sequence is playing. @@ -99,61 +97,61 @@ impl RenderOnce for PlayheadElement { /// (clamped to the sequence) and emits [`TimelineEvent::PlayheadChanged`](super::TimelineEvent::PlayheadChanged) /// when it changes. pub struct PlayheadTicker { - rate: FrameRate, - start_frame: Frame, - start_instant: Option, + rate: FrameRate, + start_frame: Frame, + start_instant: Option, } impl PlayheadTicker { - /// Creates a stopped ticker for sequences running at `rate`. - pub fn new(rate: FrameRate) -> Self { - PlayheadTicker { - rate, - start_frame: Frame::ZERO, - start_instant: None, - } - } + /// Creates a stopped ticker for sequences running at `rate`. + pub fn new(rate: FrameRate) -> Self { + PlayheadTicker { + rate, + start_frame: Frame::ZERO, + start_instant: None, + } + } - /// Whether playback is currently running. - pub fn is_playing(&self) -> bool { - self.start_instant.is_some() - } + /// Whether playback is currently running. + pub fn is_playing(&self) -> bool { + self.start_instant.is_some() + } - /// Starts playback from `start_frame`. - /// - /// Re-anchors both the wall-clock and frame anchors (see the type docs), - /// so pausing and resuming never accumulates error. If already playing, - /// this restarts the anchor — useful for jog/shuttle seeks mid-playback. - /// - /// The owning [`TimelineView`](super::TimelineView) is responsible for - /// scheduling the animation-frame loop - /// ([`Window::request_animation_frame`](crate::Window::request_animation_frame)) and polling - /// [`Self::current_frame`] each tick, pushing the result through - /// [`TimelineState::set_playhead`](super::TimelineState::set_playhead) - /// and emitting [`TimelineEvent::PlayheadChanged`](super::TimelineEvent::PlayheadChanged) on change. - pub fn start(&mut self, start_frame: Frame) { - self.start_frame = start_frame; - self.start_instant = Some(Instant::now()); - } + /// Starts playback from `start_frame`. + /// + /// Re-anchors both the wall-clock and frame anchors (see the type docs), + /// so pausing and resuming never accumulates error. If already playing, + /// this restarts the anchor — useful for jog/shuttle seeks mid-playback. + /// + /// The owning [`TimelineView`](super::TimelineView) is responsible for + /// scheduling the animation-frame loop + /// ([`Window::request_animation_frame`](crate::Window::request_animation_frame)) and polling + /// [`Self::current_frame`] each tick, pushing the result through + /// [`TimelineState::set_playhead`](super::TimelineState::set_playhead) + /// and emitting [`TimelineEvent::PlayheadChanged`](super::TimelineEvent::PlayheadChanged) on change. + pub fn start(&mut self, start_frame: Frame) { + self.start_frame = start_frame; + self.start_instant = Some(Instant::now()); + } - /// The playhead position right now, per the drift-free formula in the - /// type docs. When stopped, returns the last anchor frame. - pub fn current_frame(&self) -> Frame { - match self.start_instant { - Some(instant) => { - self.start_frame - + super::time::seconds_to_frame(self.rate, instant.elapsed().as_secs_f64()) - } - None => self.start_frame, - } - } + /// The playhead position right now, per the drift-free formula in the + /// type docs. When stopped, returns the last anchor frame. + pub fn current_frame(&self) -> Frame { + match self.start_instant { + Some(instant) => { + self.start_frame + + super::time::seconds_to_frame(self.rate, instant.elapsed().as_secs_f64()) + } + None => self.start_frame, + } + } - /// Stops playback. Returns the frame playback stopped at, so the caller - /// can make it the new playhead rest position. - pub fn stop(&mut self) -> Frame { - let frame = self.current_frame(); - self.start_instant = None; - self.start_frame = frame; - frame - } + /// Stops playback. Returns the frame playback stopped at, so the caller + /// can make it the new playhead rest position. + pub fn stop(&mut self) -> Frame { + let frame = self.current_frame(); + self.start_instant = None; + self.start_frame = frame; + frame + } } diff --git a/crates/gpui/src/timeline/ruler.rs b/crates/gpui/src/timeline/ruler.rs index 9414ec5c7d..54351b6f6e 100644 --- a/crates/gpui/src/timeline/ruler.rs +++ b/crates/gpui/src/timeline/ruler.rs @@ -19,13 +19,13 @@ //! shorter and unlabeled. use crate::{ - App, Bounds, Font, SharedString, TextAlign, TextRun, Window, canvas, fill, hsla, point, px, - size, prelude::*, + App, Bounds, Font, SharedString, TextAlign, TextRun, Window, canvas, fill, hsla, point, + prelude::*, px, size, }; use super::{ - state::TimelineState, - time::{Frame, FrameRange, FrameRate, TimeDisplay}, + state::TimelineState, + time::{Frame, FrameRange, FrameRate, TimeDisplay}, }; /// The sequence ruler rendered above the tracks. @@ -38,311 +38,303 @@ use super::{ /// because elements cannot emit events. #[derive(IntoElement)] pub struct TimelineRuler { - state: TimelineState, - frame_rate: FrameRate, - sequence_length: Frame, - display: TimeDisplay, + state: TimelineState, + frame_rate: FrameRate, + sequence_length: Frame, + display: TimeDisplay, } impl TimelineRuler { - /// Minimum on-screen distance between two labeled ticks, in pixels. The - /// adaptive step ladder never picks a step smaller than this. - pub const MIN_TICK_SPACING: f32 = 80.0; + /// Minimum on-screen distance between two labeled ticks, in pixels. The + /// adaptive step ladder never picks a step smaller than this. + pub const MIN_TICK_SPACING: f32 = 80.0; - /// Creates a ruler element snapshotting the given view state. - /// - /// * `state` — supplies zoom and horizontal scroll; the ruler shares the - /// clip area's mapping exactly. - /// * `frame_rate` / `sequence_length` — from the - /// [`TimelineDataSource`](super::TimelineDataSource). - pub fn new( - state: TimelineState, - frame_rate: FrameRate, - sequence_length: Frame, - ) -> Self { - TimelineRuler { - state, - frame_rate, - sequence_length, - display: TimeDisplay::default(), - } - } + /// Creates a ruler element snapshotting the given view state. + /// + /// * `state` — supplies zoom and horizontal scroll; the ruler shares the + /// clip area's mapping exactly. + /// * `frame_rate` / `sequence_length` — from the + /// [`TimelineDataSource`](super::TimelineDataSource). + pub fn new(state: TimelineState, frame_rate: FrameRate, sequence_length: Frame) -> Self { + TimelineRuler { + state, + frame_rate, + sequence_length, + display: TimeDisplay::default(), + } + } - /// Builder: how to label major ticks. Defaults to - /// [`TimeDisplay::Timecode`]. - pub fn time_display(mut self, display: TimeDisplay) -> Self { - self.display = display; - self - } + /// Builder: how to label major ticks. Defaults to + /// [`TimeDisplay::Timecode`]. + pub fn time_display(mut self, display: TimeDisplay) -> Self { + self.display = display; + self + } - /// The tick step (in frames) the ruler would choose at the given zoom. - /// - /// Exposed for tests and for snapping the playhead-drag indicator to the - /// visible grid. Must return a value from the "nice step" ladder - /// described in the module docs such that - /// `step * zoom >= Self::MIN_TICK_SPACING` for all but the coarsest - /// step. - pub fn tick_step(&self, zoom: f32) -> Frame { - // Nominal (integer) frames per second, matching the non-drop-frame - // convention used by `format_timecode` (NTSC 29.97 labels in 30 fps). - let fps = self.frame_rate.as_f64().round() as i64; - // The "nice step" ladder, finest to coarsest, in frames: - // 1, 2, 5, 10, 30 (frames), 1/2/5/10/30 seconds, 1/5/10/30 minutes, - // 1/2 hours. - let ladder = [ - 1, - 2, - 5, - 10, - 30, - fps, - 2 * fps, - 5 * fps, - 10 * fps, - 30 * fps, - 60 * fps, - 300 * fps, - 600 * fps, - 1800 * fps, - 3600 * fps, - 7200 * fps, - ]; - for step in ladder { - if step as f32 * zoom >= Self::MIN_TICK_SPACING { - return Frame(step); - } - } - // Coarsest step; the spacing contract allows the last ladder entry to - // fall short of `MIN_TICK_SPACING`. - Frame(7200 * fps) - } + /// The tick step (in frames) the ruler would choose at the given zoom. + /// + /// Exposed for tests and for snapping the playhead-drag indicator to the + /// visible grid. Must return a value from the "nice step" ladder + /// described in the module docs such that + /// `step * zoom >= Self::MIN_TICK_SPACING` for all but the coarsest + /// step. + pub fn tick_step(&self, zoom: f32) -> Frame { + // Nominal (integer) frames per second, matching the non-drop-frame + // convention used by `format_timecode` (NTSC 29.97 labels in 30 fps). + let fps = self.frame_rate.as_f64().round() as i64; + // The "nice step" ladder, finest to coarsest, in frames: + // 1, 2, 5, 10, 30 (frames), 1/2/5/10/30 seconds, 1/5/10/30 minutes, + // 1/2 hours. + let ladder = [ + 1, + 2, + 5, + 10, + 30, + fps, + 2 * fps, + 5 * fps, + 10 * fps, + 30 * fps, + 60 * fps, + 300 * fps, + 600 * fps, + 1800 * fps, + 3600 * fps, + 7200 * fps, + ]; + for step in ladder { + if step as f32 * zoom >= Self::MIN_TICK_SPACING { + return Frame(step); + } + } + // Coarsest step; the spacing contract allows the last ladder entry to + // fall short of `MIN_TICK_SPACING`. + Frame(7200 * fps) + } - /// The work-area band to paint, if any. - pub fn work_area(&self) -> Option { - self.state.work_area - } + /// The work-area band to paint, if any. + pub fn work_area(&self) -> Option { + self.state.work_area + } - /// Label text for a major tick at `frame`, per - /// [`Self::time_display`]. - pub fn tick_label(&self, frame: Frame) -> SharedString { - super::time::format_timecode(frame, self.frame_rate, self.display).into() - } + /// Label text for a major tick at `frame`, per + /// [`Self::time_display`]. + pub fn tick_label(&self, frame: Frame) -> SharedString { + super::time::format_timecode(frame, self.frame_rate, self.display).into() + } } /// A single ruler tick computed during canvas prepaint. struct RulerTick { - /// Local x within the ruler (relative to its left edge, which aligns - /// with the clip area's left edge). - x: f32, - /// Whether this is a major (tall, labeled) tick. - major: bool, - /// The label for major ticks. - label: Option, + /// Local x within the ruler (relative to its left edge, which aligns + /// with the clip area's left edge). + x: f32, + /// Whether this is a major (tall, labeled) tick. + major: bool, + /// The label for major ticks. + label: Option, } /// Everything the canvas paint closure needs, computed in prepaint. struct RulerContent { - ticks: Vec, - /// Local x-extents `(left, right)` of the work-area band, if a work - /// area is set. - work_area: Option<(f32, f32)>, + ticks: Vec, + /// Local x-extents `(left, right)` of the work-area band, if a work + /// area is set. + work_area: Option<(f32, f32)>, } impl RenderOnce for TimelineRuler { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - canvas( - move |bounds, _window, _cx| { - // All state is captured by moving `self` into this prepaint - // closure; the paint closure only needs the precomputed - // `RulerContent`, so it borrows nothing from `self`. - let state = &self.state; - let work_area = self.work_area(); - let step = self.tick_step(state.zoom); - let sequence_length = self.sequence_length; + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + canvas( + move |bounds, _window, _cx| { + // All state is captured by moving `self` into this prepaint + // closure; the paint closure only needs the precomputed + // `RulerContent`, so it borrows nothing from `self`. + let state = &self.state; + let work_area = self.work_area(); + let step = self.tick_step(state.zoom); + let sequence_length = self.sequence_length; - // First and last on-screen frames in ruler-local coordinates, - // which match the clip area's, so `TimelineState`'s - // frame↔pixel mapping applies directly. The last frame is - // clamped to the sequence so the ruler doesn't draw an - // endless row of ticks if the view is scrolled far right. - let first = state.frame_at_point(px(0.0)); - let last = Frame( - state - .frame_at_point(bounds.size.width) - .0 - .min(sequence_length.0), - ); - // Align to multiples of `step` so ticks stay put relative to - // the clip grid while scrolling. - let start = Frame(first.0.div_euclid(step.0) * step.0); + // First and last on-screen frames in ruler-local coordinates, + // which match the clip area's, so `TimelineState`'s + // frame↔pixel mapping applies directly. The last frame is + // clamped to the sequence so the ruler doesn't draw an + // endless row of ticks if the view is scrolled far right. + let first = state.frame_at_point(px(0.0)); + let last = Frame( + state + .frame_at_point(bounds.size.width) + .0 + .min(sequence_length.0), + ); + // Align to multiples of `step` so ticks stay put relative to + // the clip grid while scrolling. + let start = Frame(first.0.div_euclid(step.0) * step.0); - let mut ticks = Vec::new(); - let mut frame = start; - while frame <= last { - ticks.push(RulerTick { - x: state.point_at_frame(frame).0, - major: true, - label: Some(self.tick_label(frame)), - }); - frame = frame + step; - } + let mut ticks = Vec::new(); + let mut frame = start; + while frame <= last { + ticks.push(RulerTick { + x: state.point_at_frame(frame).0, + major: true, + label: Some(self.tick_label(frame)), + }); + frame = frame + step; + } - // Minor ticks at the midpoint between majors, only when they - // keep enough pixel separation to be legible. - if step.0 >= 2 && (step.0 as f32 / 2.0) * state.zoom >= 4.0 { - let mut frame = Frame(start.0 + step.0 / 2); - while frame <= last { - ticks.push(RulerTick { - x: state.point_at_frame(frame).0, - major: false, - label: None, - }); - frame = frame + step; - } - } + // Minor ticks at the midpoint between majors, only when they + // keep enough pixel separation to be legible. + if step.0 >= 2 && (step.0 as f32 / 2.0) * state.zoom >= 4.0 { + let mut frame = Frame(start.0 + step.0 / 2); + while frame <= last { + ticks.push(RulerTick { + x: state.point_at_frame(frame).0, + major: false, + label: None, + }); + frame = frame + step; + } + } - RulerContent { - ticks, - work_area: work_area.map(|range| { - ( - state.point_at_frame(range.start).0, - state.point_at_frame(range.end).0, - ) - }), - } - }, - move |bounds, content, window, cx| { - let baseline_color = hsla(0.0, 0.0, 0.5, 0.5); - let major_color = hsla(0.0, 0.0, 0.6, 0.9); - let minor_color = hsla(0.0, 0.0, 0.6, 0.45); - let text_color = hsla(0.0, 0.0, 0.5, 1.0); - let band_color = hsla(0.63, 0.55, 0.55, 0.10); - let bottom = bounds.bottom(); + RulerContent { + ticks, + work_area: work_area.map(|range| { + ( + state.point_at_frame(range.start).0, + state.point_at_frame(range.end).0, + ) + }), + } + }, + move |bounds, content, window, cx| { + let baseline_color = hsla(0.0, 0.0, 0.5, 0.5); + let major_color = hsla(0.0, 0.0, 0.6, 0.9); + let minor_color = hsla(0.0, 0.0, 0.6, 0.45); + let text_color = hsla(0.0, 0.0, 0.5, 1.0); + let band_color = hsla(0.63, 0.55, 0.55, 0.10); + let bottom = bounds.bottom(); - // Work-area band under the ticks, with edge lines. - if let Some((left, right)) = content.work_area { - let width = px((right - left).max(0.0)); - let left = bounds.left() + px(left); - let band = Bounds { - origin: point(left, bounds.top()), - size: size(width, bounds.size.height), - }; - window.paint_quad(fill(band, band_color)); - for edge in [left.0, left.0 + width.0] { - window.paint_quad(fill( - Bounds { - origin: point(px(edge), bounds.top()), - size: size(px(1.0), bounds.size.height), - }, - band_color, - )); - } - } + // Work-area band under the ticks, with edge lines. + if let Some((left, right)) = content.work_area { + let width = px((right - left).max(0.0)); + let left = bounds.left() + px(left); + let band = Bounds { + origin: point(left, bounds.top()), + size: size(width, bounds.size.height), + }; + window.paint_quad(fill(band, band_color)); + for edge in [left.0, left.0 + width.0] { + window.paint_quad(fill( + Bounds { + origin: point(px(edge), bounds.top()), + size: size(px(1.0), bounds.size.height), + }, + band_color, + )); + } + } - // Baseline along the bottom of the ruler. - window.paint_quad(fill( - Bounds { - origin: point(bounds.left(), bottom - px(1.0)), - size: size(bounds.size.width, px(1.0)), - }, - baseline_color, - )); + // Baseline along the bottom of the ruler. + window.paint_quad(fill( + Bounds { + origin: point(bounds.left(), bottom - px(1.0)), + size: size(bounds.size.width, px(1.0)), + }, + baseline_color, + )); - for tick in content.ticks { - let x = bounds.left() + px(tick.x); - let height = if tick.major { 16.0 } else { 8.0 }; - window.paint_quad(fill( - Bounds { - origin: point(x, bottom - px(height)), - size: size(px(1.0), px(height)), - }, - if tick.major { - major_color - } else { - minor_color - }, - )); - if let Some(label) = tick.label { - let len = label.len(); - let line = window.text_system().shape_line( - label, - px(11.0), - &[TextRun { - len, - font: Font::default(), - color: text_color, - background_color: None, - underline: None, - strikethrough: None, - letter_spacing: None, - }], - None, - ); - let _ = line.paint( - point(px(x.0 + 4.0), bottom - px(23.0)), - px(12.0), - TextAlign::Left, - None, - window, - cx, - ); - } - } - }, - ) - } + for tick in content.ticks { + let x = bounds.left() + px(tick.x); + let height = if tick.major { 16.0 } else { 8.0 }; + window.paint_quad(fill( + Bounds { + origin: point(x, bottom - px(height)), + size: size(px(1.0), px(height)), + }, + if tick.major { major_color } else { minor_color }, + )); + if let Some(label) = tick.label { + let len = label.len(); + let line = window.text_system().shape_line( + label, + px(11.0), + &[TextRun { + len, + font: Font::default(), + color: text_color, + background_color: None, + underline: None, + strikethrough: None, + letter_spacing: None, + }], + None, + ); + let _ = line.paint( + point(px(x.0 + 4.0), bottom - px(23.0)), + px(12.0), + TextAlign::Left, + None, + window, + cx, + ); + } + } + }, + ) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn tick_step_returns_nice_ladder_step() { - let ruler = TimelineRuler::new( - TimelineState::new(), - FrameRate::new(30, 1), - Frame(30 * 60 * 60), - ); - // At 100 px/frame a single frame spans 100 px: the finest step wins. - assert_eq!(ruler.tick_step(100.0), Frame(1)); - // At 3 px/frame a 1-second step spans 90 px: the smallest ladder - // entry that clears MIN_TICK_SPACING is one second (30 frames). - assert_eq!(ruler.tick_step(3.0), Frame(30)); - // At 1/3000 px/frame a 2-hour step spans 64.8 px, under the minimum; - // the ladder's last entry is the fallback. - assert_eq!(ruler.tick_step(0.0003), Frame(7200 * 30)); - } + #[test] + fn tick_step_returns_nice_ladder_step() { + let ruler = TimelineRuler::new( + TimelineState::new(), + FrameRate::new(30, 1), + Frame(30 * 60 * 60), + ); + // At 100 px/frame a single frame spans 100 px: the finest step wins. + assert_eq!(ruler.tick_step(100.0), Frame(1)); + // At 3 px/frame a 1-second step spans 90 px: the smallest ladder + // entry that clears MIN_TICK_SPACING is one second (30 frames). + assert_eq!(ruler.tick_step(3.0), Frame(30)); + // At 1/3000 px/frame a 2-hour step spans 64.8 px, under the minimum; + // the ladder's last entry is the fallback. + assert_eq!(ruler.tick_step(0.0003), Frame(7200 * 30)); + } - #[test] - fn tick_step_uses_nominal_fps_for_fractional_rates() { - // NTSC 29.97 labels ticks in nominal 30 fps, matching - // `format_timecode`'s non-drop-frame convention. - let ruler = TimelineRuler::new( - TimelineState::new(), - FrameRate::NTSC_2997, - Frame(30 * 60 * 60), - ); - assert_eq!(ruler.tick_step(3.0), Frame(30)); - } + #[test] + fn tick_step_uses_nominal_fps_for_fractional_rates() { + // NTSC 29.97 labels ticks in nominal 30 fps, matching + // `format_timecode`'s non-drop-frame convention. + let ruler = TimelineRuler::new( + TimelineState::new(), + FrameRate::NTSC_2997, + Frame(30 * 60 * 60), + ); + assert_eq!(ruler.tick_step(3.0), Frame(30)); + } - #[test] - fn tick_step_keeps_labels_apart() { - let ruler = TimelineRuler::new( - TimelineState::new(), - FrameRate::new(25, 1), - Frame(25 * 60 * 60), - ); - for zoom in [0.001, 0.01, 0.1, 0.5, 1.0, 3.0, 10.0, 100.0, 1000.0] { - let step = ruler.tick_step(zoom); - // The coarsest step (2 hours at 25 fps) is the documented - // exception to the spacing contract. - if step.0 != 7200 * 25 { - assert!( - step.0 as f32 * zoom >= TimelineRuler::MIN_TICK_SPACING, - "zoom {zoom}: step {step:?} under-spaces" - ); - } - } - } + #[test] + fn tick_step_keeps_labels_apart() { + let ruler = TimelineRuler::new( + TimelineState::new(), + FrameRate::new(25, 1), + Frame(25 * 60 * 60), + ); + for zoom in [0.001, 0.01, 0.1, 0.5, 1.0, 3.0, 10.0, 100.0, 1000.0] { + let step = ruler.tick_step(zoom); + // The coarsest step (2 hours at 25 fps) is the documented + // exception to the spacing contract. + if step.0 != 7200 * 25 { + assert!( + step.0 as f32 * zoom >= TimelineRuler::MIN_TICK_SPACING, + "zoom {zoom}: step {step:?} under-spaces" + ); + } + } + } } diff --git a/crates/gpui/src/timeline/state.rs b/crates/gpui/src/timeline/state.rs index 75a86a6f1e..262b878551 100644 --- a/crates/gpui/src/timeline/state.rs +++ b/crates/gpui/src/timeline/state.rs @@ -23,7 +23,7 @@ use std::collections::BTreeSet; -use crate::{Pixels, Point, px, point}; +use crate::{Pixels, Point, point, px}; use super::data::ClipId; use super::time::{Frame, FrameRange}; @@ -45,184 +45,188 @@ pub const MAX_ZOOM: f32 = 1000.0; /// [`Self::set_playhead`]). #[derive(Debug, Clone)] pub struct TimelineState { - /// Horizontal scale, in **pixels per frame**. Drives both the clip area - /// and the ruler. Clamped to `[MIN_ZOOM, MAX_ZOOM]` by - /// [`Self::set_zoom`]. - pub zoom: f32, + /// Horizontal scale, in **pixels per frame**. Drives both the clip area + /// and the ruler. Clamped to `[MIN_ZOOM, MAX_ZOOM]` by + /// [`Self::set_zoom`]. + pub zoom: f32, - /// Content coordinate currently at the top-left of the viewport. `x` - /// scrolls the whole timeline; `y` scrolls the stacked tracks. - pub scroll_offset: Point, + /// Content coordinate currently at the top-left of the viewport. `x` + /// scrolls the whole timeline; `y` scrolls the stacked tracks. + pub scroll_offset: Point, - /// Current playhead position, in sequence frames. - pub playhead: Frame, + /// Current playhead position, in sequence frames. + pub playhead: Frame, - /// The current clip selection. - /// - /// A `BTreeSet` so iteration order is deterministic (paint z-order of - /// selection outlines, test assertions) and membership tests are cheap. - pub selection: BTreeSet, + /// The current clip selection. + /// + /// A `BTreeSet` so iteration order is deterministic (paint z-order of + /// selection outlines, test assertions) and membership tests are cheap. + pub selection: BTreeSet, - /// Whether drag operations snap to clip edges, the playhead, work-area - /// edges and markers. Toggled by the user (Oak: the magnet toolbar - /// button); checked by every drag handler before calling - /// [`snap`](super::snap). - pub snap_enabled: bool, + /// Whether drag operations snap to clip edges, the playhead, work-area + /// edges and markers. Toggled by the user (Oak: the magnet toolbar + /// button); checked by every drag handler before calling + /// [`snap`](super::snap). + pub snap_enabled: bool, - /// The work area (render/export in-out range), if set. Shown as a band - /// on the ruler and offered as snap points. - pub work_area: Option, + /// The work area (render/export in-out range), if set. Shown as a band + /// on the ruler and offered as snap points. + pub work_area: Option, } impl Default for TimelineState { - fn default() -> Self { - TimelineState { - zoom: 1.0, - scroll_offset: point(px(0.), px(0.)), - playhead: Frame::ZERO, - selection: BTreeSet::new(), - snap_enabled: true, - work_area: None, - } - } + fn default() -> Self { + TimelineState { + zoom: 1.0, + scroll_offset: point(px(0.), px(0.)), + playhead: Frame::ZERO, + selection: BTreeSet::new(), + snap_enabled: true, + work_area: None, + } + } } impl TimelineState { - /// Creates a default state: zoom 1 px/frame, no scroll, playhead at - /// zero, empty selection, snapping on, no work area. - pub fn new() -> Self { - Self::default() - } + /// Creates a default state: zoom 1 px/frame, no scroll, playhead at + /// zero, empty selection, snapping on, no work area. + pub fn new() -> Self { + Self::default() + } - /// Maps a horizontal screen position in the clip area to a sequence - /// frame: `(x + scroll_offset.x) / zoom`, rounded **toward zero** to the - /// nearest whole frame. - /// - /// Positions left of the content start yield negative frames; callers - /// clamp to the sequence as appropriate. This is the exact inverse of - /// [`Self::point_at_frame`] — see the module docs for the mapping. - /// - /// # Panics - /// - /// Never panics in release; in debug it asserts that `zoom > 0`. - pub fn frame_at_point(&self, x: Pixels) -> Frame { - debug_assert!(self.zoom > 0.0, "zoom must be positive"); - let content_x = x + self.scroll_offset.x; - Frame((content_x / px(self.zoom)) as i64) - } + /// Maps a horizontal screen position in the clip area to a sequence + /// frame: `(x + scroll_offset.x) / zoom`, rounded **toward zero** to the + /// nearest whole frame. + /// + /// Positions left of the content start yield negative frames; callers + /// clamp to the sequence as appropriate. This is the exact inverse of + /// [`Self::point_at_frame`] — see the module docs for the mapping. + /// + /// # Panics + /// + /// Never panics in release; in debug it asserts that `zoom > 0`. + pub fn frame_at_point(&self, x: Pixels) -> Frame { + debug_assert!(self.zoom > 0.0, "zoom must be positive"); + let content_x = x + self.scroll_offset.x; + Frame((content_x / px(self.zoom)) as i64) + } - /// Maps a sequence frame to its horizontal screen position in the clip - /// area: `frame * zoom - scroll_offset.x`. - /// - /// Frames scrolled off-screen yield negative or beyond-viewport values; - /// that is expected — painters clip to their bounds. - pub fn point_at_frame(&self, frame: Frame) -> Pixels { - px(frame.0 as f32 * self.zoom) - self.scroll_offset.x - } + /// Maps a sequence frame to its horizontal screen position in the clip + /// area: `frame * zoom - scroll_offset.x`. + /// + /// Frames scrolled off-screen yield negative or beyond-viewport values; + /// that is expected — painters clip to their bounds. + pub fn point_at_frame(&self, frame: Frame) -> Pixels { + px(frame.0 as f32 * self.zoom) - self.scroll_offset.x + } - /// Sets the zoom, clamped to `[MIN_ZOOM, MAX_ZOOM]`, while keeping the - /// frame under `anchor` (a screen x position, typically the cursor) - /// stationary on screen. - /// - /// # Math contract - /// - /// Let `f = frame_at_point(anchor)` (fractional, before truncation). - /// After zooming, `scroll_offset.x` is adjusted so that - /// `f * new_zoom - new_scroll_x == anchor`, i.e.: - /// - /// ```text - /// new_scroll_x = (anchor + old_scroll_x) * (new_zoom / old_zoom) - anchor - /// ``` - /// - /// clamped to `>= px(0.)`. This is the standard "zoom to cursor" - /// behavior of every NLE. - pub fn set_zoom(&mut self, zoom: f32, anchor: Pixels) { - let new_zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM); - let old_zoom = self.zoom.max(MIN_ZOOM); - let content_at_anchor = anchor + self.scroll_offset.x; - let new_scroll = content_at_anchor * (new_zoom / old_zoom) - anchor; - self.zoom = new_zoom; - self.scroll_offset.x = if new_scroll < px(0.) { px(0.) } else { new_scroll }; - } + /// Sets the zoom, clamped to `[MIN_ZOOM, MAX_ZOOM]`, while keeping the + /// frame under `anchor` (a screen x position, typically the cursor) + /// stationary on screen. + /// + /// # Math contract + /// + /// Let `f = frame_at_point(anchor)` (fractional, before truncation). + /// After zooming, `scroll_offset.x` is adjusted so that + /// `f * new_zoom - new_scroll_x == anchor`, i.e.: + /// + /// ```text + /// new_scroll_x = (anchor + old_scroll_x) * (new_zoom / old_zoom) - anchor + /// ``` + /// + /// clamped to `>= px(0.)`. This is the standard "zoom to cursor" + /// behavior of every NLE. + pub fn set_zoom(&mut self, zoom: f32, anchor: Pixels) { + let new_zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM); + let old_zoom = self.zoom.max(MIN_ZOOM); + let content_at_anchor = anchor + self.scroll_offset.x; + let new_scroll = content_at_anchor * (new_zoom / old_zoom) - anchor; + self.zoom = new_zoom; + self.scroll_offset.x = if new_scroll < px(0.) { + px(0.) + } else { + new_scroll + }; + } - /// Sets the playhead, clamped to `[Frame::ZERO, sequence_length]`. - /// - /// `sequence_length` comes from - /// [`TimelineDataSource::sequence_length`](super::TimelineDataSource::sequence_length); - /// passing the inclusive end is legal — the playhead may rest one frame - /// past the last content frame. - pub fn set_playhead(&mut self, frame: Frame, sequence_length: Frame) { - self.playhead = frame.clamp(Frame::ZERO, sequence_length); - } + /// Sets the playhead, clamped to `[Frame::ZERO, sequence_length]`. + /// + /// `sequence_length` comes from + /// [`TimelineDataSource::sequence_length`](super::TimelineDataSource::sequence_length); + /// passing the inclusive end is legal — the playhead may rest one frame + /// past the last content frame. + pub fn set_playhead(&mut self, frame: Frame, sequence_length: Frame) { + self.playhead = frame.clamp(Frame::ZERO, sequence_length); + } - /// Replaces the selection with exactly `id`. - pub fn select(&mut self, id: ClipId) { - self.selection.clear(); - self.selection.insert(id); - } + /// Replaces the selection with exactly `id`. + pub fn select(&mut self, id: ClipId) { + self.selection.clear(); + self.selection.insert(id); + } - /// Adds `id` to the selection without disturbing the rest (shift-click). - pub fn add_to_selection(&mut self, id: ClipId) { - self.selection.insert(id); - } + /// Adds `id` to the selection without disturbing the rest (shift-click). + pub fn add_to_selection(&mut self, id: ClipId) { + self.selection.insert(id); + } - /// Toggles `id`'s membership in the selection (ctrl/cmd-click). - pub fn toggle(&mut self, id: ClipId) { - if !self.selection.remove(&id) { - self.selection.insert(id); - } - } + /// Toggles `id`'s membership in the selection (ctrl/cmd-click). + pub fn toggle(&mut self, id: ClipId) { + if !self.selection.remove(&id) { + self.selection.insert(id); + } + } - /// Empties the selection. - pub fn clear_selection(&mut self) { - self.selection.clear(); - } + /// Empties the selection. + pub fn clear_selection(&mut self) { + self.selection.clear(); + } - /// Selects exactly the given clips (marquee/rubber-band result). - /// - /// The hit-testing that produces `ids` lives in - /// [`TimelineView`](super::TimelineView); this method only stores the - /// outcome, replacing any previous selection. - pub fn select_range(&mut self, ids: impl IntoIterator) { - self.selection = ids.into_iter().collect(); - } + /// Selects exactly the given clips (marquee/rubber-band result). + /// + /// The hit-testing that produces `ids` lives in + /// [`TimelineView`](super::TimelineView); this method only stores the + /// outcome, replacing any previous selection. + pub fn select_range(&mut self, ids: impl IntoIterator) { + self.selection = ids.into_iter().collect(); + } - /// Whether `id` is currently selected. - pub fn is_selected(&self, id: ClipId) -> bool { - self.selection.contains(&id) - } + /// Whether `id` is currently selected. + pub fn is_selected(&self, id: ClipId) -> bool { + self.selection.contains(&id) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn point_and_frame_are_inverse() { - let mut state = TimelineState::new(); - state.zoom = 2.5; - state.scroll_offset.x = px(40.); - let x = state.point_at_frame(Frame(100)); - assert_eq!(state.frame_at_point(x), Frame(100)); - } + #[test] + fn point_and_frame_are_inverse() { + let mut state = TimelineState::new(); + state.zoom = 2.5; + state.scroll_offset.x = px(40.); + let x = state.point_at_frame(Frame(100)); + assert_eq!(state.frame_at_point(x), Frame(100)); + } - #[test] - fn zoom_keeps_anchor_frame_stationary() { - let mut state = TimelineState::new(); - state.scroll_offset.x = px(100.); - let anchor = px(200.); - // At zoom 1, frame 300 sits at screen x = 300 - 100 = 200 = anchor. - state.set_zoom(4.0, anchor); - // After zooming, frame 300 must still sit exactly under the anchor. - assert_eq!(state.point_at_frame(Frame(300)), anchor); - } + #[test] + fn zoom_keeps_anchor_frame_stationary() { + let mut state = TimelineState::new(); + state.scroll_offset.x = px(100.); + let anchor = px(200.); + // At zoom 1, frame 300 sits at screen x = 300 - 100 = 200 = anchor. + state.set_zoom(4.0, anchor); + // After zooming, frame 300 must still sit exactly under the anchor. + assert_eq!(state.point_at_frame(Frame(300)), anchor); + } - #[test] - fn playhead_is_clamped() { - let mut state = TimelineState::new(); - state.set_playhead(Frame(-5), Frame(100)); - assert_eq!(state.playhead, Frame::ZERO); - state.set_playhead(Frame(500), Frame(100)); - assert_eq!(state.playhead, Frame(100)); - } + #[test] + fn playhead_is_clamped() { + let mut state = TimelineState::new(); + state.set_playhead(Frame(-5), Frame(100)); + assert_eq!(state.playhead, Frame::ZERO); + state.set_playhead(Frame(500), Frame(100)); + assert_eq!(state.playhead, Frame(100)); + } } diff --git a/crates/gpui/src/timeline/time.rs b/crates/gpui/src/timeline/time.rs index ad2f6ebc3a..75f1395c39 100644 --- a/crates/gpui/src/timeline/time.rs +++ b/crates/gpui/src/timeline/time.rs @@ -45,51 +45,54 @@ use crate::{Pixels, px}; /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FrameRate { - /// Numerator of the rate (frames per `den` seconds). Must be non-zero. - pub num: u32, - /// Denominator of the rate. Must be non-zero. - pub den: u32, + /// Numerator of the rate (frames per `den` seconds). Must be non-zero. + pub num: u32, + /// Denominator of the rate. Must be non-zero. + pub den: u32, } impl FrameRate { - /// NTSC "29.97" fps, exactly `30000/1001`. - pub const NTSC_2997: FrameRate = FrameRate { - num: 30000, - den: 1001, - }; + /// NTSC "29.97" fps, exactly `30000/1001`. + pub const NTSC_2997: FrameRate = FrameRate { + num: 30000, + den: 1001, + }; - /// NTSC "23.976" fps, exactly `24000/1001`. - pub const NTSC_23976: FrameRate = FrameRate { - num: 24000, - den: 1001, - }; + /// NTSC "23.976" fps, exactly `24000/1001`. + pub const NTSC_23976: FrameRate = FrameRate { + num: 24000, + den: 1001, + }; - /// Creates a frame rate from a numerator and denominator. - /// - /// # Panics - /// - /// Panics if either `num` or `den` is zero — a zero frame rate is - /// meaningless and would cause division by zero in every conversion. - /// - /// # Examples - /// - /// ``` - /// # use gpui::timeline::FrameRate; - /// let pal = FrameRate::new(25, 1); - /// assert_eq!(pal.as_f64(), 25.0); - /// ``` - pub fn new(num: u32, den: u32) -> Self { - assert!(num != 0 && den != 0, "frame rate components must be non-zero"); - FrameRate { num, den } - } + /// Creates a frame rate from a numerator and denominator. + /// + /// # Panics + /// + /// Panics if either `num` or `den` is zero — a zero frame rate is + /// meaningless and would cause division by zero in every conversion. + /// + /// # Examples + /// + /// ``` + /// # use gpui::timeline::FrameRate; + /// let pal = FrameRate::new(25, 1); + /// assert_eq!(pal.as_f64(), 25.0); + /// ``` + pub fn new(num: u32, den: u32) -> Self { + assert!( + num != 0 && den != 0, + "frame rate components must be non-zero" + ); + FrameRate { num, den } + } - /// Returns the rate as a floating-point frames-per-second value. - /// - /// Intended for display and for one-shot wall-clock conversions only; - /// never store positions or durations derived from this value. - pub fn as_f64(self) -> f64 { - self.num as f64 / self.den as f64 - } + /// Returns the rate as a floating-point frames-per-second value. + /// + /// Intended for display and for one-shot wall-clock conversions only; + /// never store positions or durations derived from this value. + pub fn as_f64(self) -> f64 { + self.num as f64 / self.den as f64 + } } /// An absolute position or duration on the timeline, in frames. @@ -104,43 +107,43 @@ impl FrameRate { pub struct Frame(pub i64); impl Frame { - /// Frame zero, the start of every sequence. - pub const ZERO: Frame = Frame(0); + /// Frame zero, the start of every sequence. + pub const ZERO: Frame = Frame(0); - /// Returns the raw frame number. - pub fn number(self) -> i64 { - self.0 - } + /// Returns the raw frame number. + pub fn number(self) -> i64 { + self.0 + } - /// Returns this frame as a [`Pixels`] offset at the given zoom - /// (pixels per frame). Pure scaling, no scroll compensation — see - /// [`TimelineState::point_at_frame`](super::TimelineState::point_at_frame) - /// for the scroll-aware variant. - pub fn to_pixels(self, zoom: f32) -> Pixels { - Pixels::from(self.0 as f32 * zoom) - } + /// Returns this frame as a [`Pixels`] offset at the given zoom + /// (pixels per frame). Pure scaling, no scroll compensation — see + /// [`TimelineState::point_at_frame`](super::TimelineState::point_at_frame) + /// for the scroll-aware variant. + pub fn to_pixels(self, zoom: f32) -> Pixels { + Pixels::from(self.0 as f32 * zoom) + } } impl std::ops::Add for Frame { - type Output = Frame; + type Output = Frame; - fn add(self, rhs: Frame) -> Frame { - Frame(self.0 + rhs.0) - } + fn add(self, rhs: Frame) -> Frame { + Frame(self.0 + rhs.0) + } } impl std::ops::Sub for Frame { - type Output = Frame; + type Output = Frame; - fn sub(self, rhs: Frame) -> Frame { - Frame(self.0 - rhs.0) - } + fn sub(self, rhs: Frame) -> Frame { + Frame(self.0 - rhs.0) + } } impl std::ops::AddAssign for Frame { - fn add_assign(&mut self, rhs: Frame) { - self.0 += rhs.0; - } + fn add_assign(&mut self, rhs: Frame) { + self.0 += rhs.0; + } } /// A half-open range of frames, `[start, end)`. @@ -151,66 +154,66 @@ impl std::ops::AddAssign for Frame { /// represents zero duration. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct FrameRange { - /// First frame of the range (inclusive). - pub start: Frame, - /// End of the range (exclusive). - pub end: Frame, + /// First frame of the range (inclusive). + pub start: Frame, + /// End of the range (exclusive). + pub end: Frame, } impl FrameRange { - /// Creates a range from `start` (inclusive) to `end` (exclusive). - /// - /// # Panics - /// - /// Panics if `end < start`. - pub fn new(start: Frame, end: Frame) -> Self { - assert!(end >= start, "frame range end must not precede start"); - FrameRange { start, end } - } + /// Creates a range from `start` (inclusive) to `end` (exclusive). + /// + /// # Panics + /// + /// Panics if `end < start`. + pub fn new(start: Frame, end: Frame) -> Self { + assert!(end >= start, "frame range end must not precede start"); + FrameRange { start, end } + } - /// The number of frames in the range. Zero for an empty range. - pub fn len(&self) -> Frame { - Frame(self.end.0 - self.start.0) - } + /// The number of frames in the range. Zero for an empty range. + pub fn len(&self) -> Frame { + Frame(self.end.0 - self.start.0) + } - /// Whether the range contains no frames. - pub fn is_empty(&self) -> bool { - self.start == self.end - } + /// Whether the range contains no frames. + pub fn is_empty(&self) -> bool { + self.start == self.end + } - /// Whether `frame` lies inside the range (`start <= frame < end`). - pub fn contains(&self, frame: Frame) -> bool { - self.start <= frame && frame < self.end - } + /// Whether `frame` lies inside the range (`start <= frame < end`). + pub fn contains(&self, frame: Frame) -> bool { + self.start <= frame && frame < self.end + } - /// Whether two ranges share at least one frame. - pub fn overlaps(&self, other: &FrameRange) -> bool { - self.start < other.end && other.start < self.end - } + /// Whether two ranges share at least one frame. + pub fn overlaps(&self, other: &FrameRange) -> bool { + self.start < other.end && other.start < self.end + } } /// How time should be presented to the user in rulers and inspectors. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum TimeDisplay { - /// `HH:MM:SS:FF` non-drop-frame timecode, the standard for integer frame - /// rates (24, 25, 30, 50, 60) and the fallback for NTSC rates when - /// wall-clock alignment is not required. - /// - /// This is the default and the professional-video standard for integer - /// rates. - #[default] - Timecode, - /// `HH:MM:SS;FF` SMPTE drop-frame timecode for NTSC-derived rates - /// (29.97, 23.976, 59.94, 119.88): frame numbers `00`/`01` (or `00`..`03` - /// at 59.94/119.88) are skipped at the start of every minute except the - /// tenth, keeping the timecode in lockstep with wall-clock time. For - /// non-NTSC rates [`format_timecode`] falls back to non-drop-frame - /// output. See [`format_timecode`] for the exact algorithm. - TimecodeDropFrame, - /// A plain frame counter, e.g. `1048576`. - Frames, - /// Seconds with millisecond precision, e.g. `83.708`. - Seconds, + /// `HH:MM:SS:FF` non-drop-frame timecode, the standard for integer frame + /// rates (24, 25, 30, 50, 60) and the fallback for NTSC rates when + /// wall-clock alignment is not required. + /// + /// This is the default and the professional-video standard for integer + /// rates. + #[default] + Timecode, + /// `HH:MM:SS;FF` SMPTE drop-frame timecode for NTSC-derived rates + /// (29.97, 23.976, 59.94, 119.88): frame numbers `00`/`01` (or `00`..`03` + /// at 59.94/119.88) are skipped at the start of every minute except the + /// tenth, keeping the timecode in lockstep with wall-clock time. For + /// non-NTSC rates [`format_timecode`] falls back to non-drop-frame + /// output. See [`format_timecode`] for the exact algorithm. + TimecodeDropFrame, + /// A plain frame counter, e.g. `1048576`. + Frames, + /// Seconds with millisecond precision, e.g. `83.708`. + Seconds, } /// Converts a frame position to floating-point seconds at `rate`. @@ -228,7 +231,7 @@ pub enum TimeDisplay { /// assert!((frame_to_seconds(Frame(30), rate) - 1.001).abs() < 1e-9); /// ``` pub fn frame_to_seconds(frame: Frame, rate: FrameRate) -> f64 { - frame.0 as f64 * rate.den as f64 / rate.num as f64 + frame.0 as f64 * rate.den as f64 / rate.num as f64 } /// Converts floating-point seconds to the nearest frame at `rate`. @@ -254,7 +257,7 @@ pub fn frame_to_seconds(frame: Frame, rate: FrameRate) -> f64 { /// assert_eq!(seconds_to_frame(rate, 1.03), Frame(25)); /// ``` pub fn seconds_to_frame(rate: FrameRate, seconds: f64) -> Frame { - Frame((seconds * rate.num as f64 / rate.den as f64).round() as i64) + Frame((seconds * rate.num as f64 / rate.den as f64).round() as i64) } /// Formats `frame` for display according to `display`. @@ -312,57 +315,57 @@ pub fn seconds_to_frame(rate: FrameRate, seconds: f64) -> Frame { /// assert_eq!(format_timecode(Frame(1800), ntsc, TimeDisplay::TimecodeDropFrame), "00:01:00;02"); /// ``` pub fn format_timecode(frame: Frame, rate: FrameRate, display: TimeDisplay) -> String { - match display { - TimeDisplay::Frames => frame.0.to_string(), - TimeDisplay::Seconds => format!("{:.3}", frame_to_seconds(frame, rate)), - TimeDisplay::Timecode => { - let negative = frame.0 < 0; - let mut n = frame.0.unsigned_abs(); - // Nominal (integer) frame count per second, matching the - // non-drop-frame convention: NTSC 29.97 uses 30 frames/sec. - let fps = rate.as_f64().round() as u64; - let frames = n % fps; - n /= fps; - let seconds = n % 60; - n /= 60; - let minutes = n % 60; - let hours = n / 60; - format!( - "{}{:02}:{:02}:{:02}:{:02}", - if negative { "-" } else { "" }, - hours, - minutes, - seconds, - frames - ) - } - TimeDisplay::TimecodeDropFrame => { - if rate.den != 1001 { - // There is no drop-frame convention outside NTSC-derived - // rates; fall back to the ordinary non-drop string (including - // its `:` separator) so the two variants agree. - return format_timecode(frame, rate, TimeDisplay::Timecode); - } - let negative = frame.0 < 0; - let n = frame.0.unsigned_abs(); - let (nominal, adjusted) = drop_frame_adjust(n, rate); - let mut n = adjusted; - let frames = n % nominal; - n /= nominal; - let seconds = n % 60; - n /= 60; - let minutes = n % 60; - let hours = n / 60; - format!( - "{}{:02}:{:02}:{:02};{:02}", - if negative { "-" } else { "" }, - hours, - minutes, - seconds, - frames - ) - } - } + match display { + TimeDisplay::Frames => frame.0.to_string(), + TimeDisplay::Seconds => format!("{:.3}", frame_to_seconds(frame, rate)), + TimeDisplay::Timecode => { + let negative = frame.0 < 0; + let mut n = frame.0.unsigned_abs(); + // Nominal (integer) frame count per second, matching the + // non-drop-frame convention: NTSC 29.97 uses 30 frames/sec. + let fps = rate.as_f64().round() as u64; + let frames = n % fps; + n /= fps; + let seconds = n % 60; + n /= 60; + let minutes = n % 60; + let hours = n / 60; + format!( + "{}{:02}:{:02}:{:02}:{:02}", + if negative { "-" } else { "" }, + hours, + minutes, + seconds, + frames + ) + } + TimeDisplay::TimecodeDropFrame => { + if rate.den != 1001 { + // There is no drop-frame convention outside NTSC-derived + // rates; fall back to the ordinary non-drop string (including + // its `:` separator) so the two variants agree. + return format_timecode(frame, rate, TimeDisplay::Timecode); + } + let negative = frame.0 < 0; + let n = frame.0.unsigned_abs(); + let (nominal, adjusted) = drop_frame_adjust(n, rate); + let mut n = adjusted; + let frames = n % nominal; + n /= nominal; + let seconds = n % 60; + n /= 60; + let minutes = n % 60; + let hours = n / 60; + format!( + "{}{:02}:{:02}:{:02};{:02}", + if negative { "-" } else { "" }, + hours, + minutes, + seconds, + frames + ) + } + } } /// Applies the SMPTE drop-frame adjustment to a real frame count at an @@ -371,43 +374,43 @@ pub fn format_timecode(frame: Frame, rate: FrameRate, display: TimeDisplay) -> S /// Returns `(nominal_fps, adjusted_count)`, where `adjusted_count` is the /// timecode frame count with the skipped frame numbers re-inserted. fn drop_frame_adjust(frame: u64, rate: FrameRate) -> (u64, u64) { - debug_assert!( - rate.den == 1001, - "drop-frame adjustment is only defined for NTSC-derived rates (denominator 1001)" - ); - let nominal = rate.as_f64().round() as u64; + debug_assert!( + rate.den == 1001, + "drop-frame adjustment is only defined for NTSC-derived rates (denominator 1001)" + ); + let nominal = rate.as_f64().round() as u64; - // Skipped frame numbers per non-10th minute: two per 30 fps of nominal - // rate (2 at 29.97/23.976, 4 at 59.94, 8 at 119.88). - let drop = ((nominal as f64) * 2.0 / 30.0).round() as u64; - // Real frame counts per minute and per 10 minutes at this rate. - let frames_per_10_min = (rate.as_f64() * 600.0).round() as u64; - let frames_per_min = (rate.as_f64() * 60.0).round() as u64; + // Skipped frame numbers per non-10th minute: two per 30 fps of nominal + // rate (2 at 29.97/23.976, 4 at 59.94, 8 at 119.88). + let drop = ((nominal as f64) * 2.0 / 30.0).round() as u64; + // Real frame counts per minute and per 10 minutes at this rate. + let frames_per_10_min = (rate.as_f64() * 600.0).round() as u64; + let frames_per_min = (rate.as_f64() * 60.0).round() as u64; - let ten_minute_blocks = frame / frames_per_10_min; - let within_block = frame % frames_per_10_min; + let ten_minute_blocks = frame / frames_per_10_min; + let within_block = frame % frames_per_10_min; - let mut adjusted = frame + drop * 9 * ten_minute_blocks; - if within_block > drop { - adjusted += drop * ((within_block - drop) / frames_per_min); - } - (nominal, adjusted) + let mut adjusted = frame + drop * 9 * ten_minute_blocks; + if within_block > drop { + adjusted += drop * ((within_block - drop) / frames_per_min); + } + (nominal, adjusted) } /// What produced a [`SnapPoint`]. Used by the UI to pick an indicator style /// and by tests to assert snapping priority. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SnapKind { - /// The start edge of a clip. - ClipStart, - /// The end edge of a clip. - ClipEnd, - /// The playhead. - Playhead, - /// An edge of the work area (in or out point). - WorkAreaEdge, - /// A user or chapter marker. - Marker, + /// The start edge of a clip. + ClipStart, + /// The end edge of a clip. + ClipEnd, + /// The playhead. + Playhead, + /// An edge of the work area (in or out point). + WorkAreaEdge, + /// A user or chapter marker. + Marker, } /// A frame position that dragged elements can snap to. @@ -417,23 +420,23 @@ pub enum SnapKind { /// playhead, work-area edges and markers. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SnapPoint { - /// The frame to snap to. - pub frame: Frame, - /// What this point represents. - pub kind: SnapKind, + /// The frame to snap to. + pub frame: Frame, + /// What this point represents. + pub kind: SnapKind, } /// The outcome of a successful [`snap`] query. #[derive(Debug, Clone, Copy, PartialEq)] pub struct SnapResult { - /// The snapped frame — equal to the winning [`SnapPoint`]'s frame. - pub frame: Frame, - /// The kind of the winning snap point. - pub kind: SnapKind, - /// On-screen distance between the drag position and the snap point, in - /// pixels. Always `<=` the threshold passed to [`snap`]. Useful for - /// fading the snap indicator as the cursor approaches. - pub distance: Pixels, + /// The snapped frame — equal to the winning [`SnapPoint`]'s frame. + pub frame: Frame, + /// The kind of the winning snap point. + pub kind: SnapKind, + /// On-screen distance between the drag position and the snap point, in + /// pixels. Always `<=` the threshold passed to [`snap`]. Useful for + /// fading the snap indicator as the cursor approaches. + pub distance: Pixels, } /// Finds the best snap target for a dragged position, or `None` if nothing @@ -456,264 +459,264 @@ pub struct SnapResult { /// The returned frame is always exactly one of the provided snap points' /// frames; this function never invents intermediate positions. pub fn snap( - target: Frame, - points: impl Iterator, - threshold_px: Pixels, - zoom: f32, + target: Frame, + points: impl Iterator, + threshold_px: Pixels, + zoom: f32, ) -> Option { - if threshold_px.0 <= 0.0 || zoom <= 0.0 { - return None; - } + if threshold_px.0 <= 0.0 || zoom <= 0.0 { + return None; + } - // Work in frame space for the distance comparison: a threshold given in - // pixels is `threshold_px / zoom` frames at this zoom, and a point's - // on-screen distance is `|point - target| * zoom`. Both quantities scale - // by the same positive zoom, so ordering is preserved — we compare in - // frame space and only convert the winning distance back to pixels. - let threshold_frames = threshold_px.0 / zoom; + // Work in frame space for the distance comparison: a threshold given in + // pixels is `threshold_px / zoom` frames at this zoom, and a point's + // on-screen distance is `|point - target| * zoom`. Both quantities scale + // by the same positive zoom, so ordering is preserved — we compare in + // frame space and only convert the winning distance back to pixels. + let threshold_frames = threshold_px.0 / zoom; - // Priority per [`SnapKind`] for deterministic tie-breaking: `Playhead` - // first, then `ClipStart`/`ClipEnd`, then `WorkAreaEdge`, then `Marker`. - let kind_rank = |kind: SnapKind| match kind { - SnapKind::Playhead => 0, - SnapKind::ClipStart | SnapKind::ClipEnd => 1, - SnapKind::WorkAreaEdge => 2, - SnapKind::Marker => 3, - }; + // Priority per [`SnapKind`] for deterministic tie-breaking: `Playhead` + // first, then `ClipStart`/`ClipEnd`, then `WorkAreaEdge`, then `Marker`. + let kind_rank = |kind: SnapKind| match kind { + SnapKind::Playhead => 0, + SnapKind::ClipStart | SnapKind::ClipEnd => 1, + SnapKind::WorkAreaEdge => 2, + SnapKind::Marker => 3, + }; - // Best candidate, compared lexicographically: (screen distance in frames, - // frame number, kind rank). Smaller is better on every component. The - // frame-number tiebreak prefers the earlier snap point when two points are - // equally close (so the result is always exactly one of the given frames, - // never an interpolated position), and the kind rank breaks ties between - // points sharing a frame; both keep the outcome independent of iterator - // order. - let mut best: Option<(f32, i64, u8)> = None; - let mut best_kind = SnapKind::Marker; - for point in points { - let dist = (point.frame.0 - target.0).unsigned_abs() as f32; - if dist > threshold_frames { - continue; - } - let candidate = (dist, point.frame.0, kind_rank(point.kind)); - if best.map_or(true, |current| candidate < current) { - best = Some(candidate); - best_kind = point.kind; - } - } + // Best candidate, compared lexicographically: (screen distance in frames, + // frame number, kind rank). Smaller is better on every component. The + // frame-number tiebreak prefers the earlier snap point when two points are + // equally close (so the result is always exactly one of the given frames, + // never an interpolated position), and the kind rank breaks ties between + // points sharing a frame; both keep the outcome independent of iterator + // order. + let mut best: Option<(f32, i64, u8)> = None; + let mut best_kind = SnapKind::Marker; + for point in points { + let dist = (point.frame.0 - target.0).unsigned_abs() as f32; + if dist > threshold_frames { + continue; + } + let candidate = (dist, point.frame.0, kind_rank(point.kind)); + if best.map_or(true, |current| candidate < current) { + best = Some(candidate); + best_kind = point.kind; + } + } - best.map(|(dist, frame, _)| SnapResult { - frame: Frame(frame), - kind: best_kind, - distance: px(dist * zoom), - }) + best.map(|(dist, frame, _)| SnapResult { + frame: Frame(frame), + kind: best_kind, + distance: px(dist * zoom), + }) } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn frame_range_is_half_open() { - let range = FrameRange::new(Frame(10), Frame(20)); - assert_eq!(range.len(), Frame(10)); - assert!(range.contains(Frame(10))); - assert!(!range.contains(Frame(20))); - assert!(range.overlaps(&FrameRange::new(Frame(19), Frame(30)))); - assert!(!range.overlaps(&FrameRange::new(Frame(20), Frame(30)))); - } + #[test] + fn frame_range_is_half_open() { + let range = FrameRange::new(Frame(10), Frame(20)); + assert_eq!(range.len(), Frame(10)); + assert!(range.contains(Frame(10))); + assert!(!range.contains(Frame(20))); + assert!(range.overlaps(&FrameRange::new(Frame(19), Frame(30)))); + assert!(!range.overlaps(&FrameRange::new(Frame(20), Frame(30)))); + } - #[test] - fn seconds_round_trips_to_nearest_frame() { - let rate = FrameRate::NTSC_2997; - let frame = seconds_to_frame(rate, 10.0); - assert!((frame_to_seconds(frame, rate) - 10.0).abs() < 0.02); - } + #[test] + fn seconds_round_trips_to_nearest_frame() { + let rate = FrameRate::NTSC_2997; + let frame = seconds_to_frame(rate, 10.0); + assert!((frame_to_seconds(frame, rate) - 10.0).abs() < 0.02); + } - #[test] - fn drop_frame_timecode_skips_minute_boundary_frames() { - let rate = FrameRate::NTSC_2997; - let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); - // Within the first minute nothing is dropped. - assert_eq!(tc(0), "00:00:00;00"); - assert_eq!(tc(30), "00:00:01;00"); - assert_eq!(tc(1798), "00:00:59;28"); - assert_eq!(tc(1799), "00:00:59;29"); - // Frame 1800 is just past the first minute boundary; frames 00 and 01 - // of that minute are skipped, so the label jumps to ...;02. - assert_eq!(tc(1800), "00:01:00;02"); - } + #[test] + fn drop_frame_timecode_skips_minute_boundary_frames() { + let rate = FrameRate::NTSC_2997; + let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); + // Within the first minute nothing is dropped. + assert_eq!(tc(0), "00:00:00;00"); + assert_eq!(tc(30), "00:00:01;00"); + assert_eq!(tc(1798), "00:00:59;28"); + assert_eq!(tc(1799), "00:00:59;29"); + // Frame 1800 is just past the first minute boundary; frames 00 and 01 + // of that minute are skipped, so the label jumps to ...;02. + assert_eq!(tc(1800), "00:01:00;02"); + } - #[test] - fn drop_frame_timecode_preserves_tenth_minute() { - let rate = FrameRate::NTSC_2997; - let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); - // 10 minutes of real time at 29.97 is 17982 frames; no frames are - // dropped at the start of the tenth minute. - assert_eq!(tc(17982), "00:10:00;00"); - assert_eq!(tc(17984), "00:10:00;02"); - // 20 minutes: two 10-minute blocks. - assert_eq!(tc(17982 * 2), "00:20:00;00"); - } + #[test] + fn drop_frame_timecode_preserves_tenth_minute() { + let rate = FrameRate::NTSC_2997; + let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); + // 10 minutes of real time at 29.97 is 17982 frames; no frames are + // dropped at the start of the tenth minute. + assert_eq!(tc(17982), "00:10:00;00"); + assert_eq!(tc(17984), "00:10:00;02"); + // 20 minutes: two 10-minute blocks. + assert_eq!(tc(17982 * 2), "00:20:00;00"); + } - #[test] - fn drop_frame_timecode_aligns_with_wall_clock_at_one_hour() { - // One real hour at 29.97 = 107892 frames; drop-frame timecode reads - // exactly 01:00:00;00 (non-drop would read 01:00:02;12, the ~3.6s/h - // drift the convention exists to cancel). - let rate = FrameRate::NTSC_2997; - assert_eq!( - format_timecode(Frame(107892), rate, TimeDisplay::TimecodeDropFrame), - "01:00:00;00" - ); - } + #[test] + fn drop_frame_timecode_aligns_with_wall_clock_at_one_hour() { + // One real hour at 29.97 = 107892 frames; drop-frame timecode reads + // exactly 01:00:00;00 (non-drop would read 01:00:02;12, the ~3.6s/h + // drift the convention exists to cancel). + let rate = FrameRate::NTSC_2997; + assert_eq!( + format_timecode(Frame(107892), rate, TimeDisplay::TimecodeDropFrame), + "01:00:00;00" + ); + } - #[test] - fn drop_frame_timecode_at_59_94_drops_four_frames() { - let rate = FrameRate::new(60000, 1001); - let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); - assert_eq!(tc(0), "00:00:00;00"); - // Real frames per minute at 59.94: round(3596.4) = 3596. Just past - // the first minute the four skipped numbers (00..03) are visible. - assert_eq!(tc(3596), "00:00:59;56"); - assert_eq!(tc(3600), "00:01:00;04"); - // One real hour at 59.94 = round(59.94 * 3600) = 215784 frames. - assert_eq!(tc(215784), "01:00:00;00"); - } + #[test] + fn drop_frame_timecode_at_59_94_drops_four_frames() { + let rate = FrameRate::new(60000, 1001); + let tc = |frame| format_timecode(Frame(frame), rate, TimeDisplay::TimecodeDropFrame); + assert_eq!(tc(0), "00:00:00;00"); + // Real frames per minute at 59.94: round(3596.4) = 3596. Just past + // the first minute the four skipped numbers (00..03) are visible. + assert_eq!(tc(3596), "00:00:59;56"); + assert_eq!(tc(3600), "00:01:00;04"); + // One real hour at 59.94 = round(59.94 * 3600) = 215784 frames. + assert_eq!(tc(215784), "01:00:00;00"); + } - #[test] - fn drop_frame_timecode_falls_back_for_non_ntsc_rates() { - // 24 fps is not NTSC-derived (denominator 1): drop-frame output must - // be identical to non-drop output. - let rate = FrameRate::new(24, 1); - let frame = Frame(24 * 3600 + 24 * 60 + 24 + 12); - assert_eq!( - format_timecode(frame, rate, TimeDisplay::TimecodeDropFrame), - format_timecode(frame, rate, TimeDisplay::Timecode), - ); - assert_eq!( - format_timecode(frame, rate, TimeDisplay::TimecodeDropFrame), - "01:01:01:12", - ); - } + #[test] + fn drop_frame_timecode_falls_back_for_non_ntsc_rates() { + // 24 fps is not NTSC-derived (denominator 1): drop-frame output must + // be identical to non-drop output. + let rate = FrameRate::new(24, 1); + let frame = Frame(24 * 3600 + 24 * 60 + 24 + 12); + assert_eq!( + format_timecode(frame, rate, TimeDisplay::TimecodeDropFrame), + format_timecode(frame, rate, TimeDisplay::Timecode), + ); + assert_eq!( + format_timecode(frame, rate, TimeDisplay::TimecodeDropFrame), + "01:01:01:12", + ); + } - #[test] - fn drop_frame_timecode_handles_negative_frames() { - let rate = FrameRate::NTSC_2997; - assert_eq!( - format_timecode(Frame(-1800), rate, TimeDisplay::TimecodeDropFrame), - "-00:01:00;02", - ); - } + #[test] + fn drop_frame_timecode_handles_negative_frames() { + let rate = FrameRate::NTSC_2997; + assert_eq!( + format_timecode(Frame(-1800), rate, TimeDisplay::TimecodeDropFrame), + "-00:01:00;02", + ); + } - #[test] - fn snap_disabled_by_zero_threshold() { - let points = [SnapPoint { - frame: Frame(10), - kind: SnapKind::Playhead, - }]; - assert!(snap(Frame(12), points.into_iter(), px(0.0), 1.0).is_none()); - } + #[test] + fn snap_disabled_by_zero_threshold() { + let points = [SnapPoint { + frame: Frame(10), + kind: SnapKind::Playhead, + }]; + assert!(snap(Frame(12), points.into_iter(), px(0.0), 1.0).is_none()); + } - #[test] - fn snap_requires_positive_zoom() { - let points = [SnapPoint { - frame: Frame(10), - kind: SnapKind::Playhead, - }]; - assert!(snap(Frame(10), points.into_iter(), px(10.0), 0.0).is_none()); - } + #[test] + fn snap_requires_positive_zoom() { + let points = [SnapPoint { + frame: Frame(10), + kind: SnapKind::Playhead, + }]; + assert!(snap(Frame(10), points.into_iter(), px(10.0), 0.0).is_none()); + } - #[test] - fn snap_returns_none_when_nothing_is_within_range() { - let points = [SnapPoint { - frame: Frame(100), - kind: SnapKind::Playhead, - }]; - // At zoom 1.0 a 5-px threshold is 5 frames; target 92 is 8 frames away. - assert!(snap(Frame(92), points.into_iter(), px(5.0), 1.0).is_none()); - } + #[test] + fn snap_returns_none_when_nothing_is_within_range() { + let points = [SnapPoint { + frame: Frame(100), + kind: SnapKind::Playhead, + }]; + // At zoom 1.0 a 5-px threshold is 5 frames; target 92 is 8 frames away. + assert!(snap(Frame(92), points.into_iter(), px(5.0), 1.0).is_none()); + } - #[test] - fn snap_prefers_the_nearest_point() { - let points = [ - SnapPoint { - frame: Frame(90), - kind: SnapKind::Marker, - }, - SnapPoint { - frame: Frame(95), - kind: SnapKind::Marker, - }, - ]; - let result = snap(Frame(92), points.into_iter(), px(50.0), 1.0).unwrap(); - assert_eq!(result.frame, Frame(90)); - assert_eq!(result.kind, SnapKind::Marker); - assert_eq!(result.distance, px(2.0)); - } + #[test] + fn snap_prefers_the_nearest_point() { + let points = [ + SnapPoint { + frame: Frame(90), + kind: SnapKind::Marker, + }, + SnapPoint { + frame: Frame(95), + kind: SnapKind::Marker, + }, + ]; + let result = snap(Frame(92), points.into_iter(), px(50.0), 1.0).unwrap(); + assert_eq!(result.frame, Frame(90)); + assert_eq!(result.kind, SnapKind::Marker); + assert_eq!(result.distance, px(2.0)); + } - #[test] - fn snap_ties_break_by_kind_priority_regardless_of_iteration_order() { - let priority_points = [ - SnapPoint { - frame: Frame(100), - kind: SnapKind::Playhead, - }, - SnapPoint { - frame: Frame(100), - kind: SnapKind::ClipStart, - }, - ]; - // Equal distance; higher-priority kind wins even though it appears - // first in the iterator. - let result = snap(Frame(100), priority_points.into_iter(), px(10.0), 1.0).unwrap(); - assert_eq!(result.kind, SnapKind::Playhead); - assert_eq!(result.frame, Frame(100)); + #[test] + fn snap_ties_break_by_kind_priority_regardless_of_iteration_order() { + let priority_points = [ + SnapPoint { + frame: Frame(100), + kind: SnapKind::Playhead, + }, + SnapPoint { + frame: Frame(100), + kind: SnapKind::ClipStart, + }, + ]; + // Equal distance; higher-priority kind wins even though it appears + // first in the iterator. + let result = snap(Frame(100), priority_points.into_iter(), px(10.0), 1.0).unwrap(); + assert_eq!(result.kind, SnapKind::Playhead); + assert_eq!(result.frame, Frame(100)); - // Reversed iteration order changes nothing. - let reversed = [ - SnapPoint { - frame: Frame(100), - kind: SnapKind::ClipStart, - }, - SnapPoint { - frame: Frame(100), - kind: SnapKind::Playhead, - }, - ]; - let result = snap(Frame(100), reversed.into_iter(), px(10.0), 1.0).unwrap(); - assert_eq!(result.kind, SnapKind::Playhead); - } + // Reversed iteration order changes nothing. + let reversed = [ + SnapPoint { + frame: Frame(100), + kind: SnapKind::ClipStart, + }, + SnapPoint { + frame: Frame(100), + kind: SnapKind::Playhead, + }, + ]; + let result = snap(Frame(100), reversed.into_iter(), px(10.0), 1.0).unwrap(); + assert_eq!(result.kind, SnapKind::Playhead); + } - #[test] - fn snap_result_frame_is_always_a_snap_point_frame() { - // Two points at equal distance on opposite sides; the earlier frame - // wins via the frame-number tiebreak, never an interpolated position. - let points = [ - SnapPoint { - frame: Frame(98), - kind: SnapKind::WorkAreaEdge, - }, - SnapPoint { - frame: Frame(102), - kind: SnapKind::ClipEnd, - }, - ]; - let result = snap(Frame(100), points.into_iter(), px(10.0), 1.0).unwrap(); - assert_eq!(result.frame, Frame(98)); - assert_eq!(result.distance, px(2.0)); - } + #[test] + fn snap_result_frame_is_always_a_snap_point_frame() { + // Two points at equal distance on opposite sides; the earlier frame + // wins via the frame-number tiebreak, never an interpolated position. + let points = [ + SnapPoint { + frame: Frame(98), + kind: SnapKind::WorkAreaEdge, + }, + SnapPoint { + frame: Frame(102), + kind: SnapKind::ClipEnd, + }, + ]; + let result = snap(Frame(100), points.into_iter(), px(10.0), 1.0).unwrap(); + assert_eq!(result.frame, Frame(98)); + assert_eq!(result.distance, px(2.0)); + } - #[test] - fn snap_threshold_scales_with_zoom() { - // At zoom 2.0 the same 10-px threshold covers only 5 frames. - let points = [SnapPoint { - frame: Frame(50), - kind: SnapKind::ClipEnd, - }]; - assert!(snap(Frame(56), points.into_iter(), px(10.0), 2.0).is_none()); - let result = snap(Frame(55), points.into_iter(), px(10.0), 2.0).unwrap(); - assert_eq!(result.frame, Frame(50)); - assert_eq!(result.distance, px(10.0)); - } + #[test] + fn snap_threshold_scales_with_zoom() { + // At zoom 2.0 the same 10-px threshold covers only 5 frames. + let points = [SnapPoint { + frame: Frame(50), + kind: SnapKind::ClipEnd, + }]; + assert!(snap(Frame(56), points.into_iter(), px(10.0), 2.0).is_none()); + let result = snap(Frame(55), points.into_iter(), px(10.0), 2.0).unwrap(); + assert_eq!(result.frame, Frame(50)); + assert_eq!(result.distance, px(10.0)); + } } diff --git a/crates/gpui/src/timeline/timeline_view.rs b/crates/gpui/src/timeline/timeline_view.rs index 612bc7599c..3dfe228e7d 100644 --- a/crates/gpui/src/timeline/timeline_view.rs +++ b/crates/gpui/src/timeline/timeline_view.rs @@ -68,29 +68,29 @@ use std::collections::BTreeSet; use std::sync::{Arc, RwLock}; use crate::{ - AnyElement, App, Context, DragMoveEvent, ElementId, Entity, EventEmitter, FocusHandle, - Focusable, Hsla, MouseButton, MouseDownEvent, Pixels, PinchEvent, Point, Render, ScrollDelta, - ScrollWheelEvent, SharedString, Window, div, hsla, px, prelude::*, + AnyElement, App, Context, DragMoveEvent, ElementId, Entity, EventEmitter, FocusHandle, + Focusable, Hsla, MouseButton, MouseDownEvent, PinchEvent, Pixels, Point, Render, ScrollDelta, + ScrollWheelEvent, SharedString, Window, div, hsla, prelude::*, px, }; use super::{ - clip::{ClipContent, ClipDecorator, ClipElement, NoopClipDecorator, TRIM_HANDLE_WIDTH}, - data::{ClipData, ClipId, TimelineDataSource, TrackData, TrackKind}, - playhead::PlayheadElement, - ruler::TimelineRuler, - state::TimelineState, - time::{Frame, FrameRange, SnapKind, SnapPoint, snap}, - track_header::TrackHeader, + clip::{ClipContent, ClipDecorator, ClipElement, NoopClipDecorator, TRIM_HANDLE_WIDTH}, + data::{ClipData, ClipId, TimelineDataSource, TrackData, TrackKind}, + playhead::PlayheadElement, + ruler::TimelineRuler, + state::TimelineState, + time::{Frame, FrameRange, SnapKind, SnapPoint, snap}, + track_header::TrackHeader, }; /// Which edge of a clip a trim gesture grabbed. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TrimEdge { - /// The clip's left (in) edge. Trimming it changes both the clip's start - /// and its [`ClipData::media_in`](super::ClipData::media_in). - Start, - /// The clip's right (out) edge. - End, + /// The clip's left (in) edge. Trimming it changes both the clip's start + /// and its [`ClipData::media_in`](super::ClipData::media_in). + Start, + /// The clip's right (out) edge. + End, } /// An edit or view-state change requested by the timeline widget. @@ -115,87 +115,87 @@ pub enum TrimEdge { /// state; not undoable. #[derive(Debug, Clone, PartialEq)] pub enum TimelineEvent { - /// The user asked to move a clip (and, per the gesture docs on - /// [`TimelineView`], its linked group) to a new position. - /// - /// `new_track` is an index into - /// [`TimelineDataSource::track`]; - /// `new_start` is the requested [`FrameRange`](super::FrameRange) start - /// after snapping. The engine must validate collisions and track - /// compatibility; the widget only guarantees the source track was not - /// locked. - ClipMoveRequested { - /// The clip the gesture grabbed. The host expands this to the full - /// linked group. - clip: ClipId, - /// Target track index. - new_track: usize, - /// Requested new start frame. - new_start: Frame, - }, + /// The user asked to move a clip (and, per the gesture docs on + /// [`TimelineView`], its linked group) to a new position. + /// + /// `new_track` is an index into + /// [`TimelineDataSource::track`]; + /// `new_start` is the requested [`FrameRange`](super::FrameRange) start + /// after snapping. The engine must validate collisions and track + /// compatibility; the widget only guarantees the source track was not + /// locked. + ClipMoveRequested { + /// The clip the gesture grabbed. The host expands this to the full + /// linked group. + clip: ClipId, + /// Target track index. + new_track: usize, + /// Requested new start frame. + new_start: Frame, + }, - /// The user asked to trim one edge of a clip. - /// - /// `new_frame` is the requested new position of the grabbed `edge`, - /// after snapping and after clamping to the neighboring clips and to - /// zero minimum length. For [`TrimEdge::Start`] the host must also - /// adjust `media_in` by the same delta. - ClipTrimRequested { - /// The trimmed clip (expand to the linked group, as with moves). - clip: ClipId, - /// Which edge was grabbed. - edge: TrimEdge, - /// Requested new frame position of that edge. - new_frame: Frame, - }, + /// The user asked to trim one edge of a clip. + /// + /// `new_frame` is the requested new position of the grabbed `edge`, + /// after snapping and after clamping to the neighboring clips and to + /// zero minimum length. For [`TrimEdge::Start`] the host must also + /// adjust `media_in` by the same delta. + ClipTrimRequested { + /// The trimmed clip (expand to the linked group, as with moves). + clip: ClipId, + /// Which edge was grabbed. + edge: TrimEdge, + /// Requested new frame position of that edge. + new_frame: Frame, + }, - /// The playhead moved, by any means (ruler seek, keyboard, playback - /// ticker). Carries the new position. Not undoable. - PlayheadChanged(Frame), + /// The playhead moved, by any means (ruler seek, keyboard, playback + /// ticker). Carries the new position. Not undoable. + PlayheadChanged(Frame), - /// The selection changed. The new set is readable from - /// [`TimelineView::selection`]. Not undoable. - SelectionChanged, + /// The selection changed. The new set is readable from + /// [`TimelineView::selection`]. Not undoable. + SelectionChanged, - /// A track header was clicked, toggling the track's selection state. - /// - /// The widget keeps the selected-track set locally (readable from - /// [`TimelineView::selected_tracks`]); the host may persist it. Not - /// undoable. - TrackSelected { - /// Index of the clicked track. - track: usize, - /// Whether the track is now selected. - selected: bool, - }, + /// A track header was clicked, toggling the track's selection state. + /// + /// The widget keeps the selected-track set locally (readable from + /// [`TimelineView::selected_tracks`]); the host may persist it. Not + /// undoable. + TrackSelected { + /// Index of the clicked track. + track: usize, + /// Whether the track is now selected. + selected: bool, + }, - /// The user dragged a transition wedge's edge to change its length. - /// - /// `new_length` is the requested transition duration in frames, clamped - /// to the clip length and to a one-frame minimum. The host validates - /// against the actual transition implementation and re-notifies. - TransitionChanged { - /// The clip whose transition was grabbed. - clip: ClipId, - /// Which edge the transition sits on. - edge: TrimEdge, - /// Requested new transition length. - new_length: Frame, - }, + /// The user dragged a transition wedge's edge to change its length. + /// + /// `new_length` is the requested transition duration in frames, clamped + /// to the clip length and to a one-frame minimum. The host validates + /// against the actual transition implementation and re-notifies. + TransitionChanged { + /// The clip whose transition was grabbed. + clip: ClipId, + /// Which edge the transition sits on. + edge: TrimEdge, + /// Requested new transition length. + new_length: Frame, + }, - /// A track's height changed via header-separator drag. The host should - /// write this back so [`TrackData::height`](super::TrackData::height) - /// returns it on the next read. - TrackHeightChanged { - /// Index of the resized track. - track: usize, - /// New row height. - height: Pixels, - }, + /// A track's height changed via header-separator drag. The host should + /// write this back so [`TrackData::height`](super::TrackData::height) + /// returns it on the next read. + TrackHeightChanged { + /// Index of the resized track. + track: usize, + /// New row height. + height: Pixels, + }, - /// The zoom (pixels per frame) changed via ctrl-scroll or pinch. - /// Persist as view state if desired. - ZoomChanged(f32), + /// The zoom (pixels per frame) changed via ctrl-scroll or pinch. + /// Persist as view state if desired. + ZoomChanged(f32), } /// The video-editing timeline widget. @@ -220,14 +220,14 @@ pub enum TimelineEvent { /// See the module-level docs for the layout, the full interaction list, and /// the edit-request contract. pub struct TimelineView { - source: Entity, - /// View-local state (zoom, scroll, playhead, selection). Public for - /// read access; mutate through [`TimelineState`]'s methods to preserve - /// invariants. - pub state: TimelineState, - /// The set of tracks selected via their headers. - selected_tracks: BTreeSet, - focus_handle: FocusHandle, + source: Entity, + /// View-local state (zoom, scroll, playhead, selection). Public for + /// read access; mutate through [`TimelineState`]'s methods to preserve + /// invariants. + pub state: TimelineState, + /// The set of tracks selected via their headers. + selected_tracks: BTreeSet, + focus_handle: FocusHandle, } /// Width of the track-headers column, in pixels. @@ -240,892 +240,886 @@ const MIN_TRACK_HEIGHT: f32 = 24.0; const SNAP_THRESHOLD_PX: f32 = 8.0; impl TimelineView { - /// Creates a timeline view over `source`. - /// - /// Subscribes to `source`'s notifications: after the host applies an - /// edit and calls `cx.notify()` on the source entity, the view re-reads - /// all data and repaints. - pub fn new(source: Entity, _window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - cx.observe(&source, |_this, _source, cx| cx.notify()).detach(); - TimelineView { - source, - state: TimelineState::new(), - selected_tracks: BTreeSet::new(), - focus_handle, - } - } + /// Creates a timeline view over `source`. + /// + /// Subscribes to `source`'s notifications: after the host applies an + /// edit and calls `cx.notify()` on the source entity, the view re-reads + /// all data and repaints. + pub fn new(source: Entity, _window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + cx.observe(&source, |_this, _source, cx| cx.notify()) + .detach(); + TimelineView { + source, + state: TimelineState::new(), + selected_tracks: BTreeSet::new(), + focus_handle, + } + } - /// The set of tracks selected via header clicks. - pub fn selected_tracks(&self) -> &BTreeSet { - &self.selected_tracks - } + /// The set of tracks selected via header clicks. + pub fn selected_tracks(&self) -> &BTreeSet { + &self.selected_tracks + } - /// Builder: sets the initial zoom (pixels per frame), clamped to - /// [`MIN_ZOOM`](super::MIN_ZOOM)..=[`MAX_ZOOM`](super::MAX_ZOOM). - pub fn zoom(mut self, zoom: f32) -> Self { - self.state.set_zoom(zoom, crate::px(0.)); - self - } + /// Builder: sets the initial zoom (pixels per frame), clamped to + /// [`MIN_ZOOM`](super::MIN_ZOOM)..=[`MAX_ZOOM`](super::MAX_ZOOM). + pub fn zoom(mut self, zoom: f32) -> Self { + self.state.set_zoom(zoom, crate::px(0.)); + self + } - /// Builder: enables or disables snapping initially. - pub fn snap_enabled(mut self, enabled: bool) -> Self { - self.state.snap_enabled = enabled; - self - } + /// Builder: enables or disables snapping initially. + pub fn snap_enabled(mut self, enabled: bool) -> Self { + self.state.snap_enabled = enabled; + self + } - /// The data source entity this view was created over. - pub fn source(&self) -> &Entity { - &self.source - } + /// The data source entity this view was created over. + pub fn source(&self) -> &Entity { + &self.source + } - /// The current selection, in deterministic (id) order. - pub fn selection(&self) -> &BTreeSet { - &self.state.selection - } + /// The current selection, in deterministic (id) order. + pub fn selection(&self) -> &BTreeSet { + &self.state.selection + } - /// Seeks the playhead to `frame`, clamped to the sequence, emitting - /// [`TimelineEvent::PlayheadChanged`] if the position changed. - pub fn seek(&mut self, frame: Frame, cx: &mut Context) { - let seq_len = self.sequence_length(cx); - let old = self.state.playhead; - self.state.set_playhead(frame, seq_len); - if self.state.playhead != old { - cx.emit(TimelineEvent::PlayheadChanged(self.state.playhead)); - cx.notify(); - } - } + /// Seeks the playhead to `frame`, clamped to the sequence, emitting + /// [`TimelineEvent::PlayheadChanged`] if the position changed. + pub fn seek(&mut self, frame: Frame, cx: &mut Context) { + let seq_len = self.sequence_length(cx); + let old = self.state.playhead; + self.state.set_playhead(frame, seq_len); + if self.state.playhead != old { + cx.emit(TimelineEvent::PlayheadChanged(self.state.playhead)); + cx.notify(); + } + } - /// The sequence length from the data source. - fn sequence_length(&self, cx: &mut Context) -> Frame { - self.source.read(cx).sequence_length() - } + /// The sequence length from the data source. + fn sequence_length(&self, cx: &mut Context) -> Frame { + self.source.read(cx).sequence_length() + } - /// The current range of the clip with `id`, scanned from the data source. - /// Falls back to an empty range when the clip no longer exists. - fn clip_range(&self, id: ClipId, cx: &mut Context) -> FrameRange { - let source = self.source.read(cx); - for index in 0..source.track_count() { - if let Some(track) = source.track(index) { - for clip in track.clips() { - if clip.id() == id { - return clip.range(); - } - } - } - } - FrameRange::new(Frame::ZERO, Frame::ZERO) - } + /// The current range of the clip with `id`, scanned from the data source. + /// Falls back to an empty range when the clip no longer exists. + fn clip_range(&self, id: ClipId, cx: &mut Context) -> FrameRange { + let source = self.source.read(cx); + for index in 0..source.track_count() { + if let Some(track) = source.track(index) { + for clip in track.clips() { + if clip.id() == id { + return clip.range(); + } + } + } + } + FrameRange::new(Frame::ZERO, Frame::ZERO) + } - /// Snap points gathered fresh from the data source: work-area edges, - /// markers, the playhead, and every enabled clip edge. `dragged` (the - /// clip being moved) is excluded so a clip never snaps to itself. - fn snap_points(&self, dragged: Option, cx: &mut Context) -> Vec { - let source = self.source.read(cx); - let mut points = Vec::new(); - if let Some(area) = self.state.work_area { - points.push(SnapPoint { - frame: area.start, - kind: SnapKind::WorkAreaEdge, - }); - points.push(SnapPoint { - frame: area.end, - kind: SnapKind::WorkAreaEdge, - }); - } - for marker in source.markers() { - points.push(SnapPoint { - frame: marker.frame, - kind: SnapKind::Marker, - }); - } - points.push(SnapPoint { - frame: self.state.playhead, - kind: SnapKind::Playhead, - }); - for index in 0..source.track_count() { - if let Some(track) = source.track(index) { - for clip in track.clips() { - if clip.is_enabled() && Some(clip.id()) != dragged { - let range = clip.range(); - points.push(SnapPoint { - frame: range.start, - kind: SnapKind::ClipStart, - }); - points.push(SnapPoint { - frame: range.end, - kind: SnapKind::ClipEnd, - }); - } - } - } - } - points - } + /// Snap points gathered fresh from the data source: work-area edges, + /// markers, the playhead, and every enabled clip edge. `dragged` (the + /// clip being moved) is excluded so a clip never snaps to itself. + fn snap_points(&self, dragged: Option, cx: &mut Context) -> Vec { + let source = self.source.read(cx); + let mut points = Vec::new(); + if let Some(area) = self.state.work_area { + points.push(SnapPoint { + frame: area.start, + kind: SnapKind::WorkAreaEdge, + }); + points.push(SnapPoint { + frame: area.end, + kind: SnapKind::WorkAreaEdge, + }); + } + for marker in source.markers() { + points.push(SnapPoint { + frame: marker.frame, + kind: SnapKind::Marker, + }); + } + points.push(SnapPoint { + frame: self.state.playhead, + kind: SnapKind::Playhead, + }); + for index in 0..source.track_count() { + if let Some(track) = source.track(index) { + for clip in track.clips() { + if clip.is_enabled() && Some(clip.id()) != dragged { + let range = clip.range(); + points.push(SnapPoint { + frame: range.start, + kind: SnapKind::ClipStart, + }); + points.push(SnapPoint { + frame: range.end, + kind: SnapKind::ClipEnd, + }); + } + } + } + } + points + } - /// Index of the track whose row contains screen `y` (relative to the - /// clip area's top). Falls back to the last track when below all rows. - fn track_at_y(&self, y: f32, cx: &mut Context) -> usize { - let source = self.source.read(cx); - let count = source.track_count(); - let mut acc = 0.0; - for index in 0..count { - if let Some(track) = source.track(index) { - acc += track.height().0.max(MIN_TRACK_HEIGHT); - if y < acc { - return index; - } - } - } - count.saturating_sub(1) - } + /// Index of the track whose row contains screen `y` (relative to the + /// clip area's top). Falls back to the last track when below all rows. + fn track_at_y(&self, y: f32, cx: &mut Context) -> usize { + let source = self.source.read(cx); + let count = source.track_count(); + let mut acc = 0.0; + for index in 0..count { + if let Some(track) = source.track(index) { + acc += track.height().0.max(MIN_TRACK_HEIGHT); + if y < acc { + return index; + } + } + } + count.saturating_sub(1) + } - /// Whether the track at `index` is locked. Out-of-range tracks count as - /// locked so drop requests onto them are rejected. - fn track_locked(&self, index: usize, cx: &mut Context) -> bool { - self.source - .read(cx) - .track(index) - .map(|track| track.is_locked()) - .unwrap_or(true) - } + /// Whether the track at `index` is locked. Out-of-range tracks count as + /// locked so drop requests onto them are rejected. + fn track_locked(&self, index: usize, cx: &mut Context) -> bool { + self.source + .read(cx) + .track(index) + .map(|track| track.is_locked()) + .unwrap_or(true) + } - /// Updates a clip-move drag from the pointer position: computes the new - /// start frame (with snapping) and the track under the cursor. - fn update_clip_drag( - &mut self, - event: &DragMoveEvent>>, - cx: &mut Context, - ) { - let drag = Arc::clone(event.drag(cx)); - let press = cx - .active_drag - .as_ref() - .map(|drag| drag.cursor_offset) - .unwrap_or_default(); - let now = event.event.position - event.bounds.origin; - let mut drag = drag.write().expect("clip drag lock is not poisoned"); - let wrapper_x = self.state.point_at_frame(drag.original_start).0; - let dx = now.x.0 - (wrapper_x + press.x.0); - let mut new_start = Frame(drag.original_start.0 + (dx / self.state.zoom).round() as i64); - let new_track = self.track_at_y(now.y.0, cx); - if self.state.snap_enabled { - if let Some(result) = snap( - new_start, - self.snap_points(Some(drag.clip), cx).into_iter(), - px(SNAP_THRESHOLD_PX), - self.state.zoom, - ) { - new_start = result.frame; - } - } - drag.new_start = new_start; - drag.new_track = new_track; - } + /// Updates a clip-move drag from the pointer position: computes the new + /// start frame (with snapping) and the track under the cursor. + fn update_clip_drag( + &mut self, + event: &DragMoveEvent>>, + cx: &mut Context, + ) { + let drag = Arc::clone(event.drag(cx)); + let press = cx + .active_drag + .as_ref() + .map(|drag| drag.cursor_offset) + .unwrap_or_default(); + let now = event.event.position - event.bounds.origin; + let mut drag = drag.write().expect("clip drag lock is not poisoned"); + let wrapper_x = self.state.point_at_frame(drag.original_start).0; + let dx = now.x.0 - (wrapper_x + press.x.0); + let mut new_start = Frame(drag.original_start.0 + (dx / self.state.zoom).round() as i64); + let new_track = self.track_at_y(now.y.0, cx); + if self.state.snap_enabled { + if let Some(result) = snap( + new_start, + self.snap_points(Some(drag.clip), cx).into_iter(), + px(SNAP_THRESHOLD_PX), + self.state.zoom, + ) { + new_start = result.frame; + } + } + drag.new_start = new_start; + drag.new_track = new_track; + } - /// Updates a trim drag from the pointer position: computes the new - /// position of the grabbed edge, clamped to keep the clip non-empty and - /// inside the sequence, then snapped and re-clamped. - fn update_trim_drag( - &mut self, - event: &DragMoveEvent>>, - cx: &mut Context, - ) { - let drag = Arc::clone(event.drag(cx)); - let press = cx - .active_drag - .as_ref() - .map(|drag| drag.cursor_offset) - .unwrap_or_default(); - let now = event.event.position - event.bounds.origin; - let seq_len = self.sequence_length(cx); - let mut drag = drag.write().expect("trim drag lock is not poisoned"); - let range = self.clip_range(drag.clip, cx); - let handle_x = match drag.edge { - TrimEdge::Start => now.x.0 - press.x.0, - TrimEdge::End => now.x.0 - press.x.0 + TRIM_HANDLE_WIDTH, - }; - let mut new_frame = self.state.frame_at_point(px(handle_x)); - let clamp = |frame: Frame| match drag.edge { - TrimEdge::Start => frame.max(Frame::ZERO).min(Frame((range.end.0 - 1).max(0))), - TrimEdge::End => frame - .max(Frame((range.start.0 + 1).min(seq_len.0))) - .min(seq_len), - }; - new_frame = clamp(new_frame); - if self.state.snap_enabled { - if let Some(result) = snap( - new_frame, - self.snap_points(Some(drag.clip), cx).into_iter(), - px(SNAP_THRESHOLD_PX), - self.state.zoom, - ) { - new_frame = clamp(result.frame); - } - } - drag.new_frame = new_frame; - } + /// Updates a trim drag from the pointer position: computes the new + /// position of the grabbed edge, clamped to keep the clip non-empty and + /// inside the sequence, then snapped and re-clamped. + fn update_trim_drag( + &mut self, + event: &DragMoveEvent>>, + cx: &mut Context, + ) { + let drag = Arc::clone(event.drag(cx)); + let press = cx + .active_drag + .as_ref() + .map(|drag| drag.cursor_offset) + .unwrap_or_default(); + let now = event.event.position - event.bounds.origin; + let seq_len = self.sequence_length(cx); + let mut drag = drag.write().expect("trim drag lock is not poisoned"); + let range = self.clip_range(drag.clip, cx); + let handle_x = match drag.edge { + TrimEdge::Start => now.x.0 - press.x.0, + TrimEdge::End => now.x.0 - press.x.0 + TRIM_HANDLE_WIDTH, + }; + let mut new_frame = self.state.frame_at_point(px(handle_x)); + let clamp = |frame: Frame| match drag.edge { + TrimEdge::Start => frame.max(Frame::ZERO).min(Frame((range.end.0 - 1).max(0))), + TrimEdge::End => frame + .max(Frame((range.start.0 + 1).min(seq_len.0))) + .min(seq_len), + }; + new_frame = clamp(new_frame); + if self.state.snap_enabled { + if let Some(result) = snap( + new_frame, + self.snap_points(Some(drag.clip), cx).into_iter(), + px(SNAP_THRESHOLD_PX), + self.state.zoom, + ) { + new_frame = clamp(result.frame); + } + } + drag.new_frame = new_frame; + } - /// Updates a track-height drag from the pointer position. - fn update_height_drag( - &mut self, - event: &DragMoveEvent>>, - cx: &mut Context, - ) { - let drag = Arc::clone(event.drag(cx)); - let press = cx - .active_drag - .as_ref() - .map(|drag| drag.cursor_offset) - .unwrap_or_default(); - let now = event.event.position - event.bounds.origin; - let mut drag = drag.write().expect("height drag lock is not poisoned"); - let dy = now.y.0 - (press.y.0 + drag.separator_y); - drag.new_height = px((drag.start_height.0 + dy).max(MIN_TRACK_HEIGHT)); - } + /// Updates a track-height drag from the pointer position. + fn update_height_drag( + &mut self, + event: &DragMoveEvent>>, + cx: &mut Context, + ) { + let drag = Arc::clone(event.drag(cx)); + let press = cx + .active_drag + .as_ref() + .map(|drag| drag.cursor_offset) + .unwrap_or_default(); + let now = event.event.position - event.bounds.origin; + let mut drag = drag.write().expect("height drag lock is not poisoned"); + let dy = now.y.0 - (press.y.0 + drag.separator_y); + drag.new_height = px((drag.start_height.0 + dy).max(MIN_TRACK_HEIGHT)); + } - /// Updates a marquee selection from the pointer position: hit-tests the - /// rows intersecting the rubber-band rect and selects the clips inside. - fn update_marquee( - &mut self, - event: &DragMoveEvent, - rows: &[RowData], - cx: &mut Context, - ) { - let press = cx - .active_drag - .as_ref() - .map(|drag| drag.cursor_offset) - .unwrap_or_default(); - let now = event.event.position - event.bounds.origin; - let mut frame_start = self.state.frame_at_point(px(press.x.0)); - let mut frame_end = self.state.frame_at_point(px(now.x.0)); - if frame_start > frame_end { - std::mem::swap(&mut frame_start, &mut frame_end); - } - let y0 = press.y.min(now.y).0; - let y1 = press.y.max(now.y).0; - let mut ids = BTreeSet::new(); - for row in rows { - if y1 >= row.y && y0 <= row.y + row.height { - for clip in &row.clips { - if clip.enabled - && clip.range.end.0 > frame_start.0 - && clip.range.start.0 < frame_end.0 - { - ids.insert(clip.id); - } - } - } - } - self.state.select_range(ids); - } + /// Updates a marquee selection from the pointer position: hit-tests the + /// rows intersecting the rubber-band rect and selects the clips inside. + fn update_marquee( + &mut self, + event: &DragMoveEvent, + rows: &[RowData], + cx: &mut Context, + ) { + let press = cx + .active_drag + .as_ref() + .map(|drag| drag.cursor_offset) + .unwrap_or_default(); + let now = event.event.position - event.bounds.origin; + let mut frame_start = self.state.frame_at_point(px(press.x.0)); + let mut frame_end = self.state.frame_at_point(px(now.x.0)); + if frame_start > frame_end { + std::mem::swap(&mut frame_start, &mut frame_end); + } + let y0 = press.y.min(now.y).0; + let y1 = press.y.max(now.y).0; + let mut ids = BTreeSet::new(); + for row in rows { + if y1 >= row.y && y0 <= row.y + row.height { + for clip in &row.clips { + if clip.enabled + && clip.range.end.0 > frame_start.0 + && clip.range.start.0 < frame_end.0 + { + ids.insert(clip.id); + } + } + } + } + self.state.select_range(ids); + } - /// Emits [`TimelineEvent::ClipMoveRequested`] for a finished clip move, - /// unless the gesture didn't move the clip or a locked track was involved. - fn finish_clip_drag(&mut self, drag: &Arc>, cx: &mut Context) { - let (clip, original_start, original_track, new_start, new_track) = { - let drag = drag.read().expect("clip drag lock is not poisoned"); - ( - drag.clip, - drag.original_start, - drag.original_track, - drag.new_start, - drag.new_track, - ) - }; - if (new_start, new_track) != (original_start, original_track) - && !self.track_locked(original_track, cx) - && !self.track_locked(new_track, cx) - { - cx.emit(TimelineEvent::ClipMoveRequested { - clip, - new_track, - new_start, - }); - cx.notify(); - } - } + /// Emits [`TimelineEvent::ClipMoveRequested`] for a finished clip move, + /// unless the gesture didn't move the clip or a locked track was involved. + fn finish_clip_drag(&mut self, drag: &Arc>, cx: &mut Context) { + let (clip, original_start, original_track, new_start, new_track) = { + let drag = drag.read().expect("clip drag lock is not poisoned"); + ( + drag.clip, + drag.original_start, + drag.original_track, + drag.new_start, + drag.new_track, + ) + }; + if (new_start, new_track) != (original_start, original_track) + && !self.track_locked(original_track, cx) + && !self.track_locked(new_track, cx) + { + cx.emit(TimelineEvent::ClipMoveRequested { + clip, + new_track, + new_start, + }); + cx.notify(); + } + } - /// Emits [`TimelineEvent::ClipTrimRequested`] for a finished trim. - fn finish_trim_drag(&mut self, drag: &Arc>, cx: &mut Context) { - let (clip, edge, original_frame, new_frame) = { - let drag = drag.read().expect("trim drag lock is not poisoned"); - (drag.clip, drag.edge, drag.original_frame, drag.new_frame) - }; - if new_frame != original_frame { - cx.emit(TimelineEvent::ClipTrimRequested { - clip, - edge, - new_frame, - }); - cx.notify(); - } - } + /// Emits [`TimelineEvent::ClipTrimRequested`] for a finished trim. + fn finish_trim_drag(&mut self, drag: &Arc>, cx: &mut Context) { + let (clip, edge, original_frame, new_frame) = { + let drag = drag.read().expect("trim drag lock is not poisoned"); + (drag.clip, drag.edge, drag.original_frame, drag.new_frame) + }; + if new_frame != original_frame { + cx.emit(TimelineEvent::ClipTrimRequested { + clip, + edge, + new_frame, + }); + cx.notify(); + } + } - /// Apply a transition-resize drag move: scale the requested length by the - /// horizontal mouse delta and clamp to the clip length. - fn update_transition_drag( - &mut self, - event: &DragMoveEvent>>, - cx: &mut Context, - ) { - let drag = Arc::clone(event.drag(cx)); - let press = cx - .active_drag - .as_ref() - .map(|drag| drag.cursor_offset) - .unwrap_or_default(); - let now = event.event.position - event.bounds.origin; - let dx = now.x - press.x; - let clip_len = { - let range = self.clip_range(drag.read().unwrap().clip, cx); - range.end - range.start - }; - let mut drag = drag.write().expect("transition drag lock is not poisoned"); - let frames_per_pixel = 1.0 / self.state.zoom as f64; - let delta = (dx.0 as f64 * frames_per_pixel).round() as i64; - let requested = drag.original_length.0 + delta; - drag.new_length = Frame(requested.clamp(1, clip_len.0.max(1))); - cx.notify(); - } + /// Apply a transition-resize drag move: scale the requested length by the + /// horizontal mouse delta and clamp to the clip length. + fn update_transition_drag( + &mut self, + event: &DragMoveEvent>>, + cx: &mut Context, + ) { + let drag = Arc::clone(event.drag(cx)); + let press = cx + .active_drag + .as_ref() + .map(|drag| drag.cursor_offset) + .unwrap_or_default(); + let now = event.event.position - event.bounds.origin; + let dx = now.x - press.x; + let clip_len = { + let range = self.clip_range(drag.read().unwrap().clip, cx); + range.end - range.start + }; + let mut drag = drag.write().expect("transition drag lock is not poisoned"); + let frames_per_pixel = 1.0 / self.state.zoom as f64; + let delta = (dx.0 as f64 * frames_per_pixel).round() as i64; + let requested = drag.original_length.0 + delta; + drag.new_length = Frame(requested.clamp(1, clip_len.0.max(1))); + cx.notify(); + } - /// Emits [`TimelineEvent::TransitionChanged`] for a finished resize. - fn finish_transition_drag(&mut self, drag: &Arc>, cx: &mut Context) { - let (clip, edge, original_length, new_length) = { - let drag = drag.read().expect("transition drag lock is not poisoned"); - (drag.clip, drag.edge, drag.original_length, drag.new_length) - }; - if new_length != original_length { - cx.emit(TimelineEvent::TransitionChanged { - clip, - edge, - new_length, - }); - cx.notify(); - } - } + /// Emits [`TimelineEvent::TransitionChanged`] for a finished resize. + fn finish_transition_drag( + &mut self, + drag: &Arc>, + cx: &mut Context, + ) { + let (clip, edge, original_length, new_length) = { + let drag = drag.read().expect("transition drag lock is not poisoned"); + (drag.clip, drag.edge, drag.original_length, drag.new_length) + }; + if new_length != original_length { + cx.emit(TimelineEvent::TransitionChanged { + clip, + edge, + new_length, + }); + cx.notify(); + } + } - /// Emits [`TimelineEvent::TrackHeightChanged`] for a finished resize. - fn finish_height_drag(&mut self, drag: &Arc>, cx: &mut Context) { - let (track, start_height, new_height) = { - let drag = drag.read().expect("height drag lock is not poisoned"); - (drag.track, drag.start_height, drag.new_height) - }; - if new_height.0 != start_height.0 { - cx.emit(TimelineEvent::TrackHeightChanged { track, height: new_height }); - cx.notify(); - } - } + /// Emits [`TimelineEvent::TrackHeightChanged`] for a finished resize. + fn finish_height_drag(&mut self, drag: &Arc>, cx: &mut Context) { + let (track, start_height, new_height) = { + let drag = drag.read().expect("height drag lock is not poisoned"); + (drag.track, drag.start_height, drag.new_height) + }; + if new_height.0 != start_height.0 { + cx.emit(TimelineEvent::TrackHeightChanged { + track, + height: new_height, + }); + cx.notify(); + } + } - /// Emits [`TimelineEvent::SelectionChanged`] for a finished marquee. - fn finish_marquee(&mut self, cx: &mut Context) { - cx.emit(TimelineEvent::SelectionChanged); - cx.notify(); - } + /// Emits [`TimelineEvent::SelectionChanged`] for a finished marquee. + fn finish_marquee(&mut self, cx: &mut Context) { + cx.emit(TimelineEvent::SelectionChanged); + cx.notify(); + } - // --- interaction handlers (private) --- - // - // The gesture logic lives in the render method's inline listeners and - // the `update_*` / `finish_*` helpers above; these signatures are - // retained (with `#[allow(dead_code)]`) as documentation of the - // gesture set the view wires up. + // --- interaction handlers (private) --- + // + // The gesture logic lives in the render method's inline listeners and + // the `update_*` / `finish_*` helpers above; these signatures are + // retained (with `#[allow(dead_code)]`) as documentation of the + // gesture set the view wires up. - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_ruler_mouse_down(&mut self, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_ruler_mouse_down(&mut self, _window: &mut Window, _cx: &mut Context) {} - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_clip_drag_move(&mut self, _clip: ClipId, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_clip_drag_move(&mut self, _clip: ClipId, _window: &mut Window, _cx: &mut Context) {} - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_clip_drag_drop(&mut self, _clip: ClipId, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_clip_drag_drop(&mut self, _clip: ClipId, _window: &mut Window, _cx: &mut Context) {} - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_trim_drag(&mut self, _clip: ClipId, _edge: TrimEdge, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_trim_drag( + &mut self, + _clip: ClipId, + _edge: TrimEdge, + _window: &mut Window, + _cx: &mut Context, + ) { + } - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_marquee(&mut self, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_marquee(&mut self, _window: &mut Window, _cx: &mut Context) {} - #[allow(dead_code)] // gesture documentation; see the section comment above - fn on_scroll(&mut self, _window: &mut Window, _cx: &mut Context) {} + #[allow(dead_code)] // gesture documentation; see the section comment above + fn on_scroll(&mut self, _window: &mut Window, _cx: &mut Context) {} } impl Focusable for TimelineView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl EventEmitter for TimelineView {} impl Render for TimelineView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let state = self.state.clone(); - let source = self.source.read(cx); - let frame_rate = source.frame_rate(); - let seq_len = source.sequence_length(); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let state = self.state.clone(); + let source = self.source.read(cx); + let frame_rate = source.frame_rate(); + let seq_len = source.sequence_length(); - // Snapshot the rows: one per track, stacked from the top of the clip - // area, with y positions accumulated from the model's track heights. - let mut rows: Vec = Vec::new(); - let mut y = 0.0; - for index in 0..source.track_count() { - if let Some(track) = source.track(index) { - let height = track.height().0.max(MIN_TRACK_HEIGHT); - let clips = track - .clips() - .iter() - .map(|clip| { - let color = clip.color().unwrap_or_else(|| kind_color(track.kind())); - let in_transition = clip - .in_transition() - .filter(|duration| duration.0 > 0) - .map(|duration| FrameRange::new(Frame::ZERO, duration)); - let out_transition = clip - .out_transition() - .filter(|duration| duration.0 > 0) - .map(|duration| FrameRange::new(Frame::ZERO, duration)); - ClipRenderData { - id: clip.id(), - range: clip.range(), - label: clip.label(), - color, - enabled: clip.is_enabled(), - in_transition, - out_transition, - } - }) - .collect(); - rows.push(RowData { - index, - name: track.name(), - kind: track.kind(), - height, - y, - locked: track.is_locked(), - muted: track.is_muted(), - solo: track.is_solo(), - visible: track.is_visible(), - clips, - }); - y += height; - } - } + // Snapshot the rows: one per track, stacked from the top of the clip + // area, with y positions accumulated from the model's track heights. + let mut rows: Vec = Vec::new(); + let mut y = 0.0; + for index in 0..source.track_count() { + if let Some(track) = source.track(index) { + let height = track.height().0.max(MIN_TRACK_HEIGHT); + let clips = track + .clips() + .iter() + .map(|clip| { + let color = clip.color().unwrap_or_else(|| kind_color(track.kind())); + let in_transition = clip + .in_transition() + .filter(|duration| duration.0 > 0) + .map(|duration| FrameRange::new(Frame::ZERO, duration)); + let out_transition = clip + .out_transition() + .filter(|duration| duration.0 > 0) + .map(|duration| FrameRange::new(Frame::ZERO, duration)); + ClipRenderData { + id: clip.id(), + range: clip.range(), + label: clip.label(), + color, + enabled: clip.is_enabled(), + in_transition, + out_transition, + } + }) + .collect(); + rows.push(RowData { + index, + name: track.name(), + kind: track.kind(), + height, + y, + locked: track.is_locked(), + muted: track.is_muted(), + solo: track.is_solo(), + visible: track.is_visible(), + clips, + }); + y += height; + } + } - // The marquee handler needs the row geometry; keep a snapshot for it - // (the clip-area child iterator consumes `rows` below). - let marquee_rows = Arc::new(rows.clone()); - let playhead_x = state.point_at_frame(state.playhead).0; - let decorator: Arc> = Arc::new(RwLock::new(NoopClipDecorator)); + // The marquee handler needs the row geometry; keep a snapshot for it + // (the clip-area child iterator consumes `rows` below). + let marquee_rows = Arc::new(rows.clone()); + let playhead_x = state.point_at_frame(state.playhead).0; + let decorator: Arc> = Arc::new(RwLock::new(NoopClipDecorator)); - let ruler = div() - .flex_row() - .h(px(32.)) - .flex_shrink_0() - .child(div().w(px(HEADER_WIDTH)).flex_shrink_0()) - .child( - div() - .flex_1() - .h_full() - .id("timeline-ruler") - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - // The ruler's left edge aligns with the clip - // area's, i.e. HEADER_WIDTH px from the window's - // left edge (this widget is expected to sit at - // window x = 0). - let frame = - this.state.frame_at_point(event.position.x - px(HEADER_WIDTH)); - this.seek(frame, cx); - }), - ) - .child(TimelineRuler::new(state.clone(), frame_rate, seq_len)), - ); + let ruler = div() + .flex_row() + .h(px(32.)) + .flex_shrink_0() + .child(div().w(px(HEADER_WIDTH)).flex_shrink_0()) + .child( + div() + .flex_1() + .h_full() + .id("timeline-ruler") + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + // The ruler's left edge aligns with the clip + // area's, i.e. HEADER_WIDTH px from the window's + // left edge (this widget is expected to sit at + // window x = 0). + let frame = this + .state + .frame_at_point(event.position.x - px(HEADER_WIDTH)); + this.seek(frame, cx); + }), + ) + .child(TimelineRuler::new(state.clone(), frame_rate, seq_len)), + ); - let headers = div() - .w(px(HEADER_WIDTH)) - .flex_shrink_0() - .flex_col() - .id("timeline-track-headers") - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - this.update_height_drag(event, cx); - }, - ), - ) - .on_drop( - cx.listener(|this, drag: &Arc>, _window, cx| { - this.finish_height_drag(drag, cx); - }), - ) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - this.update_transition_drag(event, cx); - }, - ), - ) - .on_drop( - cx.listener(|this, drag: &Arc>, _window, cx| { - this.finish_transition_drag(drag, cx); - }), - ) - .children(rows.iter().map(|row| { - let height = row.height; - let separator_y = row.y + height - TrackHeader::SEPARATOR_HEIGHT; - div() - .h(px(height)) - .relative() - .child( - div() - .id(ElementId::named_usize("timeline-track-header", row.index)) - .h(px(height - TrackHeader::SEPARATOR_HEIGHT)) - .cursor_pointer() - .on_click({ - let track = row.index; - cx.listener(move |this, _event: &crate::ClickEvent, _window, cx| { - let selected = if this.selected_tracks.contains(&track) { - this.selected_tracks.remove(&track); - false - } else { - this.selected_tracks.insert(track); - true - }; - cx.emit(TimelineEvent::TrackSelected { - track, - selected, - }); - cx.notify(); - }) - }) - .child( - TrackHeader::new(row.index, row.name.clone(), row.kind) - .locked(row.locked) - .muted(row.muted) - .solo(row.solo) - .visible(row.visible), - ), - ) - .child( - div() - .absolute() - .left(px(0.)) - .right(px(0.)) - .bottom(px(0.)) - .h(px(TrackHeader::SEPARATOR_HEIGHT)) - .id(ElementId::named_usize("timeline-track-resize", row.index)) - .cursor_row_resize() - .on_drag( - Arc::new(RwLock::new(HeightDrag { - track: row.index, - start_height: px(height), - new_height: px(height), - separator_y, - })), - drag_ghost, - ), - ) - })); + let headers = div() + .w(px(HEADER_WIDTH)) + .flex_shrink_0() + .flex_col() + .id("timeline-track-headers") + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + this.update_height_drag(event, cx); + }, + )) + .on_drop( + cx.listener(|this, drag: &Arc>, _window, cx| { + this.finish_height_drag(drag, cx); + }), + ) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + this.update_transition_drag(event, cx); + }, + )) + .on_drop( + cx.listener(|this, drag: &Arc>, _window, cx| { + this.finish_transition_drag(drag, cx); + }), + ) + .children(rows.iter().map(|row| { + let height = row.height; + let separator_y = row.y + height - TrackHeader::SEPARATOR_HEIGHT; + div() + .h(px(height)) + .relative() + .child( + div() + .id(ElementId::named_usize("timeline-track-header", row.index)) + .h(px(height - TrackHeader::SEPARATOR_HEIGHT)) + .cursor_pointer() + .on_click({ + let track = row.index; + cx.listener(move |this, _event: &crate::ClickEvent, _window, cx| { + let selected = if this.selected_tracks.contains(&track) { + this.selected_tracks.remove(&track); + false + } else { + this.selected_tracks.insert(track); + true + }; + cx.emit(TimelineEvent::TrackSelected { track, selected }); + cx.notify(); + }) + }) + .child( + TrackHeader::new(row.index, row.name.clone(), row.kind) + .locked(row.locked) + .muted(row.muted) + .solo(row.solo) + .visible(row.visible), + ), + ) + .child( + div() + .absolute() + .left(px(0.)) + .right(px(0.)) + .bottom(px(0.)) + .h(px(TrackHeader::SEPARATOR_HEIGHT)) + .id(ElementId::named_usize("timeline-track-resize", row.index)) + .cursor_row_resize() + .on_drag( + Arc::new(RwLock::new(HeightDrag { + track: row.index, + start_height: px(height), + new_height: px(height), + separator_y, + })), + drag_ghost, + ), + ) + })); - let clip_area = div() - .flex_1() - .h_full() - .id("timeline-clip-area") - .relative() - .overflow_hidden() - .flex_col() - .on_scroll_wheel( - cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { - if event.modifiers.control || event.modifiers.platform { - let anchor = event.position.x - px(HEADER_WIDTH); - let factor = match event.delta { - ScrollDelta::Pixels(delta) => 1.0 + delta.y.0 * 0.01, - ScrollDelta::Lines(delta) => 1.0 + delta.y * 0.01, - } - .clamp(0.5, 2.0); - let old = this.state.zoom; - this.state.set_zoom(old * factor, anchor); - if (this.state.zoom - old).abs() > f32::EPSILON { - cx.emit(TimelineEvent::ZoomChanged(this.state.zoom)); - } - } else { - let dx = match event.delta { - ScrollDelta::Pixels(delta) => delta.y.0, - ScrollDelta::Lines(delta) => delta.y * 24.0, - }; - this.state.scroll_offset.x = px((this.state.scroll_offset.x.0 + dx).max(0.0)); - } - cx.notify(); - }), - ) - .on_pinch( - cx.listener(|this, event: &PinchEvent, _window, cx| { - let anchor = event.position.x - px(HEADER_WIDTH); - let old = this.state.zoom; - this.state.set_zoom(old * (1.0 + event.delta), anchor); - if (this.state.zoom - old).abs() > f32::EPSILON { - cx.emit(TimelineEvent::ZoomChanged(this.state.zoom)); - } - cx.notify(); - }), - ) - .on_drag(MarqueeDrag, drag_ghost) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - this.update_clip_drag(event, cx); - }, - ), - ) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - this.update_trim_drag(event, cx); - }, - ), - ) - .on_drag_move({ - cx.listener( - move |this, event: &DragMoveEvent, _window, cx| { - this.update_marquee(event, marquee_rows.as_slice(), cx); - }, - ) - }) - .on_drop( - cx.listener(|this, drag: &Arc>, _window, cx| { - this.finish_clip_drag(drag, cx); - }), - ) - .on_drop( - cx.listener(|this, drag: &Arc>, _window, cx| { - this.finish_trim_drag(drag, cx); - }), - ) - .on_drop( - cx.listener(|this, _drag: &MarqueeDrag, _window, cx| { - this.finish_marquee(cx); - }), - ) - .on_drop( - cx.listener(|this, drag: &Arc>, _window, cx| { - this.finish_height_drag(drag, cx); - }), - ) - .children(rows.into_iter().map(move |row| { - let height = row.height; - let row_locked = row.locked; - let kind = row.kind; - let row_index = row.index; - let state = &state; - let decorator = &decorator; - div() - .h(px(height)) - .relative() - .children(row.clips.into_iter().map(move |clip| { - let x0 = state.point_at_frame(clip.range.start).0; - let x1 = state.point_at_frame(clip.range.end).0; - let width = (x1 - x0).max(1.0); - let clip_height = (height - TrackHeader::SEPARATOR_HEIGHT).max(1.0); - let mut children: Vec = vec![ - ClipElement::new( - clip.id, - clip.label.clone(), - clip.color, - clip.in_transition, - clip.out_transition, - decorator.clone(), - ) - .selected(state.is_selected(clip.id)) - .enabled(clip.enabled) - .locked(row_locked) - .content(match kind { - TrackKind::Video => ClipContent::Thumbnails, - TrackKind::Audio => ClipContent::Waveform, - TrackKind::Subtitle => ClipContent::None, - }) - .into_any_element(), - ]; - if !row_locked { - children.push( - div() - .absolute() - .left(px(0.)) - .top(px(0.)) - .bottom(px(0.)) - .w(px(TRIM_HANDLE_WIDTH)) - .id(ElementId::named_usize( - "timeline-trim-start", - clip.id.0 as usize, - )) - .cursor_ew_resize() - .on_drag( - Arc::new(RwLock::new(TrimDrag { - clip: clip.id, - edge: TrimEdge::Start, - original_frame: clip.range.start, - new_frame: clip.range.start, - })), - drag_ghost, - ) - .into_any_element(), - ); - children.push( - div() - .absolute() - .right(px(0.)) - .top(px(0.)) - .bottom(px(0.)) - .w(px(TRIM_HANDLE_WIDTH)) - .id(ElementId::named_usize( - "timeline-trim-end", - clip.id.0 as usize, - )) - .cursor_ew_resize() - .on_drag( - Arc::new(RwLock::new(TrimDrag { - clip: clip.id, - edge: TrimEdge::End, - original_frame: clip.range.end, - new_frame: clip.range.end, - })), - drag_ghost, - ) - .into_any_element(), - ); - } + let clip_area = div() + .flex_1() + .h_full() + .id("timeline-clip-area") + .relative() + .overflow_hidden() + .flex_col() + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { + if event.modifiers.control || event.modifiers.platform { + let anchor = event.position.x - px(HEADER_WIDTH); + let factor = match event.delta { + ScrollDelta::Pixels(delta) => 1.0 + delta.y.0 * 0.01, + ScrollDelta::Lines(delta) => 1.0 + delta.y * 0.01, + } + .clamp(0.5, 2.0); + let old = this.state.zoom; + this.state.set_zoom(old * factor, anchor); + if (this.state.zoom - old).abs() > f32::EPSILON { + cx.emit(TimelineEvent::ZoomChanged(this.state.zoom)); + } + } else { + let dx = match event.delta { + ScrollDelta::Pixels(delta) => delta.y.0, + ScrollDelta::Lines(delta) => delta.y * 24.0, + }; + this.state.scroll_offset.x = px((this.state.scroll_offset.x.0 + dx).max(0.0)); + } + cx.notify(); + })) + .on_pinch(cx.listener(|this, event: &PinchEvent, _window, cx| { + let anchor = event.position.x - px(HEADER_WIDTH); + let old = this.state.zoom; + this.state.set_zoom(old * (1.0 + event.delta), anchor); + if (this.state.zoom - old).abs() > f32::EPSILON { + cx.emit(TimelineEvent::ZoomChanged(this.state.zoom)); + } + cx.notify(); + })) + .on_drag(MarqueeDrag, drag_ghost) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + this.update_clip_drag(event, cx); + }, + )) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + this.update_trim_drag(event, cx); + }, + )) + .on_drag_move({ + cx.listener( + move |this, event: &DragMoveEvent, _window, cx| { + this.update_marquee(event, marquee_rows.as_slice(), cx); + }, + ) + }) + .on_drop( + cx.listener(|this, drag: &Arc>, _window, cx| { + this.finish_clip_drag(drag, cx); + }), + ) + .on_drop( + cx.listener(|this, drag: &Arc>, _window, cx| { + this.finish_trim_drag(drag, cx); + }), + ) + .on_drop(cx.listener(|this, _drag: &MarqueeDrag, _window, cx| { + this.finish_marquee(cx); + })) + .on_drop( + cx.listener(|this, drag: &Arc>, _window, cx| { + this.finish_height_drag(drag, cx); + }), + ) + .children(rows.into_iter().map(move |row| { + let height = row.height; + let row_locked = row.locked; + let kind = row.kind; + let row_index = row.index; + let state = &state; + let decorator = &decorator; + div() + .h(px(height)) + .relative() + .children(row.clips.into_iter().map(move |clip| { + let x0 = state.point_at_frame(clip.range.start).0; + let x1 = state.point_at_frame(clip.range.end).0; + let width = (x1 - x0).max(1.0); + let clip_height = (height - TrackHeader::SEPARATOR_HEIGHT).max(1.0); + let mut children: Vec = vec![ + ClipElement::new( + clip.id, + clip.label.clone(), + clip.color, + clip.in_transition, + clip.out_transition, + decorator.clone(), + ) + .selected(state.is_selected(clip.id)) + .enabled(clip.enabled) + .locked(row_locked) + .content(match kind { + TrackKind::Video => ClipContent::Thumbnails, + TrackKind::Audio => ClipContent::Waveform, + TrackKind::Subtitle => ClipContent::None, + }) + .into_any_element(), + ]; + if !row_locked { + children.push( + div() + .absolute() + .left(px(0.)) + .top(px(0.)) + .bottom(px(0.)) + .w(px(TRIM_HANDLE_WIDTH)) + .id(ElementId::named_usize( + "timeline-trim-start", + clip.id.0 as usize, + )) + .cursor_ew_resize() + .on_drag( + Arc::new(RwLock::new(TrimDrag { + clip: clip.id, + edge: TrimEdge::Start, + original_frame: clip.range.start, + new_frame: clip.range.start, + })), + drag_ghost, + ) + .into_any_element(), + ); + children.push( + div() + .absolute() + .right(px(0.)) + .top(px(0.)) + .bottom(px(0.)) + .w(px(TRIM_HANDLE_WIDTH)) + .id(ElementId::named_usize( + "timeline-trim-end", + clip.id.0 as usize, + )) + .cursor_ew_resize() + .on_drag( + Arc::new(RwLock::new(TrimDrag { + clip: clip.id, + edge: TrimEdge::End, + original_frame: clip.range.end, + new_frame: clip.range.end, + })), + drag_ghost, + ) + .into_any_element(), + ); + } - // Transition-resize handles sit at the wedges' edges. - if let Some(transition) = clip.in_transition { - children.push( - div() - .absolute() - .left(px(0.)) - .top(px(0.)) - .bottom(px(0.)) - .w(px(TRIM_HANDLE_WIDTH)) - .id(ElementId::named_usize( - "timeline-transition-handle", - clip.id.0 as usize, - )) - .cursor_ew_resize() - .on_drag( - Arc::new(RwLock::new(TransitionDrag { - clip: clip.id, - edge: TrimEdge::Start, - original_length: transition.end, - new_length: transition.end, - })), - drag_ghost, - ) - .into_any_element(), - ); - } - if let Some(transition) = clip.out_transition { - children.push( - div() - .absolute() - .right(px(0.)) - .top(px(0.)) - .bottom(px(0.)) - .w(px(TRIM_HANDLE_WIDTH)) - .id(ElementId::named_usize( - "timeline-transition-handle", - clip.id.0 as usize, - )) - .cursor_ew_resize() - .on_drag( - Arc::new(RwLock::new(TransitionDrag { - clip: clip.id, - edge: TrimEdge::End, - original_length: transition.end, - new_length: transition.end, - })), - drag_ghost, - ) - .into_any_element(), - ); - } - div() - .absolute() - .left(px(x0)) - .top(px(0.)) - .w(px(width)) - .h(px(clip_height)) - .id(ElementId::named_usize("timeline-clip", clip.id.0 as usize)) - .on_drag( - Arc::new(RwLock::new(ClipDrag { - clip: clip.id, - original_start: clip.range.start, - original_track: row_index, - new_start: clip.range.start, - new_track: row_index, - })), - drag_ghost, - ) - .children(children) - })) - })); + // Transition-resize handles sit at the wedges' edges. + if let Some(transition) = clip.in_transition { + children.push( + div() + .absolute() + .left(px(0.)) + .top(px(0.)) + .bottom(px(0.)) + .w(px(TRIM_HANDLE_WIDTH)) + .id(ElementId::named_usize( + "timeline-transition-handle", + clip.id.0 as usize, + )) + .cursor_ew_resize() + .on_drag( + Arc::new(RwLock::new(TransitionDrag { + clip: clip.id, + edge: TrimEdge::Start, + original_length: transition.end, + new_length: transition.end, + })), + drag_ghost, + ) + .into_any_element(), + ); + } + if let Some(transition) = clip.out_transition { + children.push( + div() + .absolute() + .right(px(0.)) + .top(px(0.)) + .bottom(px(0.)) + .w(px(TRIM_HANDLE_WIDTH)) + .id(ElementId::named_usize( + "timeline-transition-handle", + clip.id.0 as usize, + )) + .cursor_ew_resize() + .on_drag( + Arc::new(RwLock::new(TransitionDrag { + clip: clip.id, + edge: TrimEdge::End, + original_length: transition.end, + new_length: transition.end, + })), + drag_ghost, + ) + .into_any_element(), + ); + } + div() + .absolute() + .left(px(x0)) + .top(px(0.)) + .w(px(width)) + .h(px(clip_height)) + .id(ElementId::named_usize("timeline-clip", clip.id.0 as usize)) + .on_drag( + Arc::new(RwLock::new(ClipDrag { + clip: clip.id, + original_start: clip.range.start, + original_track: row_index, + new_start: clip.range.start, + new_track: row_index, + })), + drag_ghost, + ) + .children(children) + })) + })); - let playhead = div() - .absolute() - .left(px(HEADER_WIDTH + playhead_x)) - .top(px(0.)) - .bottom(px(0.)) - .w(px(1.)) - .child(PlayheadElement::new(px(0.), playhead_color())); + let playhead = div() + .absolute() + .left(px(HEADER_WIDTH + playhead_x)) + .top(px(0.)) + .bottom(px(0.)) + .w(px(1.)) + .child(PlayheadElement::new(px(0.), playhead_color())); - div() - .size_full() - .flex() - .flex_col() - .child(ruler) - .child( - div() - .flex_row() - .flex_1() - .relative() - .child(headers) - .child(clip_area) - .child(playhead), - ) - } + div().size_full().flex().flex_col().child(ruler).child( + div() + .flex_row() + .flex_1() + .relative() + .child(headers) + .child(clip_area) + .child(playhead), + ) + } } /// Shared state for a clip-move gesture (the clip wrapper starts it, the /// clip area updates and finishes it). struct ClipDrag { - clip: ClipId, - original_start: Frame, - original_track: usize, - new_start: Frame, - new_track: usize, + clip: ClipId, + original_start: Frame, + original_track: usize, + new_start: Frame, + new_track: usize, } /// Shared state for a trim gesture; see [`TrimEdge`]. struct TrimDrag { - clip: ClipId, - edge: TrimEdge, - original_frame: Frame, - new_frame: Frame, + clip: ClipId, + edge: TrimEdge, + original_frame: Frame, + new_frame: Frame, } /// In-flight payload of a transition-resize drag. struct TransitionDrag { - clip: ClipId, - edge: TrimEdge, - original_length: Frame, - new_length: Frame, + clip: ClipId, + edge: TrimEdge, + original_length: Frame, + new_length: Frame, } /// Shared state for a track-height resize gesture. struct HeightDrag { - track: usize, - start_height: Pixels, - new_height: Pixels, - /// The separator strip's top edge, relative to the headers column's top. - separator_y: f32, + track: usize, + start_height: Pixels, + new_height: Pixels, + /// The separator strip's top edge, relative to the headers column's top. + separator_y: f32, } /// Marker type for a marquee (rubber-band) selection gesture. @@ -1134,59 +1128,59 @@ struct MarqueeDrag; /// Layout snapshot of one track row, captured during render. #[derive(Clone)] struct RowData { - index: usize, - name: SharedString, - kind: TrackKind, - height: f32, - y: f32, - locked: bool, - muted: bool, - solo: bool, - visible: bool, - clips: Vec, + index: usize, + name: SharedString, + kind: TrackKind, + height: f32, + y: f32, + locked: bool, + muted: bool, + solo: bool, + visible: bool, + clips: Vec, } /// Layout snapshot of one clip, captured during render. #[derive(Clone)] struct ClipRenderData { - id: ClipId, - range: FrameRange, - label: SharedString, - color: Hsla, - enabled: bool, - in_transition: Option, - out_transition: Option, + id: ClipId, + range: FrameRange, + label: SharedString, + color: Hsla, + enabled: bool, + in_transition: Option, + out_transition: Option, } /// The ghost rendered under the cursor during any timeline drag. struct DragPreview; impl Render for DragPreview { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().bg(hsla(0.6, 0.7, 0.9, 0.35)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().bg(hsla(0.6, 0.7, 0.9, 0.35)) + } } /// Builds the drag ghost view for a drag of any value type. fn drag_ghost( - _drag: &T, - _offset: Point, - _window: &mut Window, - cx: &mut App, + _drag: &T, + _offset: Point, + _window: &mut Window, + cx: &mut App, ) -> Entity { - cx.new(|_cx| DragPreview) + cx.new(|_cx| DragPreview) } /// The playhead line color. fn playhead_color() -> Hsla { - hsla(0.0, 0.0, 0.9, 0.9) + hsla(0.0, 0.0, 0.9, 0.9) } /// The default body color for clips on a track of `kind`. fn kind_color(kind: TrackKind) -> Hsla { - match kind { - TrackKind::Video => hsla(0.58, 0.45, 0.35, 1.0), - TrackKind::Audio => hsla(0.35, 0.45, 0.35, 1.0), - TrackKind::Subtitle => hsla(0.10, 0.45, 0.35, 1.0), - } + match kind { + TrackKind::Video => hsla(0.58, 0.45, 0.35, 1.0), + TrackKind::Audio => hsla(0.35, 0.45, 0.35, 1.0), + TrackKind::Subtitle => hsla(0.10, 0.45, 0.35, 1.0), + } } diff --git a/crates/gpui/src/timeline/track_header.rs b/crates/gpui/src/timeline/track_header.rs index cebb1c5d35..a391d2fce5 100644 --- a/crates/gpui/src/timeline/track_header.rs +++ b/crates/gpui/src/timeline/track_header.rs @@ -21,7 +21,7 @@ //! that turn a press into a [`TrackHeaderEvent`] are attached by //! [`TimelineView`](super::TimelineView)'s interactive wrapper. -use crate::{App, Hsla, SharedString, Window, div, hsla, px, prelude::*}; +use crate::{App, Hsla, SharedString, Window, div, hsla, prelude::*, px}; use super::data::TrackKind; @@ -32,14 +32,14 @@ use super::data::TrackKind; /// as undoable) and notifies the data source. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TrackHeaderEvent { - /// Lock toggle requested. Locked tracks reject all clip edits. - ToggleLock, - /// Mute toggle requested (audio tracks). - ToggleMute, - /// Solo toggle requested (audio tracks). - ToggleSolo, - /// Visibility (show) toggle requested (video/subtitle tracks). - ToggleVisibility, + /// Lock toggle requested. Locked tracks reject all clip edits. + ToggleLock, + /// Mute toggle requested (audio tracks). + ToggleMute, + /// Solo toggle requested (audio tracks). + ToggleSolo, + /// Visibility (show) toggle requested (video/subtitle tracks). + ToggleVisibility, } /// The header control for one track, rendered in the left column. @@ -48,149 +48,154 @@ pub enum TrackHeaderEvent { /// [`Self::SEPARATOR_HEIGHT`] pixels are the height-resize drag zone. #[derive(IntoElement)] pub struct TrackHeader { - index: usize, - name: SharedString, - kind: TrackKind, - locked: bool, - muted: bool, - solo: bool, - visible: bool, + index: usize, + name: SharedString, + kind: TrackKind, + locked: bool, + muted: bool, + solo: bool, + visible: bool, } impl TrackHeader { - /// Height of the drag separator zone at the header's bottom edge, in - /// pixels. - pub const SEPARATOR_HEIGHT: f32 = 4.0; + /// Height of the drag separator zone at the header's bottom edge, in + /// pixels. + pub const SEPARATOR_HEIGHT: f32 = 4.0; - /// Creates a header for the track at `index` from its - /// [`TrackData`](super::TrackData) snapshot. - pub fn new(index: usize, name: SharedString, kind: TrackKind) -> Self { - TrackHeader { - index, - name, - kind, - locked: false, - muted: false, - solo: false, - visible: true, - } - } + /// Creates a header for the track at `index` from its + /// [`TrackData`](super::TrackData) snapshot. + pub fn new(index: usize, name: SharedString, kind: TrackKind) -> Self { + TrackHeader { + index, + name, + kind, + locked: false, + muted: false, + solo: false, + visible: true, + } + } - /// Builder: current lock state (drives the lock button's active style). - pub fn locked(mut self, locked: bool) -> Self { - self.locked = locked; - self - } + /// Builder: current lock state (drives the lock button's active style). + pub fn locked(mut self, locked: bool) -> Self { + self.locked = locked; + self + } - /// Builder: current mute state. Only meaningful for - /// [`TrackKind::Audio`]; ignored otherwise. - pub fn muted(mut self, muted: bool) -> Self { - self.muted = muted; - self - } + /// Builder: current mute state. Only meaningful for + /// [`TrackKind::Audio`]; ignored otherwise. + pub fn muted(mut self, muted: bool) -> Self { + self.muted = muted; + self + } - /// Builder: current solo state. Only meaningful for - /// [`TrackKind::Audio`]; ignored otherwise. - pub fn solo(mut self, solo: bool) -> Self { - self.solo = solo; - self - } + /// Builder: current solo state. Only meaningful for + /// [`TrackKind::Audio`]; ignored otherwise. + pub fn solo(mut self, solo: bool) -> Self { + self.solo = solo; + self + } - /// Builder: current visibility (show) state. Only meaningful for - /// [`TrackKind::Video`] and [`TrackKind::Subtitle`]; ignored otherwise. - pub fn visible(mut self, visible: bool) -> Self { - self.visible = visible; - self - } + /// Builder: current visibility (show) state. Only meaningful for + /// [`TrackKind::Video`] and [`TrackKind::Subtitle`]; ignored otherwise. + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } - /// Index of the track this header controls. - pub fn track_index(&self) -> usize { - self.index - } + /// Index of the track this header controls. + pub fn track_index(&self) -> usize { + self.index + } - /// The background tint for a track of `kind`. - fn kind_background(kind: TrackKind) -> Hsla { - match kind { - TrackKind::Video => hsla(0.58, 0.45, 0.32, 0.18), - TrackKind::Audio => hsla(0.35, 0.45, 0.32, 0.18), - TrackKind::Subtitle => hsla(0.10, 0.45, 0.32, 0.18), - } - } + /// The background tint for a track of `kind`. + fn kind_background(kind: TrackKind) -> Hsla { + match kind { + TrackKind::Video => hsla(0.58, 0.45, 0.32, 0.18), + TrackKind::Audio => hsla(0.35, 0.45, 0.32, 0.18), + TrackKind::Subtitle => hsla(0.10, 0.45, 0.32, 0.18), + } + } - /// The label color for a track of `kind`. - fn kind_text(kind: TrackKind) -> Hsla { - match kind { - TrackKind::Video => hsla(0.58, 0.35, 0.85, 1.0), - TrackKind::Audio => hsla(0.35, 0.35, 0.85, 1.0), - TrackKind::Subtitle => hsla(0.10, 0.35, 0.85, 1.0), - } - } + /// The label color for a track of `kind`. + fn kind_text(kind: TrackKind) -> Hsla { + match kind { + TrackKind::Video => hsla(0.58, 0.35, 0.85, 1.0), + TrackKind::Audio => hsla(0.35, 0.35, 0.85, 1.0), + TrackKind::Subtitle => hsla(0.10, 0.35, 0.85, 1.0), + } + } - /// A small toggle glyph (one or two letters) reflecting `active`. - fn toggle_glyph(&self, label: &str, active: bool) -> impl IntoElement { - div() - .px_1() - .rounded(px(3.)) - .text_xs() - .font_weight(if active { - crate::FontWeight::BOLD - } else { - crate::FontWeight::NORMAL - }) - .text_color(if active { - hsla(0.63, 0.6, 0.65, 1.0) - } else { - hsla(0.0, 0.0, 0.5, 0.55) - }) - .child(label.to_string()) - } + /// A small toggle glyph (one or two letters) reflecting `active`. + fn toggle_glyph(&self, label: &str, active: bool) -> impl IntoElement { + div() + .px_1() + .rounded(px(3.)) + .text_xs() + .font_weight(if active { + crate::FontWeight::BOLD + } else { + crate::FontWeight::NORMAL + }) + .text_color(if active { + hsla(0.63, 0.6, 0.65, 1.0) + } else { + hsla(0.0, 0.0, 0.5, 0.55) + }) + .child(label.to_string()) + } - /// The kind-appropriate toggle glyphs, left of the separator. - fn toggle_row(&self) -> impl IntoElement { - let lock = self.toggle_glyph("L", self.locked); - match self.kind { - TrackKind::Audio => { - div().flex().flex_row().items_center().gap(px(3.)).child(lock).child( - self.toggle_glyph("M", self.muted), - ).child(self.toggle_glyph("S", self.solo)) - } - TrackKind::Video | TrackKind::Subtitle => { - div().flex().flex_row().items_center().gap(px(3.)).child(lock).child( - self.toggle_glyph("V", self.visible), - ) - } - } - } + /// The kind-appropriate toggle glyphs, left of the separator. + fn toggle_row(&self) -> impl IntoElement { + let lock = self.toggle_glyph("L", self.locked); + match self.kind { + TrackKind::Audio => div() + .flex() + .flex_row() + .items_center() + .gap(px(3.)) + .child(lock) + .child(self.toggle_glyph("M", self.muted)) + .child(self.toggle_glyph("S", self.solo)), + TrackKind::Video | TrackKind::Subtitle => div() + .flex() + .flex_row() + .items_center() + .gap(px(3.)) + .child(lock) + .child(self.toggle_glyph("V", self.visible)), + } + } } impl RenderOnce for TrackHeader { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let background = Self::kind_background(self.kind); - let text = Self::kind_text(self.kind); - let separator_height = px(TrackHeader::SEPARATOR_HEIGHT); + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let background = Self::kind_background(self.kind); + let text = Self::kind_text(self.kind); + let separator_height = px(TrackHeader::SEPARATOR_HEIGHT); - div() - .size_full() - .bg(background) - .flex() - .flex_col() - .child( - div() - .flex_1() - .flex() - .flex_row() - .items_center() - .gap(px(6.)) - .px_2() - .child(div().text_sm().text_color(text).child(self.name.clone())) - .child(div().flex_1()) - .child(self.toggle_row()), - ) - .child( - div() - .h(separator_height) - .w_full() - .bg(hsla(0.0, 0.0, 0.5, 0.25)), - ) - } + div() + .size_full() + .bg(background) + .flex() + .flex_col() + .child( + div() + .flex_1() + .flex() + .flex_row() + .items_center() + .gap(px(6.)) + .px_2() + .child(div().text_sm().text_color(text).child(self.name.clone())) + .child(div().flex_1()) + .child(self.toggle_row()), + ) + .child( + div() + .h(separator_height) + .w_full() + .bg(hsla(0.0, 0.0, 0.5, 0.25)), + ) + } } diff --git a/crates/gpui/src/transition.rs b/crates/gpui/src/transition.rs index 1e62a53b08..db67773638 100644 --- a/crates/gpui/src/transition.rs +++ b/crates/gpui/src/transition.rs @@ -1,8 +1,8 @@ use std::{ - borrow::BorrowMut, - cell::{Ref, RefCell}, - rc::Rc, - time::{Duration, Instant}, + borrow::BorrowMut, + cell::{Ref, RefCell}, + rc::Rc, + time::{Duration, Instant}, }; use crate::{App, Entity, EntityId, Window, lerp::Lerp, linear}; @@ -43,223 +43,223 @@ use crate::{App, Entity, EntityId, Window, lerp::Lerp, linear}; /// ``` #[derive(Clone)] pub struct Transition { - /// The amount of time for which this transition should run. - duration_secs: f32, + /// The amount of time for which this transition should run. + duration_secs: f32, - /// A function that takes a delta between 0 and 1 and returns a new delta - /// between 0 and 1 based on the given easing function. - easing: Rc f32>, + /// A function that takes a delta between 0 and 1 and returns a new delta + /// between 0 and 1 based on the given easing function. + easing: Rc f32>, - state: Entity>, + state: Entity>, - /// A cached version of the transition's value. - cached_value: RefCell>, + /// A cached version of the transition's value. + cached_value: RefCell>, - /// Whether to continue the transition from the current value when the goal changes. - /// If true, transitions smoothly from current animated value to new goal. - /// If false, restarts from the original start value. - continuous: bool, + /// Whether to continue the transition from the current value when the goal changes. + /// If true, transitions smoothly from current animated value to new goal. + /// If false, restarts from the original start value. + continuous: bool, } impl Transition { - /// Create a new transition with the given duration using the specified state. - pub fn new(state: Entity>, duration: Duration) -> Self { - Self { - duration_secs: duration.as_secs_f32(), - easing: Rc::new(linear), - state, - cached_value: RefCell::new(None), - continuous: true, - } - } + /// Create a new transition with the given duration using the specified state. + pub fn new(state: Entity>, duration: Duration) -> Self { + Self { + duration_secs: duration.as_secs_f32(), + easing: Rc::new(linear), + state, + cached_value: RefCell::new(None), + continuous: true, + } + } - /// Set the easing function to use for this transition. - /// The easing function will take a time delta between 0 and 1 and return a new delta - /// between 0 and 1 - pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { - self.easing = Rc::new(easing); - self - } + /// Set the easing function to use for this transition. + /// The easing function will take a time delta between 0 and 1 and return a new delta + /// between 0 and 1 + pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { + self.easing = Rc::new(easing); + self + } - /// Sets whether the transition should be continuous. - /// - /// On goal updates, transitions continue from the current value by default. - /// If `continuous` is set to false, the transition will restart from its initial value. - pub fn continuous(mut self, continuous: bool) -> Self { - self.continuous = continuous; - self - } + /// Sets whether the transition should be continuous. + /// + /// On goal updates, transitions continue from the current value by default. + /// If `continuous` is set to false, the transition will restart from its initial value. + pub fn continuous(mut self, continuous: bool) -> Self { + self.continuous = continuous; + self + } - fn default_goal_updated_at(&self) -> Instant { - Instant::now() - Duration::from_secs_f32(self.duration_secs) - } + fn default_goal_updated_at(&self) -> Instant { + Instant::now() - Duration::from_secs_f32(self.duration_secs) + } - /// Evaluates the value of the transition without using the cache. - /// Returns if the transition is finished (bool) and the evaluated value (T). - fn raw_evaluate(&self, cx: &mut App) -> (bool, T) { - let mut state_entity = self.state.as_mut(cx); - let state: &mut TransitionState = state_entity.borrow_mut(); + /// Evaluates the value of the transition without using the cache. + /// Returns if the transition is finished (bool) and the evaluated value (T). + fn raw_evaluate(&self, cx: &mut App) -> (bool, T) { + let mut state_entity = self.state.as_mut(cx); + let state: &mut TransitionState = state_entity.borrow_mut(); - let elapsed_secs = state - .goal_last_updated_at - .unwrap_or_else(|| self.default_goal_updated_at()) - .elapsed() - .as_secs_f32(); - let delta = (self.easing)((elapsed_secs / self.duration_secs).min(1.)); + let elapsed_secs = state + .goal_last_updated_at + .unwrap_or_else(|| self.default_goal_updated_at()) + .elapsed() + .as_secs_f32(); + let delta = (self.easing)((elapsed_secs / self.duration_secs).min(1.)); - debug_assert!( - (0.0..=1.0).contains(&delta), - "delta should always be between 0 and 1" - ); + debug_assert!( + (0.0..=1.0).contains(&delta), + "delta should always be between 0 and 1" + ); - state.last_delta = delta; + state.last_delta = delta; - let evaluated_value = state.start_goal.lerp(&state.end_goal, delta); + let evaluated_value = state.start_goal.lerp(&state.end_goal, delta); - (delta != 1., evaluated_value) - } + (delta != 1., evaluated_value) + } - /// Evaluates and returns the current interpolated value of the transition. - /// - /// This method calculates the value based on the elapsed time since the last - /// goal update, applies the easing function, and caches the result. If the - /// transition is still in progress, it automatically requests an animation - /// frame to continue the animation. - /// - /// The returned value is cached for the duration of the current frame to avoid - /// redundant calculations when called multiple times. - pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> { - if self.cached_value.borrow().is_none() { - let (in_progress, evaluated_value) = self.raw_evaluate(cx); + /// Evaluates and returns the current interpolated value of the transition. + /// + /// This method calculates the value based on the elapsed time since the last + /// goal update, applies the easing function, and caches the result. If the + /// transition is still in progress, it automatically requests an animation + /// frame to continue the animation. + /// + /// The returned value is cached for the duration of the current frame to avoid + /// redundant calculations when called multiple times. + pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> { + if self.cached_value.borrow().is_none() { + let (in_progress, evaluated_value) = self.raw_evaluate(cx); - if in_progress { - window.request_animation_frame(); - } + if in_progress { + window.request_animation_frame(); + } - *self.cached_value.borrow_mut() = Some(evaluated_value); - } + *self.cached_value.borrow_mut() = Some(evaluated_value); + } - Ref::map(self.cached_value.borrow(), |opt| opt.as_ref().unwrap()) - } + Ref::map(self.cached_value.borrow(), |opt| opt.as_ref().unwrap()) + } - /// Reads the end goal of the transitions. - pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T { - &self.state.read(cx).end_goal - } + /// Reads the end goal of the transitions. + pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T { + &self.state.read(cx).end_goal + } - /// Reads the current value of the cached transition, if it exists. - pub fn read_cache(&self) -> Ref<'_, Option> { - self.cached_value.borrow() - } + /// Reads the current value of the cached transition, if it exists. + pub fn read_cache(&self) -> Ref<'_, Option> { + self.cached_value.borrow() + } - /// Evaluates and returns the current progress delta of the transition. - /// - /// Returns a value between 0.0 and 1.0 representing how far the transition - /// has progressed, after applying the easing function. A value of 0.0 means - /// the transition just started, and 1.0 means it has completed. - pub fn evaluate_delta<'b>(&'b self, cx: &'b App) -> f32 { - let goal_last_updated_at = self - .state - .read(cx) - .goal_last_updated_at - .unwrap_or_else(|| self.default_goal_updated_at()); + /// Evaluates and returns the current progress delta of the transition. + /// + /// Returns a value between 0.0 and 1.0 representing how far the transition + /// has progressed, after applying the easing function. A value of 0.0 means + /// the transition just started, and 1.0 means it has completed. + pub fn evaluate_delta<'b>(&'b self, cx: &'b App) -> f32 { + let goal_last_updated_at = self + .state + .read(cx) + .goal_last_updated_at + .unwrap_or_else(|| self.default_goal_updated_at()); - let elapsed_secs = goal_last_updated_at.elapsed().as_secs_f32(); - (self.easing)((elapsed_secs / self.duration_secs).min(1.)) - } + let elapsed_secs = goal_last_updated_at.elapsed().as_secs_f32(); + (self.easing)((elapsed_secs / self.duration_secs).min(1.)) + } - /// Updates the goal value for the transition. - /// - /// The provided closure receives a mutable reference to the current goal value - /// and can modify it. If the goal changes (and continuous mode is enabled), - /// a new animation will begin from the current interpolated value toward the - /// new goal. - /// - /// Returns `true` if the goal was actually updated (i.e., the new value differs - /// from the previous goal), `false` otherwise. - /// - /// Note: This method does not automatically notify GPUI of changes. You should - /// call `cx.notify()` within the closure if you want to trigger a re-render. - pub fn update( - &self, - cx: &mut App, - update: impl FnOnce(&mut T, &mut crate::Context>) -> R, - ) -> bool { - let mut was_updated = false; + /// Updates the goal value for the transition. + /// + /// The provided closure receives a mutable reference to the current goal value + /// and can modify it. If the goal changes (and continuous mode is enabled), + /// a new animation will begin from the current interpolated value toward the + /// new goal. + /// + /// Returns `true` if the goal was actually updated (i.e., the new value differs + /// from the previous goal), `false` otherwise. + /// + /// Note: This method does not automatically notify GPUI of changes. You should + /// call `cx.notify()` within the closure if you want to trigger a re-render. + pub fn update( + &self, + cx: &mut App, + update: impl FnOnce(&mut T, &mut crate::Context>) -> R, + ) -> bool { + let mut was_updated = false; - self.state.update(cx, |state, cx| { - let last_end_goal = state.end_goal.clone(); + self.state.update(cx, |state, cx| { + let last_end_goal = state.end_goal.clone(); - update(&mut state.end_goal, cx); + update(&mut state.end_goal, cx); - if self.continuous && state.end_goal == last_end_goal { - return; - }; + if self.continuous && state.end_goal == last_end_goal { + return; + }; - state.goal_last_updated_at = Some(Instant::now()); + state.goal_last_updated_at = Some(Instant::now()); - if self.continuous { - state.start_goal = state.start_goal.lerp(&last_end_goal, state.last_delta); - } + if self.continuous { + state.start_goal = state.start_goal.lerp(&last_end_goal, state.last_delta); + } - was_updated = true; - }); + was_updated = true; + }); - was_updated - } + was_updated + } - /// Instantly set the transition to the given target value without animation. - /// - /// Sets both the start and end goals to `target` so that subsequent evaluations - /// return `target` immediately. This is useful for transitions that require a - /// different start value on each update. - pub fn jump_to(&self, target: T, cx: &mut App) { - self.state.update(cx, |state, _cx| { - state.start_goal = target.clone(); - state.end_goal = target; - state.goal_last_updated_at = Some(Instant::now()); - }); - self.cached_value.borrow_mut().take(); - } + /// Instantly set the transition to the given target value without animation. + /// + /// Sets both the start and end goals to `target` so that subsequent evaluations + /// return `target` immediately. This is useful for transitions that require a + /// different start value on each update. + pub fn jump_to(&self, target: T, cx: &mut App) { + self.state.update(cx, |state, _cx| { + state.start_goal = target.clone(); + state.end_goal = target; + state.goal_last_updated_at = Some(Instant::now()); + }); + self.cached_value.borrow_mut().take(); + } - /// Scale the transition's start and end goals by the given ratio. - /// - /// This preserves the relative progress of an in-flight animation when the - /// coordinate space changes (e.g. on window resize). Both the start and - /// end goal are multiplied by `ratio` so the interpolated value remains - /// proportionally correct. - pub fn scale_by(&self, ratio: f32, cx: &mut App) - where - T: std::ops::Mul, - { - self.state.update(cx, |state, _cx| { - state.start_goal = state.start_goal.clone() * ratio; - state.end_goal = state.end_goal.clone() * ratio; - }); - self.cached_value.borrow_mut().take(); - } + /// Scale the transition's start and end goals by the given ratio. + /// + /// This preserves the relative progress of an in-flight animation when the + /// coordinate space changes (e.g. on window resize). Both the start and + /// end goal are multiplied by `ratio` so the interpolated value remains + /// proportionally correct. + pub fn scale_by(&self, ratio: f32, cx: &mut App) + where + T: std::ops::Mul, + { + self.state.update(cx, |state, _cx| { + state.start_goal = state.start_goal.clone() * ratio; + state.end_goal = state.end_goal.clone() * ratio; + }); + self.cached_value.borrow_mut().take(); + } - /// Returns the entity ID associated with this transition's state. - /// - /// This can be useful for tracking or comparing transitions. - pub fn entity_id(&self) -> EntityId { - self.state.entity_id() - } + /// Returns the entity ID associated with this transition's state. + /// + /// This can be useful for tracking or comparing transitions. + pub fn entity_id(&self) -> EntityId { + self.state.entity_id() + } - /// Resets the transition to its initial state. - /// - /// This clears all progress and sets both the start and end goals back to - /// the initial value that was provided when the transition was created. - /// The cache is also cleared. - pub fn reset(&self, cx: &mut App) { - self.state.update(cx, |state, _cx| { - state.goal_last_updated_at = None; - state.start_goal = state.initial_goal.clone(); - state.end_goal = state.initial_goal.clone(); - state.last_delta = 0.0; - }); - *self.cached_value.borrow_mut() = None; - } + /// Resets the transition to its initial state. + /// + /// This clears all progress and sets both the start and end goals back to + /// the initial value that was provided when the transition was created. + /// The cache is also cleared. + pub fn reset(&self, cx: &mut App) { + self.state.update(cx, |state, _cx| { + state.goal_last_updated_at = None; + state.start_goal = state.initial_goal.clone(); + state.end_goal = state.initial_goal.clone(); + state.last_delta = 0.0; + }); + *self.cached_value.borrow_mut() = None; + } } // Internal state container for a [`Transition`](crate::Transition). @@ -269,488 +269,488 @@ impl Transition { /// computed delta value. #[derive(Clone)] pub struct TransitionState { - pub(crate) goal_last_updated_at: Option, - pub(crate) initial_goal: T, - pub(crate) start_goal: T, - pub(crate) end_goal: T, - pub(crate) last_delta: f32, + pub(crate) goal_last_updated_at: Option, + pub(crate) initial_goal: T, + pub(crate) start_goal: T, + pub(crate) end_goal: T, + pub(crate) last_delta: f32, } impl TransitionState { - /// Creates a new transition state with the given initial goal. - /// - /// The start goal, end goal, and initial goal are all set to the provided value. - /// The transition begins in a "completed" state (delta = 1.0) until the goal - /// is updated. - pub fn new(initial_goal: T) -> Self { - Self { - goal_last_updated_at: None, - initial_goal: initial_goal.clone(), - start_goal: initial_goal.clone(), - end_goal: initial_goal, - last_delta: 1., - } - } + /// Creates a new transition state with the given initial goal. + /// + /// The start goal, end goal, and initial goal are all set to the provided value. + /// The transition begins in a "completed" state (delta = 1.0) until the goal + /// is updated. + pub fn new(initial_goal: T) -> Self { + Self { + goal_last_updated_at: None, + initial_goal: initial_goal.clone(), + start_goal: initial_goal.clone(), + end_goal: initial_goal, + last_delta: 1., + } + } } #[cfg(all(test, feature = "test-support"))] mod tests { - use crate::AppContext; - - use super::*; - use gpui::{Point, Rgba, TestAppContext, px}; - - /// Helper to create a Transition directly without using window hooks. - /// This bypasses the render-phase restriction of use_transition/use_keyed_transition. - fn create_transition( - cx: &mut App, - duration: Duration, - initial: T, - ) -> Transition { - let state = cx.new(|_| TransitionState::new(initial)); - Transition::new(state, duration) - } - - #[gpui::test] - fn test_transition_creation(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - // Read the goal - should be the initial value - let goal = transition.read_goal(cx); - assert_eq!(*goal, 0.0); - }); - } - - #[gpui::test] - fn test_transition_read_goal(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32); - - let goal = transition.read_goal(cx); - assert_eq!(*goal, 42.0); - }); - } - - #[gpui::test] - fn test_transition_update_returns_true_on_change(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - let was_updated = transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - assert!(was_updated); - }); - } - - #[gpui::test] - fn test_transition_update_returns_false_on_no_change(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32); - - // Update to the same value - let was_updated = transition.update(cx, |val, _cx| { - *val = 50.0; - }); - - assert!(!was_updated); - }); - } - - #[gpui::test] - fn test_transition_goal_updated_after_update(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - transition.update(cx, |val, _cx| { - *val = 200.0; - }); - - let goal = transition.read_goal(cx); - assert_eq!(*goal, 200.0); - }); - } - - #[gpui::test] - fn test_transition_entity_id(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition1 = create_transition(cx, Duration::from_millis(300), 0.0_f32); - let transition2 = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - // Different transitions should have different entity IDs - assert_ne!(transition1.entity_id(), transition2.entity_id()); - }); - } - - #[gpui::test] - fn test_transition_reset(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 10.0_f32); - - // Update the goal - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - assert_eq!(*transition.read_goal(cx), 100.0); - - // Reset the transition - transition.reset(cx); - - // Goal should be back to initial value - assert_eq!(*transition.read_goal(cx), 10.0); - }); - } - - #[gpui::test] - fn test_transition_cache_cleared_on_reset(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 25.0_f32); - - // Manually populate the cache using raw_evaluate - let (_, value) = transition.raw_evaluate(cx); - *transition.cached_value.borrow_mut() = Some(value); - assert!(transition.read_cache().is_some()); - - // Reset - transition.reset(cx); - - // Cache should be cleared - assert!(transition.read_cache().is_none()); - }); - } - - #[gpui::test] - fn test_transition_with_custom_easing(cx: &mut TestAppContext) { - cx.update(|cx| { - // Custom easing that always returns 0.5 - let transition = - create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); - - // Update goal to trigger animation - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - // With our custom easing, the delta should be 0.5 - // So the value should be lerp(0, 100, 0.5) = 50 - let (_, value) = transition.raw_evaluate(cx); - assert_eq!(value, 50.0); - }); - } - - #[gpui::test] - fn test_transition_with_point(cx: &mut TestAppContext) { - cx.update(|cx| { - let initial: Point = Point { x: 0.0, y: 0.0 }; - let transition = create_transition(cx, Duration::from_millis(300), initial); - - let goal = transition.read_goal(cx); - assert_eq!(goal.x, 0.0); - assert_eq!(goal.y, 0.0); - - transition.update(cx, |point, _cx| { - point.x = 100.0; - point.y = 200.0; - }); - - let goal = transition.read_goal(cx); - assert_eq!(goal.x, 100.0); - assert_eq!(goal.y, 200.0); - }); - } - - #[gpui::test] - fn test_transition_with_rgba(cx: &mut TestAppContext) { - cx.update(|cx| { - let initial = Rgba { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - let transition = create_transition(cx, Duration::from_millis(300), initial); - - let goal = transition.read_goal(cx); - assert_eq!(goal.r, 1.0); - assert_eq!(goal.g, 0.0); - assert_eq!(goal.b, 0.0); - }); - } - - #[gpui::test] - fn test_transition_continuous_mode_default(cx: &mut TestAppContext) { - cx.update(|cx| { - // By default, transitions are continuous - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - // First update - transition.update(cx, |val, _cx| { - *val = 50.0; - }); - - // Second update (in continuous mode, this should work from current interpolated position) - let was_updated = transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - assert!(was_updated); - assert_eq!(*transition.read_goal(cx), 100.0); - }); - } - - #[gpui::test] - fn test_transition_non_continuous_mode(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = - create_transition(cx, Duration::from_millis(300), 0.0_f32).continuous(false); - - // Update the goal - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - assert_eq!(*transition.read_goal(cx), 100.0); - }); - } - - #[gpui::test] - fn test_evaluate_delta_initial(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - // Without any update, the transition should be "complete" (delta = 1.0) - // because it starts in completed state - let delta = transition.evaluate_delta(cx); - assert_eq!(delta, 1.0); - }); - } - - #[gpui::test] - fn test_evaluate_delta_after_update(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - // Immediately after update, delta should be close to 0 - let delta = transition.evaluate_delta(cx); - assert!( - delta < 0.1, - "delta should be small immediately after update" - ); - }); - } - - #[gpui::test] - fn test_transition_clone(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32); - - let cloned = transition.clone(); - - // Both should reference the same entity - assert_eq!(transition.entity_id(), cloned.entity_id()); - - // Updating one should affect the other - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - assert_eq!(*cloned.read_goal(cx), 100.0); - }); - } - - #[gpui::test] - fn test_transition_cache_consistency(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = - create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); // Always return 0.5 for deterministic testing - - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - // First evaluation using raw_evaluate - let (_, value1) = transition.raw_evaluate(cx); - - // Second evaluation should return the same value - let (_, value2) = transition.raw_evaluate(cx); - - assert_eq!(value1, value2); - }); - } - - #[gpui::test] - fn test_multiple_transitions_independent(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition_a = create_transition(cx, Duration::from_millis(300), 0.0_f32); - let transition_b = create_transition(cx, Duration::from_millis(300), 100.0_f32); - - // Update only transition_a - transition_a.update(cx, |val, _cx| { - *val = 50.0; - }); - - // transition_b should remain unchanged - assert_eq!(*transition_a.read_goal(cx), 50.0); - assert_eq!(*transition_b.read_goal(cx), 100.0); - }); - } - - #[gpui::test] - fn test_transition_with_pixels(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), px(0.0)); - - let goal = transition.read_goal(cx); - assert_eq!(*goal, px(0.0)); - - transition.update(cx, |val, _cx| { - *val = px(100.0); - }); - - let goal = transition.read_goal(cx); - assert_eq!(*goal, px(100.0)); - }); - } - - #[gpui::test] - fn test_transition_rapid_updates(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - // Rapidly update multiple times - for i in 1..=10 { - transition.update(cx, |val, _cx| { - *val = i as f32 * 10.0; - }); - } - - // Final goal should be 100.0 - assert_eq!(*transition.read_goal(cx), 100.0); - }); - } - - #[gpui::test] - fn test_raw_evaluate_in_progress(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); - - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - // Immediately after update, the transition should be in progress - let (in_progress, _value) = transition.raw_evaluate(cx); - assert!( - in_progress, - "transition should be in progress immediately after update" - ); - }); - } - - #[gpui::test] - fn test_raw_evaluate_completed(cx: &mut TestAppContext) { - cx.update(|cx| { - let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32); - - // Without any update, the transition starts as "complete" - let (in_progress, value) = transition.raw_evaluate(cx); - assert!( - !in_progress, - "transition should be complete without updates" - ); - assert_eq!(value, 50.0); - }); - } - - #[gpui::test] - fn test_transition_interpolation_with_easing(cx: &mut TestAppContext) { - cx.update(|cx| { - // Test with different easing values - for expected_delta in [0.0, 0.25, 0.5, 0.75, 1.0] { - let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32) - .with_easing(move |_| expected_delta); - - transition.update(cx, |val, _cx| { - *val = 100.0; - }); - - let (_, value) = transition.raw_evaluate(cx); - let expected_value = 0.0_f32.lerp(&100.0_f32, expected_delta); - assert_eq!(value, expected_value); - } - }); - } - - #[test] - fn test_new_state_initialization() { - let state = TransitionState::new(42.0_f32); - - assert_eq!(state.initial_goal, 42.0); - assert_eq!(state.start_goal, 42.0); - assert_eq!(state.end_goal, 42.0); - assert_eq!(state.last_delta, 1.0); - assert!(state.goal_last_updated_at.is_none()); - } - - #[test] - fn test_state_with_point() { - let initial: Point = Point { x: 10.0, y: 20.0 }; - let state = TransitionState::new(initial); - - assert_eq!(state.initial_goal.x, 10.0); - assert_eq!(state.initial_goal.y, 20.0); - assert_eq!(state.start_goal.x, 10.0); - assert_eq!(state.end_goal.x, 10.0); - } - - #[test] - fn test_state_with_rgba() { - let initial = Rgba { - r: 1.0, - g: 0.5, - b: 0.0, - a: 1.0, - }; - let state = TransitionState::new(initial); - - assert_eq!(state.initial_goal.r, 1.0); - assert_eq!(state.initial_goal.g, 0.5); - assert_eq!(state.initial_goal.b, 0.0); - assert_eq!(state.initial_goal.a, 1.0); - } - - #[test] - fn test_state_clone() { - let state = TransitionState::new(100.0_f32); - let cloned = state.clone(); - - assert_eq!(state.initial_goal, cloned.initial_goal); - assert_eq!(state.start_goal, cloned.start_goal); - assert_eq!(state.end_goal, cloned.end_goal); - assert_eq!(state.last_delta, cloned.last_delta); - } - - #[test] - fn test_state_with_integer() { - let state = TransitionState::new(50_i32); - - assert_eq!(state.initial_goal, 50); - assert_eq!(state.start_goal, 50); - assert_eq!(state.end_goal, 50); - } - - #[test] - fn test_state_starts_completed() { - let state = TransitionState::new(0.0_f32); - - // last_delta should be 1.0 indicating the transition is "complete" - assert_eq!(state.last_delta, 1.0); - } + use crate::AppContext; + + use super::*; + use gpui::{Point, Rgba, TestAppContext, px}; + + /// Helper to create a Transition directly without using window hooks. + /// This bypasses the render-phase restriction of use_transition/use_keyed_transition. + fn create_transition( + cx: &mut App, + duration: Duration, + initial: T, + ) -> Transition { + let state = cx.new(|_| TransitionState::new(initial)); + Transition::new(state, duration) + } + + #[gpui::test] + fn test_transition_creation(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + // Read the goal - should be the initial value + let goal = transition.read_goal(cx); + assert_eq!(*goal, 0.0); + }); + } + + #[gpui::test] + fn test_transition_read_goal(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32); + + let goal = transition.read_goal(cx); + assert_eq!(*goal, 42.0); + }); + } + + #[gpui::test] + fn test_transition_update_returns_true_on_change(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + let was_updated = transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + assert!(was_updated); + }); + } + + #[gpui::test] + fn test_transition_update_returns_false_on_no_change(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32); + + // Update to the same value + let was_updated = transition.update(cx, |val, _cx| { + *val = 50.0; + }); + + assert!(!was_updated); + }); + } + + #[gpui::test] + fn test_transition_goal_updated_after_update(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + transition.update(cx, |val, _cx| { + *val = 200.0; + }); + + let goal = transition.read_goal(cx); + assert_eq!(*goal, 200.0); + }); + } + + #[gpui::test] + fn test_transition_entity_id(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition1 = create_transition(cx, Duration::from_millis(300), 0.0_f32); + let transition2 = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + // Different transitions should have different entity IDs + assert_ne!(transition1.entity_id(), transition2.entity_id()); + }); + } + + #[gpui::test] + fn test_transition_reset(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 10.0_f32); + + // Update the goal + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + assert_eq!(*transition.read_goal(cx), 100.0); + + // Reset the transition + transition.reset(cx); + + // Goal should be back to initial value + assert_eq!(*transition.read_goal(cx), 10.0); + }); + } + + #[gpui::test] + fn test_transition_cache_cleared_on_reset(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 25.0_f32); + + // Manually populate the cache using raw_evaluate + let (_, value) = transition.raw_evaluate(cx); + *transition.cached_value.borrow_mut() = Some(value); + assert!(transition.read_cache().is_some()); + + // Reset + transition.reset(cx); + + // Cache should be cleared + assert!(transition.read_cache().is_none()); + }); + } + + #[gpui::test] + fn test_transition_with_custom_easing(cx: &mut TestAppContext) { + cx.update(|cx| { + // Custom easing that always returns 0.5 + let transition = + create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); + + // Update goal to trigger animation + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + // With our custom easing, the delta should be 0.5 + // So the value should be lerp(0, 100, 0.5) = 50 + let (_, value) = transition.raw_evaluate(cx); + assert_eq!(value, 50.0); + }); + } + + #[gpui::test] + fn test_transition_with_point(cx: &mut TestAppContext) { + cx.update(|cx| { + let initial: Point = Point { x: 0.0, y: 0.0 }; + let transition = create_transition(cx, Duration::from_millis(300), initial); + + let goal = transition.read_goal(cx); + assert_eq!(goal.x, 0.0); + assert_eq!(goal.y, 0.0); + + transition.update(cx, |point, _cx| { + point.x = 100.0; + point.y = 200.0; + }); + + let goal = transition.read_goal(cx); + assert_eq!(goal.x, 100.0); + assert_eq!(goal.y, 200.0); + }); + } + + #[gpui::test] + fn test_transition_with_rgba(cx: &mut TestAppContext) { + cx.update(|cx| { + let initial = Rgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + let transition = create_transition(cx, Duration::from_millis(300), initial); + + let goal = transition.read_goal(cx); + assert_eq!(goal.r, 1.0); + assert_eq!(goal.g, 0.0); + assert_eq!(goal.b, 0.0); + }); + } + + #[gpui::test] + fn test_transition_continuous_mode_default(cx: &mut TestAppContext) { + cx.update(|cx| { + // By default, transitions are continuous + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + // First update + transition.update(cx, |val, _cx| { + *val = 50.0; + }); + + // Second update (in continuous mode, this should work from current interpolated position) + let was_updated = transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + assert!(was_updated); + assert_eq!(*transition.read_goal(cx), 100.0); + }); + } + + #[gpui::test] + fn test_transition_non_continuous_mode(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = + create_transition(cx, Duration::from_millis(300), 0.0_f32).continuous(false); + + // Update the goal + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + assert_eq!(*transition.read_goal(cx), 100.0); + }); + } + + #[gpui::test] + fn test_evaluate_delta_initial(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + // Without any update, the transition should be "complete" (delta = 1.0) + // because it starts in completed state + let delta = transition.evaluate_delta(cx); + assert_eq!(delta, 1.0); + }); + } + + #[gpui::test] + fn test_evaluate_delta_after_update(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + // Immediately after update, delta should be close to 0 + let delta = transition.evaluate_delta(cx); + assert!( + delta < 0.1, + "delta should be small immediately after update" + ); + }); + } + + #[gpui::test] + fn test_transition_clone(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32); + + let cloned = transition.clone(); + + // Both should reference the same entity + assert_eq!(transition.entity_id(), cloned.entity_id()); + + // Updating one should affect the other + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + assert_eq!(*cloned.read_goal(cx), 100.0); + }); + } + + #[gpui::test] + fn test_transition_cache_consistency(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = + create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); // Always return 0.5 for deterministic testing + + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + // First evaluation using raw_evaluate + let (_, value1) = transition.raw_evaluate(cx); + + // Second evaluation should return the same value + let (_, value2) = transition.raw_evaluate(cx); + + assert_eq!(value1, value2); + }); + } + + #[gpui::test] + fn test_multiple_transitions_independent(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition_a = create_transition(cx, Duration::from_millis(300), 0.0_f32); + let transition_b = create_transition(cx, Duration::from_millis(300), 100.0_f32); + + // Update only transition_a + transition_a.update(cx, |val, _cx| { + *val = 50.0; + }); + + // transition_b should remain unchanged + assert_eq!(*transition_a.read_goal(cx), 50.0); + assert_eq!(*transition_b.read_goal(cx), 100.0); + }); + } + + #[gpui::test] + fn test_transition_with_pixels(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), px(0.0)); + + let goal = transition.read_goal(cx); + assert_eq!(*goal, px(0.0)); + + transition.update(cx, |val, _cx| { + *val = px(100.0); + }); + + let goal = transition.read_goal(cx); + assert_eq!(*goal, px(100.0)); + }); + } + + #[gpui::test] + fn test_transition_rapid_updates(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + // Rapidly update multiple times + for i in 1..=10 { + transition.update(cx, |val, _cx| { + *val = i as f32 * 10.0; + }); + } + + // Final goal should be 100.0 + assert_eq!(*transition.read_goal(cx), 100.0); + }); + } + + #[gpui::test] + fn test_raw_evaluate_in_progress(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32); + + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + // Immediately after update, the transition should be in progress + let (in_progress, _value) = transition.raw_evaluate(cx); + assert!( + in_progress, + "transition should be in progress immediately after update" + ); + }); + } + + #[gpui::test] + fn test_raw_evaluate_completed(cx: &mut TestAppContext) { + cx.update(|cx| { + let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32); + + // Without any update, the transition starts as "complete" + let (in_progress, value) = transition.raw_evaluate(cx); + assert!( + !in_progress, + "transition should be complete without updates" + ); + assert_eq!(value, 50.0); + }); + } + + #[gpui::test] + fn test_transition_interpolation_with_easing(cx: &mut TestAppContext) { + cx.update(|cx| { + // Test with different easing values + for expected_delta in [0.0, 0.25, 0.5, 0.75, 1.0] { + let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32) + .with_easing(move |_| expected_delta); + + transition.update(cx, |val, _cx| { + *val = 100.0; + }); + + let (_, value) = transition.raw_evaluate(cx); + let expected_value = 0.0_f32.lerp(&100.0_f32, expected_delta); + assert_eq!(value, expected_value); + } + }); + } + + #[test] + fn test_new_state_initialization() { + let state = TransitionState::new(42.0_f32); + + assert_eq!(state.initial_goal, 42.0); + assert_eq!(state.start_goal, 42.0); + assert_eq!(state.end_goal, 42.0); + assert_eq!(state.last_delta, 1.0); + assert!(state.goal_last_updated_at.is_none()); + } + + #[test] + fn test_state_with_point() { + let initial: Point = Point { x: 10.0, y: 20.0 }; + let state = TransitionState::new(initial); + + assert_eq!(state.initial_goal.x, 10.0); + assert_eq!(state.initial_goal.y, 20.0); + assert_eq!(state.start_goal.x, 10.0); + assert_eq!(state.end_goal.x, 10.0); + } + + #[test] + fn test_state_with_rgba() { + let initial = Rgba { + r: 1.0, + g: 0.5, + b: 0.0, + a: 1.0, + }; + let state = TransitionState::new(initial); + + assert_eq!(state.initial_goal.r, 1.0); + assert_eq!(state.initial_goal.g, 0.5); + assert_eq!(state.initial_goal.b, 0.0); + assert_eq!(state.initial_goal.a, 1.0); + } + + #[test] + fn test_state_clone() { + let state = TransitionState::new(100.0_f32); + let cloned = state.clone(); + + assert_eq!(state.initial_goal, cloned.initial_goal); + assert_eq!(state.start_goal, cloned.start_goal); + assert_eq!(state.end_goal, cloned.end_goal); + assert_eq!(state.last_delta, cloned.last_delta); + } + + #[test] + fn test_state_with_integer() { + let state = TransitionState::new(50_i32); + + assert_eq!(state.initial_goal, 50); + assert_eq!(state.start_goal, 50); + assert_eq!(state.end_goal, 50); + } + + #[test] + fn test_state_starts_completed() { + let state = TransitionState::new(0.0_f32); + + // last_delta should be 1.0 indicating the transition is "complete" + assert_eq!(state.last_delta, 1.0); + } } diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 13ee752dad..89ee2c8468 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -1,92 +1,92 @@ use crate::{BackgroundExecutor, Task}; use std::{ - future::Future, - pin::Pin, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, - task, - time::Duration, + 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) - } + /// 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(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 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) }) - } + /// 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; + /// 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), - } - } + 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<()>, + #[pin] + future: T, + #[pin] + timer: Task<()>, } #[derive(Debug, thiserror::Error)] @@ -95,158 +95,158 @@ pub struct WithTimeout { pub struct Timeout; impl Future for WithTimeout { - type Output = Result; + type Output = Result; - fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { - let this = self.project(); + 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 - } - } + 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, - } - } + 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) + (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) + (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) + 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) - } + 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() + (logical * scale_factor).floor() } #[inline] pub(crate) fn ceil_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { - (logical * scale_factor).ceil() + (logical * scale_factor).ceil() } #[cfg(test)] mod tests { - use crate::TestAppContext; + use crate::TestAppContext; - use super::*; + 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); + #[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); + // 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); - } + // 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); + #[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); + // 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); + // 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); - } + // 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); - } + #[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"); + #[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 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"); - } + 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/src/view.rs b/crates/gpui/src/view.rs index 39b87dbb80..61dd14acbb 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,7 +1,7 @@ use crate::{ - AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId, - Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex, - Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity, + AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId, + Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex, + Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity, }; use crate::{Empty, Window}; use anyhow::Result; @@ -12,309 +12,309 @@ use std::rc::Rc; use std::{any::TypeId, fmt, ops::Range}; struct AnyViewState { - prepaint_range: Range, - paint_range: Range, - cache_key: ViewCacheKey, - accessed_entities: FxHashSet, + prepaint_range: Range, + paint_range: Range, + cache_key: ViewCacheKey, + accessed_entities: FxHashSet, } #[derive(Default)] struct ViewCacheKey { - bounds: Bounds, - content_mask: ContentMask, - text_style: TextStyle, + bounds: Bounds, + content_mask: ContentMask, + text_style: TextStyle, } /// A dynamically-typed handle to a view, which can be downcast to a [Entity] for a specific type. #[derive(Clone, Debug)] pub struct AnyView { - entity: AnyEntity, - render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, - cached_style: Option>, + entity: AnyEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, + cached_style: Option>, } impl From> for AnyView { - fn from(value: Entity) -> Self { - AnyView { - entity: value.into_any(), - render: any_view::render::, - cached_style: None, - } - } + fn from(value: Entity) -> Self { + AnyView { + entity: value.into_any(), + render: any_view::render::, + cached_style: None, + } + } } impl AnyView { - /// Indicate that this view should be cached when using it as an element. - /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [Context::notify] has not been called since it was rendered. - /// The one exception is when [Window::refresh] is called, in which case caching is ignored. - pub fn cached(mut self, style: StyleRefinement) -> Self { - self.cached_style = Some(style.into()); - self - } + /// Indicate that this view should be cached when using it as an element. + /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [Context::notify] has not been called since it was rendered. + /// The one exception is when [Window::refresh] is called, in which case caching is ignored. + pub fn cached(mut self, style: StyleRefinement) -> Self { + self.cached_style = Some(style.into()); + self + } - /// Convert this to a weak handle. - pub fn downgrade(&self) -> AnyWeakView { - AnyWeakView { - entity: self.entity.downgrade(), - render: self.render, - } - } + /// 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, - cached_style: self.cached_style, - }), - } - } + /// 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, + cached_style: self.cached_style, + }), + } + } - /// Gets the [TypeId] of the underlying view. - pub fn entity_type(&self) -> TypeId { - self.entity.entity_type - } + /// Gets the [TypeId] of the underlying view. + pub fn entity_type(&self) -> TypeId { + self.entity.entity_type + } - /// Gets the entity id of this handle. - pub fn entity_id(&self) -> EntityId { - self.entity.entity_id() - } + /// Gets the entity id of this handle. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } } impl PartialEq for AnyView { - fn eq(&self, other: &Self) -> bool { - self.entity == other.entity - } + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } } impl Eq for AnyView {} impl Element for AnyView { - type RequestLayoutState = Option; - type PrepaintState = Option; + type RequestLayoutState = Option; + type PrepaintState = Option; - fn id(&self) -> Option { - Some(ElementId::View(self.entity_id())) - } + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id())) + } - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - 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) { - window.with_rendered_view(self.entity_id(), |window| { - // Disable caching when inspecting so that mouse_hit_test has all hitboxes. - 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.render)(self, window, cx); - let layout_id = element.request_layout(window, cx); - (layout_id, Some(element)) - } - } - }) - } + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + window.with_rendered_view(self.entity_id(), |window| { + // Disable caching when inspecting so that mouse_hit_test has all hitboxes. + 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.render)(self, window, cx); + 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 { - window.set_view_id(self.entity_id()); - window.with_rendered_view(self.entity_id(), |window| { - if let Some(mut element) = element.take() { - element.prepaint(window, cx); - return 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 { + window.set_view_id(self.entity_id()); + window.with_rendered_view(self.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(); + 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(&self.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; + 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(&self.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); - } + 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.render)(self, window, cx); - element.layout_as_root(bounds.size.into(), window, cx); - element.prepaint_at(bounds.origin, window, cx); - element - }); + 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.render)(self, window, cx); + 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; + let prepaint_end = window.prepaint_index(); + window.refreshing = refreshing; - ( - Some(element), - AnyViewState { - accessed_entities, - prepaint_range: prepaint_start..prepaint_end, - paint_range: PaintIndex::default()..PaintIndex::default(), - cache_key: ViewCacheKey { - bounds, - content_mask, - text_style, - }, - }, - ) - }, - ) - }) - } + ( + Some(element), + AnyViewState { + accessed_entities, + prepaint_range: prepaint_start..prepaint_end, + paint_range: PaintIndex::default()..PaintIndex::default(), + cache_key: ViewCacheKey { + bounds, + content_mask, + text_style, + }, + }, + ) + }, + ) + }) + } - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _: &mut Self::RequestLayoutState, - element: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - window.with_rendered_view(self.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(); + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.with_rendered_view(self.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(); + 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()); - } + 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; + let paint_end = window.paint_index(); + element_state.paint_range = paint_start..paint_end; - ((), element_state) - }, - ) - } else { - element.as_mut().unwrap().paint(window, cx); - } - }); - } + ((), element_state) + }, + ) + } else { + element.as_mut().unwrap().paint(window, cx); + } + }); + } } impl IntoElement for Entity { - type Element = AnyView; + type Element = AnyView; - fn into_element(self) -> Self::Element { - self.into() - } + fn into_element(self) -> Self::Element { + self.into() + } } impl IntoElement for AnyView { - type Element = Self; + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self + } } /// A weak, dynamically-typed view handle that does not prevent the view from being released. pub struct AnyWeakView { - entity: AnyWeakEntity, - render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, + entity: AnyWeakEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, } impl AnyWeakView { - /// Convert to a strongly-typed handle if the referenced view has not yet been released. - pub fn upgrade(&self) -> Option { - let entity = self.entity.upgrade()?; - Some(AnyView { - entity, - render: self.render, - cached_style: None, - }) - } + /// Convert to a strongly-typed handle if the referenced view has not yet been released. + pub fn upgrade(&self) -> Option { + let entity = self.entity.upgrade()?; + Some(AnyView { + entity, + render: self.render, + cached_style: None, + }) + } } impl From> for AnyWeakView { - fn from(view: WeakEntity) -> Self { - AnyWeakView { - entity: view.into(), - render: any_view::render::, - } - } + 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 - } + 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() - } + 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}; + 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(); - view.update(cx, |view, cx| view.render(window, cx).into_any_element()) - } + pub(crate) fn render( + view: &AnyView, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let view = view.clone().downcast::().unwrap(); + view.update(cx, |view, cx| view.render(window, cx).into_any_element()) + } } /// 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 - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 5276fb15dc..710cbc7462 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,24 +1,24 @@ #[cfg(any(feature = "inspector", debug_assertions))] use crate::Inspector; use crate::{ - Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, - AsyncWindowContext, AvailableSpace, BackdropFilter, Background, BorderStyle, Bounds, BoxShadow, - Capslock, Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, - DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, - EntityId, EventEmitter, FileDropEvent, Filter, FilterBoundary, FontId, Global, GlobalElementId, - GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, - Keystroke, KeystrokeEvent, LayoutId, Lerp, 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, SMOOTH_SVG_SCALE_FACTOR, - SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledFilter, ScaledPixels, Scene, Shadow, - SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, - SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, - TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, - Transition, TransitionState, Underline, UnderlineStyle, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, - WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black, + Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, + AsyncWindowContext, AvailableSpace, BackdropFilter, Background, BorderStyle, Bounds, BoxShadow, + Capslock, Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, + DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, + EntityId, EventEmitter, FileDropEvent, Filter, FilterBoundary, FontId, Global, GlobalElementId, + GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, + Keystroke, KeystrokeEvent, LayoutId, Lerp, 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, SMOOTH_SVG_SCALE_FACTOR, + SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledFilter, ScaledPixels, Scene, Shadow, + SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, + SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, + TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, + Transition, TransitionState, Underline, UnderlineStyle, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, + WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -40,21 +40,21 @@ 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::{ - Arc, Weak, - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - }, - time::Duration, + any::{Any, TypeId}, + borrow::Cow, + cell::{Cell, RefCell}, + cmp, + fmt::{Debug, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + ops::{DerefMut, Range}, + rc::Rc, + sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, + }, + time::Duration, }; use uuid::Uuid; @@ -65,8 +65,8 @@ 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, + 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::*; @@ -76,465 +76,465 @@ 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.), + 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, + /// 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 "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 - } + /// Returns true if this represents the "capture" phase. + #[inline] + pub fn capture(self) -> bool { + self == DispatchPhase::Capture + } } struct WindowInvalidatorInner { - pub dirty: bool, - pub draw_phase: DrawPhase, - pub dirty_views: FxHashSet, - pub update_count: usize, + pub dirty: bool, + pub draw_phase: DrawPhase, + pub dirty_views: FxHashSet, + pub update_count: usize, } #[derive(Clone)] pub(crate) struct WindowInvalidator { - inner: Rc>, + inner: Rc>, } impl WindowInvalidator { - pub fn new() -> Self { - WindowInvalidator { - inner: Rc::new(RefCell::new(WindowInvalidatorInner { - dirty: true, - draw_phase: DrawPhase::None, - dirty_views: FxHashSet::default(), - update_count: 0, - })), - } - } + pub fn new() -> Self { + WindowInvalidator { + inner: Rc::new(RefCell::new(WindowInvalidatorInner { + dirty: true, + draw_phase: DrawPhase::None, + dirty_views: FxHashSet::default(), + update_count: 0, + })), + } + } - 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 { - inner.dirty = true; - cx.push_effect(Effect::Notify { emitter: entity }); - true - } else { - false - } - } + 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 { + inner.dirty = true; + cx.push_effect(Effect::Notify { emitter: entity }); + true + } else { + false + } + } - pub fn is_dirty(&self) -> bool { - self.inner.borrow().dirty - } + pub fn is_dirty(&self) -> bool { + self.inner.borrow().dirty + } - pub fn set_dirty(&self, dirty: bool) { - let mut inner = self.inner.borrow_mut(); - inner.dirty = dirty; - if dirty { - inner.update_count += 1; - } - } + pub fn set_dirty(&self, dirty: bool) { + let mut inner = self.inner.borrow_mut(); + inner.dirty = dirty; + if dirty { + inner.update_count += 1; + } + } - pub fn set_phase(&self, phase: DrawPhase) { - self.inner.borrow_mut().draw_phase = phase - } + 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 - } + pub fn update_count(&self) -> usize { + self.inner.borrow().update_count + } - pub fn take_views(&self) -> FxHashSet { - mem::take(&mut self.inner.borrow_mut().dirty_views) - } + 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 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 - } + 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_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_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" - ); - } + #[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>; + Box bool + 'static>; pub(crate) struct WindowFocusEvent { - pub(crate) previous_focus_path: SmallVec<[FocusId; 8]>, - pub(crate) current_focus_path: SmallVec<[FocusId; 8]>, + 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_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) - } + 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, + /// 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; + /// 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)); + /// 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) }; + /// 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) }; } /// 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) - } - }) + 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) + } + }) } /// RAII guard that sets CURRENT_ELEMENT_ARENA for the duration of a draw operation. /// When dropped, restores the previous arena (supporting nested draws). pub(crate) struct ElementArenaScope { - previous: Option<*const RefCell>, + previous: Option<*const RefCell>, } impl ElementArenaScope { - /// Enter a scope where element allocations use the given arena. - pub(crate) fn enter(arena: &RefCell) -> Self { - let previous = CURRENT_ELEMENT_ARENA.with(|current| { - let prev = current.get(); - current.set(Some(arena as *const RefCell)); - prev - }); - Self { previous } - } + /// Enter a scope where element allocations use the given arena. + pub(crate) fn enter(arena: &RefCell) -> Self { + let previous = CURRENT_ELEMENT_ARENA.with(|current| { + let prev = current.get(); + current.set(Some(arena as *const RefCell)); + prev + }); + Self { previous } + } } impl Drop for ElementArenaScope { - fn drop(&mut self) { - CURRENT_ELEMENT_ARENA.with(|current| { - current.set(self.previous); - }); - } + fn drop(&mut self) { + CURRENT_ELEMENT_ARENA.with(|current| { + current.set(self.previous); + }); + } } /// Returned when the element arena has been used and so must be cleared before the next draw. #[must_use] pub struct ArenaClearNeeded { - arena: *const RefCell, + arena: *const RefCell, } impl ArenaClearNeeded { - /// Create a new ArenaClearNeeded that will clear the given arena. - pub(crate) fn new(arena: &RefCell) -> Self { - Self { - arena: arena as *const RefCell, - } - } + /// Create a new ArenaClearNeeded that will clear the given arena. + pub(crate) fn new(arena: &RefCell) -> Self { + Self { + arena: arena as *const RefCell, + } + } - /// Clear the element arena. - pub fn clear(self) { - // SAFETY: The arena pointer is valid because ArenaClearNeeded is created - // at the end of draw() and must be cleared before the next draw. - let arena_cell = unsafe { &*self.arena }; - arena_cell.borrow_mut().clear(); - } + /// Clear the element arena. + pub fn clear(self) { + // SAFETY: The arena pointer is valid because ArenaClearNeeded is created + // at the end of draw() and must be cleared before the next draw. + let arena_cell = unsafe { &*self.arena }; + arena_cell.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, + 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 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 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 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) - } + /// 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, + 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)) - } + 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, - }); + 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(), - } - } + 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(), - }) - } + 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 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 - } + /// 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), - } - } + /// 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) - } + /// 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 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 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 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) - } + /// 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) - } - } + /// 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() - } + 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 - } + 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); - } + 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, + 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) - } + /// 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 - } + 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 - } + fn eq(&self, other: &FocusHandle) -> bool { + self.id == other.id + } } impl PartialEq for FocusHandle { - fn eq(&self, other: &WeakFocusHandle) -> bool { - self.id == other.id - } + 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; + /// 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) - } + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.read(cx).focus_handle(cx) + } } /// ManagedView is a view (like a Modal, Popover, Menu, etc.) @@ -549,31 +549,31 @@ pub struct DismissEvent; type FrameCallback = Box; pub(crate) type AnyMouseListener = - Box; + Box; #[derive(Clone)] pub(crate) struct CursorStyleRequest { - pub(crate) hitbox_id: Option, - pub(crate) style: CursorStyle, + 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, + 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 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]. @@ -582,172 +582,172 @@ 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) - } + /// 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. 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) - } + /// 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 - } + 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) - } + /// 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)) - } + 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: ContentMask, - /// Flags that specify hitbox behavior. - pub behavior: HitboxBehavior, + /// 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: ContentMask, + /// 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 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 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) - } + /// 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, + /// 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 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, + /// 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. @@ -755,341 +755,341 @@ pub enum HitboxBehavior { 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()) - }) - } + /// 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, + id: TooltipId, + bounds: Bounds, } #[derive(Clone)] pub(crate) struct TooltipRequest { - id: TooltipId, - tooltip: AnyTooltip, + 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, + 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, + 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, + 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, + 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(), + 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(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))] + next_inspector_instance_ids: FxHashMap::default(), - #[cfg(any(feature = "inspector", debug_assertions))] - inspector_hitboxes: FxHashMap::default(), - tab_stops: TabStopMap::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; + 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(test, feature = "test-support"))] + { + self.debug_bounds.clear(); + } - #[cfg(any(feature = "inspector", debug_assertions))] - { - self.next_inspector_instance_ids.clear(); - self.inspector_hitboxes.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 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 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 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); - } - } + 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(); - } + self.scene.finish(); + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] enum InputModality { - Mouse, - Keyboard, + Mouse, + Keyboard, } /// 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, - 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>, - 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, - next_frame_callbacks: Rc>>, - pub(crate) dirty_views: FxHashSet, - focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, - pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, - 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 = "input-latency-histogram")] - input_latency_tracker: InputLatencyTracker, - last_input_modality: InputModality, - pub(crate) refreshing: bool, - pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, - pub(crate) focus: Option, - focus_enabled: bool, - 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>, - pub(crate) a11y: A11y, + pub(crate) handle: AnyWindowHandle, + pub(crate) invalidator: WindowInvalidator, + pub(crate) removed: bool, + pub(crate) platform_window: Box, + display_id: Option, + 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>, + 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, + next_frame_callbacks: Rc>>, + pub(crate) dirty_views: FxHashSet, + focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, + pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, + 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 = "input-latency-histogram")] + input_latency_tracker: InputLatencyTracker, + last_input_modality: InputModality, + pub(crate) refreshing: bool, + pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, + pub(crate) focus: Option, + focus_enabled: bool, + 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>, + pub(crate) a11y: A11y, } #[derive(Clone, Debug, Default)] struct ModifierState { - modifiers: Modifiers, - saw_keystroke: bool, + modifiers: Modifiers, + saw_keystroke: 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, + 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), - } - } + 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); + 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; - } - } + 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 - } + 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); - } + fn prune_old_timestamps(&mut self, now: Instant) { + self.timestamps + .retain(|&t| now.duration_since(t) <= self.window); + } } /// A point-in-time snapshot of the input-latency histograms for a window, /// suitable for external formatting. #[cfg(feature = "input-latency-histogram")] 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, + /// 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, } /// Records the time between when the first input event in a frame is dispatched @@ -1097,644 +1097,644 @@ pub struct InputLatencySnapshot { /// multiple events are coalesced into a single frame. #[cfg(feature = "input-latency-histogram")] struct InputLatencyTracker { - /// Timestamp of the first unrendered input event in the current frame; - /// cleared when a frame is presented. - first_input_at: Option, - /// Count of input events received since the last frame was presented. - pending_input_count: u64, - /// Histogram of input-to-frame latency samples, in nanoseconds. - latency_histogram: Histogram, - /// Histogram of input events coalesced per rendered frame. - events_per_frame_histogram: Histogram, - /// Count of input events that arrived mid-draw and were excluded from - /// latency recording because their effects won't appear until the next frame. - mid_draw_events_dropped: u64, + /// Timestamp of the first unrendered input event in the current frame; + /// cleared when a frame is presented. + first_input_at: Option, + /// Count of input events received since the last frame was presented. + pending_input_count: u64, + /// Histogram of input-to-frame latency samples, in nanoseconds. + latency_histogram: Histogram, + /// Histogram of input events coalesced per rendered frame. + events_per_frame_histogram: Histogram, + /// Count of input events that arrived mid-draw and were excluded from + /// latency recording because their effects won't appear until the next frame. + mid_draw_events_dropped: u64, } #[cfg(feature = "input-latency-histogram")] impl InputLatencyTracker { - fn new() -> Result { - Ok(Self { - first_input_at: None, - pending_input_count: 0, - latency_histogram: Histogram::new(3) - .map_err(|e| anyhow!("Failed to create input latency histogram: {e}"))?, - events_per_frame_histogram: Histogram::new(3) - .map_err(|e| anyhow!("Failed to create events per frame histogram: {e}"))?, - mid_draw_events_dropped: 0, - }) - } + fn new() -> Result { + Ok(Self { + first_input_at: None, + pending_input_count: 0, + latency_histogram: Histogram::new(3) + .map_err(|e| anyhow!("Failed to create input latency histogram: {e}"))?, + events_per_frame_histogram: Histogram::new(3) + .map_err(|e| anyhow!("Failed to create events per frame histogram: {e}"))?, + mid_draw_events_dropped: 0, + }) + } - /// Record that an input event was dispatched at the given time. - /// Only the first event's timestamp per frame is retained (worst-case latency). - fn record_input(&mut self, dispatch_time: Instant) { - self.first_input_at.get_or_insert(dispatch_time); - self.pending_input_count += 1; - } + /// Record that an input event was dispatched at the given time. + /// Only the first event's timestamp per frame is retained (worst-case latency). + fn record_input(&mut self, dispatch_time: Instant) { + self.first_input_at.get_or_insert(dispatch_time); + self.pending_input_count += 1; + } - /// Record that an input event arrived during a draw phase and was excluded - /// from latency tracking. - fn record_mid_draw_input(&mut self) { - self.mid_draw_events_dropped += 1; - } + /// Record that an input event arrived during a draw phase and was excluded + /// from latency tracking. + fn record_mid_draw_input(&mut self) { + self.mid_draw_events_dropped += 1; + } - /// Record that a frame was presented, flushing pending latency and coalescing samples. - fn record_frame_presented(&mut self) { - if let Some(first_input_at) = self.first_input_at.take() { - let latency_nanos = first_input_at.elapsed().as_nanos() as u64; - self.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; - } - } + /// Record that a frame was presented, flushing pending latency and coalescing samples. + fn record_frame_presented(&mut self) { + if let Some(first_input_at) = self.first_input_at.take() { + let latency_nanos = first_input_at.elapsed().as_nanos() as u64; + self.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; + } + } - fn snapshot(&self) -> InputLatencySnapshot { - InputLatencySnapshot { - latency_histogram: self.latency_histogram.clone(), - events_per_frame_histogram: self.events_per_frame_histogram.clone(), - mid_draw_events_dropped: self.mid_draw_events_dropped, - } - } + fn snapshot(&self) -> InputLatencySnapshot { + InputLatencySnapshot { + latency_histogram: self.latency_histogram.clone(), + events_per_frame_histogram: self.events_per_frame_histogram.clone(), + mid_draw_events_dropped: self.mid_draw_events_dropped, + } + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum DrawPhase { - None, - Prepaint, - Paint, - Focus, + None, + Prepaint, + Paint, + Focus, } #[derive(Default, Debug)] struct PendingInput { - keystrokes: SmallVec<[Keystroke; 1]>, - focus: Option, - timer: Option>, - needs_timeout: bool, + keystrokes: SmallVec<[Keystroke; 1]>, + focus: Option, + timer: Option>, + needs_timeout: bool, } pub(crate) struct ElementStateBox { - pub(crate) inner: Box, - #[cfg(debug_assertions)] - pub(crate) type_name: &'static str, + 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()); + // 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; + const CASCADE_OFFSET: f32 = 25.0; - let display = display_id - .map(|id| cx.find_display(id)) - .unwrap_or_else(|| cx.primary_display()); + 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); + 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); + // 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 ( + 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 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 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 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)) + 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, - 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; + pub(crate) fn new( + handle: AnyWindowHandle, + options: WindowOptions, + cx: &mut App, + ) -> Result { + let WindowOptions { + window_bounds, + titlebar, + focus, + show, + kind, + is_movable, + 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 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, - is_resizable, - is_minimizable, - focus, - show, - display_id, - window_min_size, - icon, - #[cfg(target_os = "macos")] - tabbing_identifier, - }, - )?; + 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, + is_resizable, + is_minimizable, + focus, + show, + display_id, + window_min_size, + 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 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(); - 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)); + 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(); + 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); + 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(_) => {} - } + 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)); + 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 initial_tree = accesskit::TreeUpdate { - nodes: vec![(ROOT_NODE_ID, accesskit::Node::new(accesskit::Role::Window))], - 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::(); + #[cfg(not(target_family = "wasm"))] + if !accessibility_force_disabled { + let initial_tree = accesskit::TreeUpdate { + nodes: vec![(ROOT_NODE_ID, accesskit::Node::new(accesskit::Role::Window))], + 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(); - }) - }, - }); + 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(); + // 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 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(); - } + 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(); - move |request_frame_options| { - let thermal_state = handle - .update(&mut cx, |_, _, cx| cx.thermal_state()) - .log_err(); + 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(); + move |request_frame_options| { + 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.force_render - && !request_frame_options.require_presentation - && next_frame_callbacks.borrow().is_empty() - { - None - } else if !active.get() { - Some(Duration::from_micros(33333)) - } else if let Some(ThermalState::Critical | ThermalState::Serious) = thermal_state { - Some(Duration::from_micros(16667)) - } else { - None - }; + // 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.force_render + && !request_frame_options.require_presentation + && next_frame_callbacks.borrow().is_empty() + { + None + } else if !active.get() { + Some(Duration::from_micros(33333)) + } 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 - { - // Must still complete the frame on platforms that require it. - // On Wayland, `surface.frame()` was already called to request the - // next frame callback, so we must call `surface.commit()` (via - // `complete_frame`) or the compositor won't send another callback. - handle - .update(&mut cx, |_, window, _| window.complete_frame()) - .log_err(); - return; - } - } - last_frame_time.set(Some(now)); + 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 + { + // Must still complete the frame on platforms that require it. + // On Wayland, `surface.frame()` was already called to request the + // next frame callback, so we must call `surface.commit()` (via + // `complete_frame`) or the compositor won't send another callback. + handle + .update(&mut cx, |_, window, _| window.complete_frame()) + .log_err(); + return; + } + } + last_frame_time.set(Some(now)); - let next_frame_callbacks = next_frame_callbacks.take(); - if !next_frame_callbacks.is_empty() { - handle - .update(&mut cx, |_, window, cx| { - for callback in next_frame_callbacks { - callback(window, cx); - } - }) - .log_err(); - } + let next_frame_callbacks = next_frame_callbacks.take(); + if !next_frame_callbacks.is_empty() { + handle + .update(&mut cx, |_, window, cx| { + for callback in 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() - || (active.get() && input_rate_tracker.borrow_mut().is_high_rate()); + // 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() + || (active.get() && input_rate_tracker.borrow_mut().is_high_rate()); - if invalidator.is_dirty() || request_frame_options.force_render { - measure("frame duration", || { - handle - .update(&mut cx, |_, window, cx| { - if request_frame_options.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(); - }) - .log_err(); - }) - } else if needs_present { - handle - .update(&mut cx, |_, window, _| window.present()) - .log_err(); - } + if invalidator.is_dirty() || request_frame_options.force_render { + measure("frame duration", || { + handle + .update(&mut cx, |_, window, cx| { + if request_frame_options.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(); + }) + .log_err(); + }) + } else if needs_present { + handle + .update(&mut cx, |_, window, _| window.present()) + .log_err(); + } - handle - .update(&mut cx, |_, window, _| { - window.complete_frame(); - }) - .log_err(); - } - })); - 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 mut cx = cx.to_async(); - move || { - handle - .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) - .log_err(); - } - })); - 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)); + handle + .update(&mut cx, |_, window, _| { + window.complete_frame(); + }) + .log_err(); + } + })); + 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 mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) + .log_err(); + } + })); + 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(); + 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(); - }) - }); + 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); - } + if let Some(app_id) = app_id { + platform_window.set_app_id(&app_id); + } - platform_window.map_window().unwrap(); + platform_window.map_window().unwrap(); - Ok(Window { - handle, - invalidator, - removed: false, - platform_window, - display_id, - 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, - 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(), - 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 = "input-latency-histogram")] - input_latency_tracker: InputLatencyTracker::new()?, - last_input_modality: InputModality::Mouse, - refreshing: false, - activation_observers: SubscriberSet::new(), - focus: None, - focus_enabled: true, - 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, - a11y: A11y::new(a11y_active_flag, accessibility_force_disabled), - }) - } + Ok(Window { + handle, + invalidator, + removed: false, + platform_window, + display_id, + 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, + 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(), + 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 = "input-latency-histogram")] + input_latency_tracker: InputLatencyTracker::new()?, + last_input_modality: InputModality::Mouse, + refreshing: false, + activation_observers: SubscriberSet::new(), + focus: None, + focus_enabled: true, + 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, + a11y: A11y::new(a11y_active_flag, accessibility_force_disabled), + }) + } - pub(crate) fn new_focus_listener( - &self, - value: AnyWindowFocusListener, - ) -> (Subscription, impl FnOnce() + use<>) { - self.focus_listeners.insert((), value) - } + 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, + pub propagate: bool, + pub default_prevented: bool, } /// Indicates which region of the window is visible. Content falling outside of this mask will not be @@ -1743,4386 +1743,4385 @@ pub struct DispatchEventResult { #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[repr(C)] pub struct ContentMask { - /// The bounds - pub bounds: Bounds

, + /// The bounds + pub bounds: Bounds

, } 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), - } - } + /// 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), + } + } - /// Intersect the content mask with the given content mask. - pub fn intersect(&self, other: &Self) -> Self { - let bounds = self.bounds.intersect(&other.bounds); - ContentMask { bounds } - } + /// Intersect the content mask with the given content mask. + pub fn intersect(&self, other: &Self) -> Self { + let bounds = self.bounds.intersect(&other.bounds); + ContentMask { bounds } + } } 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)) - } - - /// 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.clear_pending_keystrokes(); - - // 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(); - }); - - self.refresh(); - } - - /// Remove focus from all elements within this context's window. - pub fn blur(&mut self) { - if !self.focus_enabled { - return; - } - - 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) { - self.blur(); - 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 window's input region to the union of `rects`. Pointer events - /// outside the region pass through to whatever is below the window. - /// An empty slice resets the input region, so the window will receive all - /// pointer events again. (wayland only) - pub fn set_input_region(&self, rects: &[Bounds]) { - self.platform_window.set_input_region(rects); - } - - /// Controls how a surface interacts with surrounding screen space. - /// Positive values reserve space, 0 avoids reserved space, and -1 ignores - /// reserved space and may extend underneath other surfaces. (wayland only) - pub fn set_exclusive_zone(&self, zone: Pixels) { - self.platform_window.set_exclusive_zone(zone); - } - - /// Start a window resize operation (Wayland) - pub fn start_window_resize(&self, edge: ResizeEdge) { - self.platform_window.start_window_resize(edge); - } - - /// 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)); - } - - /// 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. - pub fn request_animation_frame(&self) { - let entity = self.current_view(); - self.on_next_frame(move |_, cx| cx.notify(entity)); - } - - /// 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.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) - } - - /// 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() - } - - 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 which window controls are currently visible (Wayland) - 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); - } - - /// 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 - } - - /// 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(&self) -> ContentMask { - ContentMask { - bounds: self.cover_bounds(self.content_mask().bounds), - } - } - - /// 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 - } - - /// 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 - } - - /// The current state of the keyboard's capslock - pub fn capslock(&self) -> Capslock { - self.capslock - } - - fn complete_frame(&self) { - self.platform_window.completed_frame(); - } - - /// 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 { - // 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); - } - 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) - if let Some(input_handler) = self - .next_frame - .input_handlers - .iter_mut() - .rev() - .find_map(|h| h.take()) - { - self.platform_window.set_input_handler(input_handler); - } - - 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; - - 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_listeners - .clone() - .retain(&(), |listener| listener(self, cx)); - } - - 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); - self.needs_present.set(true); - - ArenaClearNeeded::new(&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) { - self.platform_window.draw(&self.rendered_frame.scene); - #[cfg(feature = "input-latency-histogram")] - self.input_latency_tracker.record_frame_presented(); - self.needs_present.set(false); - profiling::finish_frame!(); - } - - /// Returns a snapshot of the current input-latency histograms. - #[cfg(feature = "input-latency-histogram")] - pub fn input_latency_snapshot(&self) -> InputLatencySnapshot { - self.input_latency_tracker.snapshot() - } - - 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. - let mut root_element = self.root.as_ref().unwrap().clone().into_any(); - 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.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(); - 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 { - // clear the builder state regardless - let tree_update = self.a11y.end_frame(); - - 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(); - 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); - - let mut completed_draws = Vec::new(); - - // Process deferred draws in multiple rounds to support nesting. - // Each round processes all current deferred draws, which may produce new ones. - let mut depth = 0; - loop { - // Limit maximum nesting depth to prevent infinite loops. - assert!(depth < 10, "Exceeded maximum (10) deferred depth"); - depth += 1; - let deferred_count = self.next_frame.deferred_draws.len(); - if deferred_count == 0 { - break; - } - - // Sort by priority for this round - 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 deferred_draw = &mut 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); - self.next_frame - .dispatch_tree - .set_active_node(deferred_draw.parent_node); - - let prepaint_start = self.prepaint_index(); - if let Some(element) = deferred_draw.element.as_mut() { - self.with_rendered_view(deferred_draw.current_view, |window| { - window.with_rem_size(Some(deferred_draw.rem_size), |window| { - window.with_absolute_element_offset( - deferred_draw.absolute_offset, - |window| { - element.prepaint(window, cx); - }, - ); - }); - }) - } else { - self.reuse_prepaint(deferred_draw.prepaint_range.clone()); - } - let prepaint_end = self.prepaint_index(); - deferred_draw.prepaint_range = prepaint_start..prepaint_end; - } - - // Save completed draws and continue with newly added ones - completed_draws.append(&mut deferred_draws); - - self.element_id_stack.clear(); - self.text_style_stack.clear(); - } - - // Restore all completed draws - self.next_frame.deferred_draws = completed_draws; - } - - 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; - } - - // Deferred draws are overlays (tooltips, popovers, drag images) and must sort above the - // whole main scene. Raise the order floor so they do — this also keeps a deferred - // backdrop's order from falling inside a content-filter order range left by the main scene. - self.next_frame.scene.raise_order_floor(); - - 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; - 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, - 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.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) -> ContentMask { - 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, - }, - }) - } - - /// 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. - /// - /// This method should only be called during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. - 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 during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. - 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 - } - } - - /// Creates a new transition with automatic state management. - /// - /// The state for this transition is managed internally and will be recreated - /// on each render. For persistent state across renders, use [`use_keyed_transition`](Self::use_keyed_transition). - pub fn use_transition( - &mut self, - cx: &mut App, - duration: Duration, - init: impl Fn(&mut Window, &mut Context>) -> T, - ) -> Transition { - let state = self.use_state(cx, |window, cx| TransitionState::new(init(window, cx))); - - Transition::new(state, duration) - } - - /// Creates a new keyed transition with persistent state. - /// - /// The state for this transition is associated with the provided key and will - /// persist across renders as long as the key remains the same. This is the - /// recommended method for most use cases where you want smooth, continuous - /// animations. - /// - /// This method should only be called during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. - pub fn use_keyed_transition( - &mut self, - key: impl Into, - cx: &mut App, - duration: Duration, - init: impl Fn(&mut Window, &mut Context>) -> T, - ) -> Transition { - let state = - self.use_keyed_state(key, cx, |window, cx| TransitionState::new(init(window, cx))); - - Transition::new(state, duration) - } - - /// 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, - }); - } - } - - /// Paint a backdrop filter into the scene for the next frame at the current z-index. The - /// renderer blurs the content already painted behind `bounds` and composites the result - /// into the rounded rectangle described by `bounds` and `corner_radii` — the CSS - /// `backdrop-filter` effect (frosted glass). Typically the element then paints a translucent - /// background quad on top so its color tints the blurred backdrop. - /// - /// Does nothing when `filters` produce no visible blur. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_backdrop_filter( - &mut self, - bounds: Bounds, - corner_radii: Corners, - filters: &[Filter], - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let filters: SmallVec<[ScaledFilter; 4]> = filters - .iter() - .filter(|filter| !filter.is_identity()) - .map(|filter| filter.scale(scale_factor)) - .collect(); - if filters.is_empty() { - return; - } - - self.next_frame.scene.insert_primitive(BackdropFilter { - order: 0, - bounds: self.snap_bounds(bounds), - content_mask: self.snapped_content_mask(), - corner_radii: corner_radii.scale(scale_factor), - filters, - opacity: self.element_opacity(), - }); - } - - /// Isolate the painting performed by `f` into a content-filter group: the renderer renders - /// everything `f` paints into an offscreen target, blurs it as a single layer, and - /// composites the result back into the rounded rectangle described by `bounds` and - /// `corner_radii` — the CSS `filter` effect (e.g. blurring an element and its children). - /// - /// When `filters` produce no visible blur this simply runs `f` with no offscreen - /// indirection. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn with_filter_layer( - &mut self, - bounds: Bounds, - corner_radii: Corners, - filters: &[Filter], - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let filters: SmallVec<[ScaledFilter; 4]> = filters - .iter() - .filter(|filter| !filter.is_identity()) - .map(|filter| filter.scale(scale_factor)) - .collect(); - if filters.is_empty() { - return f(self); - } - - // Snapshot the (scaled) group parameters once so the start and end markers agree. - // - // `opacity` is 1.0 — NOT `element_opacity()`. The group's children/bg/border are painted - // through the normal paint methods while `element_opacity` is still in effect, so they - // already carry the element's opacity (consistent with gpui's per-primitive opacity for - // non-filtered elements). Re-applying it at composite time would double it (e.g. - // `.blur(r).opacity(0.5)` would render at 0.25 instead of 0.5). - let boundary = FilterBoundary { - order: 0, - bounds: self.snap_bounds(bounds), - content_mask: self.snapped_content_mask(), - corner_radii: corner_radii.scale(scale_factor), - filters, - opacity: 1.0, - is_start: true, - }; - - self.next_frame.scene.insert_primitive(boundary.clone()); - let result = f(self); - self.next_frame.scene.insert_primitive(FilterBoundary { - is_start: false, - ..boundary - }); - - result - } - - /// 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); - self.next_frame.scene.insert_primitive(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, - }); - } - - /// 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.content_mask(); - let opacity = self.element_opacity(); - path.content_mask = content_mask; - let color: Background = color.into(); - path.color = color.opacity(opacity); - self.next_frame - .scene - .insert_primitive(path.scale(scale_factor)); - } - - /// 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(); - - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask: self.snapped_content_mask(), - color: style.color.unwrap_or_default().opacity(element_opacity), - thickness, - wavy: if style.wavy { 1 } else { 0 }, - }); - } - - /// 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(); - - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask: self.snapped_content_mask(), - thickness: self.snap_stroke(style.thickness), - color: style.color.unwrap_or_default().opacity(opacity), - wavy: 0, - }); - } - - /// 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, - 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. - pub fn paint_image( - &mut self, - bounds: Bounds, - corner_radii: Corners, - data: Arc, - frame_index: usize, - grayscale: bool, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let bounds = self.snap_bounds(bounds); - 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 content_mask = self.snapped_content_mask(); - let corner_radii = corner_radii.scale(self.scale_factor()); - let opacity = self.element_opacity(); - - self.next_frame.scene.insert_primitive(PolychromeSprite { - order: 0, - pad: 0, - grayscale, - bounds, - content_mask, - corner_radii, - 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, - }); - } - - /// 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(any(target_os = "linux", target_os = "freebsd"))] - pub fn paint_surface( - &mut self, - bounds: Bounds, - texture: std::sync::Arc, - texture_size: Size, - ) { - use crate::PaintSurface; - - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - let content_mask = self.content_mask().scale(scale_factor); - self.next_frame.scene.insert_primitive(PaintSurface { - order: 0, - bounds, - content_mask, - texture, - texture_size, - }); - } - - /// 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(()) - } - - /// 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)))); - } - } - - /// 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 or keyboard event on the window. - #[profiling::function] - pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { - #[cfg(feature = "input-latency-histogram")] - let dispatch_time = Instant::now(); - 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, - _ => 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; - if 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, - }); - } - 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 => { - cx.active_drag.take(); - PlatformInput::FileDrop(FileDropEvent::Exited) - } - }, - 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); - } - - if self.invalidator.update_count() > update_count_before { - self.input_rate_tracker.borrow_mut().record_input(); - #[cfg(feature = "input-latency-histogram")] - if self.invalidator.not_drawing() { - self.input_latency_tracker.record_input(dispatch_time); - } else { - self.input_latency_tracker.record_mid_draw_input(); - } - } - - DispatchEventResult { - propagate: cx.propagate_event, - default_prevented: self.default_prevented, - } - } - - 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(); - } - - 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_keystroke - { - 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_keystroke = false - } - self.pending_modifier.modifiers = event.modifiers - } else if let Some(key_down_event) = event.downcast_ref::() { - self.pending_modifier.saw_keystroke = 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() { - currently_pending.timer.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 - }); - - currently_pending.needs_timeout |= - match_result.pending_has_binding || text_input_requires_timeout; - - if currently_pending.needs_timeout { - currently_pending.timer = Some(self.spawn(cx, async move |cx| { - cx.background_executor.timer(Duration::from_secs(1)).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(); - })); - } else { - currently_pending.timer = 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 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 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() - } - - pub(crate) fn clear_pending_keystrokes(&mut self) { - self.pending_input.take(); - } - - /// 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 - .as_ref() - .map(|pending_input| pending_input.keystrokes.as_slice()) - } - - 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 { - listener(action.as_any(), DispatchPhase::Capture, cx); - 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() { - listener(any_action, DispatchPhase::Capture, self, cx); - - 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 - listener(any_action, DispatchPhase::Bubble, self, cx); - - 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 - - listener(action.as_any(), DispatchPhase::Bubble, cx); - 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(); - } - - /// 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(); - } - - /// 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 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() - } - - /// Returns the GPU context (device + queue) if available. - /// The returned `Box` contains `(Arc, Arc)`. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub fn gpu_context(&self) -> Option> { - self.platform_window.gpu_context() - } - - /// Whether the GPU device backing this window has been lost (recovery - /// happens on a subsequent platform draw). `None` when the backend - /// cannot know. Embedders that captured the device from - /// [`Self::gpu_context`] should stop submitting while this is - /// `Some(true)` and re-acquire the device once it reads `Some(false)`. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub fn gpu_device_lost(&self) -> Option { - self.platform_window.gpu_device_lost() - } - - /// Perform titlebar double-click action. - /// This is macOS specific. - pub fn titlebar_double_click(&self) { - self.platform_window.titlebar_double_click(); - } - - /// 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() - } - - /// 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(); - } - _ => { - 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); - } + 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)) + } + + /// 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.clear_pending_keystrokes(); + + // 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(); + }); + + self.refresh(); + } + + /// Remove focus from all elements within this context's window. + pub fn blur(&mut self) { + if !self.focus_enabled { + return; + } + + 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) { + self.blur(); + 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 window's input region to the union of `rects`. Pointer events + /// outside the region pass through to whatever is below the window. + /// An empty slice resets the input region, so the window will receive all + /// pointer events again. (wayland only) + pub fn set_input_region(&self, rects: &[Bounds]) { + self.platform_window.set_input_region(rects); + } + + /// Controls how a surface interacts with surrounding screen space. + /// Positive values reserve space, 0 avoids reserved space, and -1 ignores + /// reserved space and may extend underneath other surfaces. (wayland only) + pub fn set_exclusive_zone(&self, zone: Pixels) { + self.platform_window.set_exclusive_zone(zone); + } + + /// Start a window resize operation (Wayland) + pub fn start_window_resize(&self, edge: ResizeEdge) { + self.platform_window.start_window_resize(edge); + } + + /// 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)); + } + + /// 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. + pub fn request_animation_frame(&self) { + let entity = self.current_view(); + self.on_next_frame(move |_, cx| cx.notify(entity)); + } + + /// 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.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) + } + + /// 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() + } + + 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 which window controls are currently visible (Wayland) + 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); + } + + /// 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 + } + + /// 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(&self) -> ContentMask { + ContentMask { + bounds: self.cover_bounds(self.content_mask().bounds), + } + } + + /// 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 + } + + /// 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 + } + + /// The current state of the keyboard's capslock + pub fn capslock(&self) -> Capslock { + self.capslock + } + + fn complete_frame(&self) { + self.platform_window.completed_frame(); + } + + /// 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 { + // 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); + } + 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) + if let Some(input_handler) = self + .next_frame + .input_handlers + .iter_mut() + .rev() + .find_map(|h| h.take()) + { + self.platform_window.set_input_handler(input_handler); + } + + 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; + + 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_listeners + .clone() + .retain(&(), |listener| listener(self, cx)); + } + + 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); + self.needs_present.set(true); + + ArenaClearNeeded::new(&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) { + self.platform_window.draw(&self.rendered_frame.scene); + #[cfg(feature = "input-latency-histogram")] + self.input_latency_tracker.record_frame_presented(); + self.needs_present.set(false); + profiling::finish_frame!(); + } + + /// Returns a snapshot of the current input-latency histograms. + #[cfg(feature = "input-latency-histogram")] + pub fn input_latency_snapshot(&self) -> InputLatencySnapshot { + self.input_latency_tracker.snapshot() + } + + 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. + let mut root_element = self.root.as_ref().unwrap().clone().into_any(); + 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.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(); + 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 { + // clear the builder state regardless + let tree_update = self.a11y.end_frame(); + + 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(); + 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); + + let mut completed_draws = Vec::new(); + + // Process deferred draws in multiple rounds to support nesting. + // Each round processes all current deferred draws, which may produce new ones. + let mut depth = 0; + loop { + // Limit maximum nesting depth to prevent infinite loops. + assert!(depth < 10, "Exceeded maximum (10) deferred depth"); + depth += 1; + let deferred_count = self.next_frame.deferred_draws.len(); + if deferred_count == 0 { + break; + } + + // Sort by priority for this round + 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 deferred_draw = &mut 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); + self.next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let prepaint_start = self.prepaint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_rendered_view(deferred_draw.current_view, |window| { + window.with_rem_size(Some(deferred_draw.rem_size), |window| { + window.with_absolute_element_offset( + deferred_draw.absolute_offset, + |window| { + element.prepaint(window, cx); + }, + ); + }); + }) + } else { + self.reuse_prepaint(deferred_draw.prepaint_range.clone()); + } + let prepaint_end = self.prepaint_index(); + deferred_draw.prepaint_range = prepaint_start..prepaint_end; + } + + // Save completed draws and continue with newly added ones + completed_draws.append(&mut deferred_draws); + + self.element_id_stack.clear(); + self.text_style_stack.clear(); + } + + // Restore all completed draws + self.next_frame.deferred_draws = completed_draws; + } + + 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; + } + + // Deferred draws are overlays (tooltips, popovers, drag images) and must sort above the + // whole main scene. Raise the order floor so they do — this also keeps a deferred + // backdrop's order from falling inside a content-filter order range left by the main scene. + self.next_frame.scene.raise_order_floor(); + + 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; + 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, + 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.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) -> ContentMask { + 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, + }, + }) + } + + /// 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. + /// + /// This method should only be called during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. + 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 during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. + 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 + } + } + + /// Creates a new transition with automatic state management. + /// + /// The state for this transition is managed internally and will be recreated + /// on each render. For persistent state across renders, use [`use_keyed_transition`](Self::use_keyed_transition). + pub fn use_transition( + &mut self, + cx: &mut App, + duration: Duration, + init: impl Fn(&mut Window, &mut Context>) -> T, + ) -> Transition { + let state = self.use_state(cx, |window, cx| TransitionState::new(init(window, cx))); + + Transition::new(state, duration) + } + + /// Creates a new keyed transition with persistent state. + /// + /// The state for this transition is associated with the provided key and will + /// persist across renders as long as the key remains the same. This is the + /// recommended method for most use cases where you want smooth, continuous + /// animations. + /// + /// This method should only be called during `Render::render`, `RenderOnce::render`, or the drawing functions of `Element`. + pub fn use_keyed_transition( + &mut self, + key: impl Into, + cx: &mut App, + duration: Duration, + init: impl Fn(&mut Window, &mut Context>) -> T, + ) -> Transition { + let state = + self.use_keyed_state(key, cx, |window, cx| TransitionState::new(init(window, cx))); + + Transition::new(state, duration) + } + + /// 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, + }); + } + } + + /// Paint a backdrop filter into the scene for the next frame at the current z-index. The + /// renderer blurs the content already painted behind `bounds` and composites the result + /// into the rounded rectangle described by `bounds` and `corner_radii` — the CSS + /// `backdrop-filter` effect (frosted glass). Typically the element then paints a translucent + /// background quad on top so its color tints the blurred backdrop. + /// + /// Does nothing when `filters` produce no visible blur. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_backdrop_filter( + &mut self, + bounds: Bounds, + corner_radii: Corners, + filters: &[Filter], + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let filters: SmallVec<[ScaledFilter; 4]> = filters + .iter() + .filter(|filter| !filter.is_identity()) + .map(|filter| filter.scale(scale_factor)) + .collect(); + if filters.is_empty() { + return; + } + + self.next_frame.scene.insert_primitive(BackdropFilter { + order: 0, + bounds: self.snap_bounds(bounds), + content_mask: self.snapped_content_mask(), + corner_radii: corner_radii.scale(scale_factor), + filters, + opacity: self.element_opacity(), + }); + } + + /// Isolate the painting performed by `f` into a content-filter group: the renderer renders + /// everything `f` paints into an offscreen target, blurs it as a single layer, and + /// composites the result back into the rounded rectangle described by `bounds` and + /// `corner_radii` — the CSS `filter` effect (e.g. blurring an element and its children). + /// + /// When `filters` produce no visible blur this simply runs `f` with no offscreen + /// indirection. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn with_filter_layer( + &mut self, + bounds: Bounds, + corner_radii: Corners, + filters: &[Filter], + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let filters: SmallVec<[ScaledFilter; 4]> = filters + .iter() + .filter(|filter| !filter.is_identity()) + .map(|filter| filter.scale(scale_factor)) + .collect(); + if filters.is_empty() { + return f(self); + } + + // Snapshot the (scaled) group parameters once so the start and end markers agree. + // + // `opacity` is 1.0 — NOT `element_opacity()`. The group's children/bg/border are painted + // through the normal paint methods while `element_opacity` is still in effect, so they + // already carry the element's opacity (consistent with gpui's per-primitive opacity for + // non-filtered elements). Re-applying it at composite time would double it (e.g. + // `.blur(r).opacity(0.5)` would render at 0.25 instead of 0.5). + let boundary = FilterBoundary { + order: 0, + bounds: self.snap_bounds(bounds), + content_mask: self.snapped_content_mask(), + corner_radii: corner_radii.scale(scale_factor), + filters, + opacity: 1.0, + is_start: true, + }; + + self.next_frame.scene.insert_primitive(boundary.clone()); + let result = f(self); + self.next_frame.scene.insert_primitive(FilterBoundary { + is_start: false, + ..boundary + }); + + result + } + + /// 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); + self.next_frame.scene.insert_primitive(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, + }); + } + + /// 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.content_mask(); + let opacity = self.element_opacity(); + path.content_mask = content_mask; + let color: Background = color.into(); + path.color = color.opacity(opacity); + self.next_frame + .scene + .insert_primitive(path.scale(scale_factor)); + } + + /// 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(); + + self.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds, + content_mask: self.snapped_content_mask(), + color: style.color.unwrap_or_default().opacity(element_opacity), + thickness, + wavy: if style.wavy { 1 } else { 0 }, + }); + } + + /// 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(); + + self.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds, + content_mask: self.snapped_content_mask(), + thickness: self.snap_stroke(style.thickness), + color: style.color.unwrap_or_default().opacity(opacity), + wavy: 0, + }); + } + + /// 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, + 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. + pub fn paint_image( + &mut self, + bounds: Bounds, + corner_radii: Corners, + data: Arc, + frame_index: usize, + grayscale: bool, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let bounds = self.snap_bounds(bounds); + 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 content_mask = self.snapped_content_mask(); + let corner_radii = corner_radii.scale(self.scale_factor()); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(PolychromeSprite { + order: 0, + pad: 0, + grayscale, + bounds, + content_mask, + corner_radii, + 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, + }); + } + + /// 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(any(target_os = "linux", target_os = "freebsd"))] + pub fn paint_surface( + &mut self, + bounds: Bounds, + texture: std::sync::Arc, + texture_size: Size, + ) { + use crate::PaintSurface; + + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + self.next_frame.scene.insert_primitive(PaintSurface { + order: 0, + bounds, + content_mask, + texture, + texture_size, + }); + } + + /// 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(()) + } + + /// 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)))); + } + } + + /// 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 or keyboard event on the window. + #[profiling::function] + pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { + #[cfg(feature = "input-latency-histogram")] + let dispatch_time = Instant::now(); + 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, + _ => 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; + if 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, + }); + } + 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 => { + cx.active_drag.take(); + PlatformInput::FileDrop(FileDropEvent::Exited) + } + }, + 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); + } + + if self.invalidator.update_count() > update_count_before { + self.input_rate_tracker.borrow_mut().record_input(); + #[cfg(feature = "input-latency-histogram")] + if self.invalidator.not_drawing() { + self.input_latency_tracker.record_input(dispatch_time); + } else { + self.input_latency_tracker.record_mid_draw_input(); + } + } + + DispatchEventResult { + propagate: cx.propagate_event, + default_prevented: self.default_prevented, + } + } + + 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(); + } + + 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_keystroke + { + 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_keystroke = false + } + self.pending_modifier.modifiers = event.modifiers + } else if let Some(key_down_event) = event.downcast_ref::() { + self.pending_modifier.saw_keystroke = 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() { + currently_pending.timer.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 + }); + + currently_pending.needs_timeout |= + match_result.pending_has_binding || text_input_requires_timeout; + + if currently_pending.needs_timeout { + currently_pending.timer = Some(self.spawn(cx, async move |cx| { + cx.background_executor.timer(Duration::from_secs(1)).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(); + })); + } else { + currently_pending.timer = 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 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 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() + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + self.pending_input.take(); + } + + /// 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 + .as_ref() + .map(|pending_input| pending_input.keystrokes.as_slice()) + } + + 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 { + listener(action.as_any(), DispatchPhase::Capture, cx); + 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() { + listener(any_action, DispatchPhase::Capture, self, cx); + + 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 + listener(any_action, DispatchPhase::Bubble, self, cx); + + 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 + + listener(action.as_any(), DispatchPhase::Bubble, cx); + 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(); + } + + /// 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(); + } + + /// 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 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() + } + + /// Returns the GPU context (device + queue) if available. + /// The returned `Box` contains `(Arc, Arc)`. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn gpu_context(&self) -> Option> { + self.platform_window.gpu_context() + } + + /// Whether the GPU device backing this window has been lost (recovery + /// happens on a subsequent platform draw). `None` when the backend + /// cannot know. Embedders that captured the device from + /// [`Self::gpu_context`] should stop submitting while this is + /// `Some(true)` and re-acquire the device once it reads `Some(false)`. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn gpu_device_lost(&self) -> Option { + self.platform_window.gpu_device_lost() + } + + /// Perform titlebar double-click action. + /// This is macOS specific. + pub fn titlebar_double_click(&self) { + self.platform_window.titlebar_double_click(); + } + + /// 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() + } + + /// 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(); + } + _ => { + 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; + /// 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() - } + /// 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)) - } + 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>, + #[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() - } + 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::(), - }, - state_type: PhantomData, - } - } + /// 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::(), + }, + 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")) - })? - } + /// 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"))?; + /// 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))) - })? - } + 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"))?; + /// 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)) - } + 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 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) - } + /// 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() - } + /// 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 - } + fn clone(&self) -> Self { + *self + } } impl PartialEq for WindowHandle { - fn eq(&self, other: &Self) -> bool { - self.any_handle == other.any_handle - } + 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); - } + fn hash(&self, state: &mut H) { + self.any_handle.hash(state); + } } impl From> for AnyWindowHandle { - fn from(val: WindowHandle) -> Self { - val.any_handle - } + 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, + pub(crate) id: WindowId, + state_type: TypeId, } impl AnyWindowHandle { - /// Get the ID of this window. - pub fn window_id(&self) -> WindowId { - self.id - } + /// Get the ID of this window. + pub fn window_id(&self) -> WindowId { + self.id + } - /// 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 - } - } + /// 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) - } + /// 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")?; + /// 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) - } + cx.read_window(&view, read) + } } impl HasWindowHandle for Window { - fn window_handle(&self) -> Result, HandleError> { - self.platform_window.window_handle() - } + 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() - } + fn display_handle( + &self, + ) -> std::result::Result, HandleError> { + self.platform_window.display_handle() + } } /// An identifier for an [`Element`]. @@ -6131,263 +6130,263 @@ impl HasDisplayHandle for Window { /// 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]), + /// 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) - } + /// 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)?, - } + 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(()) - } + Ok(()) + } } impl TryInto for ElementId { - type Error = anyhow::Error; + 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") - } - } + 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) - } + fn from(id: usize) -> Self { + ElementId::Integer(id as u64) + } } impl From for ElementId { - fn from(id: i32) -> Self { - Self::Integer(id as u64) - } + fn from(id: i32) -> Self { + Self::Integer(id as u64) + } } impl From for ElementId { - fn from(name: SharedString) -> Self { - ElementId::Name(name) - } + fn from(name: SharedString) -> Self { + ElementId::Name(name) + } } impl From for ElementId { - fn from(name: String) -> Self { - ElementId::Name(name.into()) - } + fn from(name: String) -> Self { + ElementId::Name(name.into()) + } } impl From> for ElementId { - fn from(name: Arc) -> Self { - ElementId::Name(name.into()) - } + fn from(name: Arc) -> Self { + ElementId::Name(name.into()) + } } impl From> for ElementId { - fn from(path: Arc) -> Self { - ElementId::Path(path) - } + 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)) - } + 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) - } + 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()) - } + 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) - } + 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) - } + 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) - } + 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) - } + 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)) - } + 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()) - } + 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) - } + 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) - } + 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, + /// 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 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 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 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 - } - } + /// 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, + 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, - } + 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(), - } + 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, + 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, - } + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: transparent_black().into(), + border_widths: (1.).into(), + border_color: border_color.into(), + border_style, + } } diff --git a/crates/gpui/src/window/a11y.rs b/crates/gpui/src/window/a11y.rs index dfb818ead9..ccefd270d8 100644 --- a/crates/gpui/src/window/a11y.rs +++ b/crates/gpui/src/window/a11y.rs @@ -91,8 +91,8 @@ use accesskit::{Action, NodeId, TreeUpdate}; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, + Arc, + atomic::{AtomicBool, Ordering}, }; /// The fixed AccessKit node ID used for the root of every window's a11y tree. @@ -100,262 +100,262 @@ 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>; + 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>, + /// 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>, } impl A11y { - pub(crate) fn new(active_flag: Arc, force_disabled: bool) -> 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(), - } - } + pub(crate) fn new(active_flag: Arc, force_disabled: bool) -> 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(), + } + } - /// 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); - } + /// 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 is_active(&self) -> bool { + self.active_this_frame + } - /// 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(); - } + /// 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(); + } - /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter. - pub(crate) fn end_frame(&mut self) -> TreeUpdate { - let tree_update = self.nodes.finalize(); + /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter. + pub(crate) fn end_frame(&mut self) -> TreeUpdate { + let tree_update = self.nodes.finalize(); - // Zed currently doesn't set any a11y APIs on *any* UI elements, so a - // tree with nodes other than the root indicates a bug in the - // `TreeUpdate`-producing logic. - // - // Remove this when adding aria attributes. - if tree_update.nodes.len() > 1 { - log::warn!( - "expected an empty a11y tree update (only the root node), but got {} nodes; Zed has no accessible UI elements yet", - tree_update.nodes.len() - ); - } + // Zed currently doesn't set any a11y APIs on *any* UI elements, so a + // tree with nodes other than the root indicates a bug in the + // `TreeUpdate`-producing logic. + // + // Remove this when adding aria attributes. + if tree_update.nodes.len() > 1 { + log::warn!( + "expected an empty a11y tree update (only the root node), but got {} nodes; Zed has no accessible UI elements yet", + tree_update.nodes.len() + ); + } - tree_update - } + tree_update + } } 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, - focus: NodeId, - #[cfg(debug_assertions)] - has_set_focus: bool, + 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, + focus: NodeId, + #[cfg(debug_assertions)] + has_set_focus: bool, } impl A11yNodeBuilder { - fn new() -> Self { - Self { - ids_stack: SmallVec::new(), - nodes_stack: SmallVec::new(), - all_nodes: Vec::new(), - seen_ids: FxHashSet::default(), - focus: ROOT_NODE_ID, - #[cfg(debug_assertions)] - has_set_focus: false, - } - } + fn new() -> Self { + Self { + ids_stack: SmallVec::new(), + nodes_stack: SmallVec::new(), + all_nodes: Vec::new(), + seen_ids: FxHashSet::default(), + focus: ROOT_NODE_ID, + #[cfg(debug_assertions)] + has_set_focus: false, + } + } - /// 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 { - debug_assert!(!self.ids_stack.is_empty(), "push called before push_root"); + /// 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 { + debug_assert!(!self.ids_stack.is_empty(), "push called 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." - ); - // We need to return `false` here because inserting a duplicate - // node will cause a panic in accesskit - return false; - } + 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." + ); + // We need to return `false` here because inserting a duplicate + // node will cause a panic in accesskit + 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 - } + if let Some(parent) = self.nodes_stack.last_mut() { + parent.push_child(id); + } + self.ids_stack.push(id); + self.nodes_stack.push(node); + true + } - /// 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"); + /// 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)); - } - } + 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) { - self.all_nodes.clear(); - self.ids_stack.clear(); - self.nodes_stack.clear(); - self.seen_ids.clear(); - #[cfg(debug_assertions)] - { - self.has_set_focus = false; - } - let root_node = accesskit::Node::new(accesskit::Role::Window); + /// Push the root node to start a new frame. + fn begin_frame(&mut self) { + self.all_nodes.clear(); + self.ids_stack.clear(); + self.nodes_stack.clear(); + self.seen_ids.clear(); + #[cfg(debug_assertions)] + { + self.has_set_focus = false; + } + let root_node = accesskit::Node::new(accesskit::Role::Window); - self.ids_stack.push(ROOT_NODE_ID); - self.nodes_stack.push(root_node); - self.focus = ROOT_NODE_ID; - } + self.ids_stack.push(ROOT_NODE_ID); + self.nodes_stack.push(root_node); + self.focus = ROOT_NODE_ID; + } - /// 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 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) + } - /// Set the focused node for this frame. - pub(crate) fn set_focus(&mut self, id: NodeId) { - #[cfg(debug_assertions)] - { - debug_assert!( - !self.has_set_focus, - "set_focus called more than once in a single frame" - ); - self.has_set_focus = true; - } - self.focus = id; - } + /// Set the focused node for this frame. + pub(crate) fn set_focus(&mut self, id: NodeId) { + #[cfg(debug_assertions)] + { + debug_assert!( + !self.has_set_focus, + "set_focus called more than once in a single frame" + ); + self.has_set_focus = true; + } + self.focus = 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); + 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 {}. \ + 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() - ); - } + 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)); - } - } + // 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 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.focus, - }; + 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.focus, + }; - Self::repair_tree_update(update) - } + 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(); + /// 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). \ + // 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; - } + 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. \ + // 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); - } - } + id, + invalid_count + ); + let valid: Vec = children + .iter() + .copied() + .filter(|child_id| node_ids.contains(child_id)) + .collect(); + node.set_children(valid); + } + } - update - } + update + } } diff --git a/crates/gpui/src/window/prompts.rs b/crates/gpui/src/window/prompts.rs index 980c6f6812..71b3b89d06 100644 --- a/crates/gpui/src/window/prompts.rs +++ b/crates/gpui/src/window/prompts.rs @@ -3,9 +3,9 @@ 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, + AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render, + StatefulInteractiveElement, Styled, div, opaque_grey, white, }; use super::Window; @@ -22,211 +22,211 @@ impl + Focusable> Prompt for V {} /// A handle to a prompt that can be used to interact with it. pub struct PromptHandle { - sender: oneshot::Sender, + sender: oneshot::Sender, } impl PromptHandle { - pub(crate) fn new(sender: oneshot::Sender) -> Self { - Self { sender } - } + 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(); + /// 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); + window.focus(&view.focus_handle(cx), cx); - RenderablePromptHandle { - view: Box::new(view), - } - } + RenderablePromptHandle { + view: Box::new(view), + } + } } /// A prompt handle capable of being rendered in a window. pub struct RenderablePromptHandle { - pub(crate) view: Box, + 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, + 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(), - }); + 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) + 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, + _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(); - })) - })); + 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), - ), - ) - } + 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() - } + fn focus_handle(&self, _: &crate::App) -> FocusHandle { + self.focus.clone() + } } pub(crate) trait PromptViewHandle { - fn any_view(&self) -> AnyView; + fn any_view(&self) -> AnyView; } impl PromptViewHandle for Entity { - fn any_view(&self) -> AnyView { - self.clone().into() - } + 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, - >, - ), + 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; + 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(), - } - } + fn deref(&self) -> &Self::Target { + match self { + Self::Default => &fallback_prompt_renderer, + Self::Custom(f) => f.as_ref(), + } + } } diff --git a/crates/gpui/tests/action_macros.rs b/crates/gpui/tests/action_macros.rs index 66ef6fba2c..c1fedf3747 100644 --- a/crates/gpui/tests/action_macros.rs +++ b/crates/gpui/tests/action_macros.rs @@ -5,51 +5,51 @@ use serde::Deserialize; #[test] fn test_action_macros() { - actions!( - test_only, - [ - SomeAction, - /// Documented action - SomeActionWithDocs, - ] - ); + 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, 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 {} + #[derive(PartialEq, Clone, gpui::private::serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct RegisterableAction {} - register_action!(RegisterableAction); + register_action!(RegisterableAction); - impl gpui::Action for RegisterableAction { - fn boxed_clone(&self) -> Box { - unimplemented!() - } + impl gpui::Action for RegisterableAction { + fn boxed_clone(&self) -> Box { + unimplemented!() + } - fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { - unimplemented!() - } + fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { + unimplemented!() + } - fn name(&self) -> &'static str { - unimplemented!() - } + fn name(&self) -> &'static str { + unimplemented!() + } - fn name_for_type() -> &'static str - where - Self: Sized, - { - unimplemented!() - } + fn name_for_type() -> &'static str + where + Self: Sized, + { + unimplemented!() + } - fn build(_value: serde_json::Value) -> anyhow::Result> - where - Self: Sized, - { - unimplemented!() - } - } + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + unimplemented!() + } + } } diff --git a/crates/gpui_ce_util/src/arc_cow.rs b/crates/gpui_ce_util/src/arc_cow.rs index 41040671bc..4c813a197a 100644 --- a/crates/gpui_ce_util/src/arc_cow.rs +++ b/crates/gpui_ce_util/src/arc_cow.rs @@ -1,141 +1,141 @@ use std::{ - borrow::Cow, - cmp::Ordering, - fmt::{self, Debug}, - hash::{Hash, Hasher}, - sync::Arc, + borrow::Cow, + cmp::Ordering, + fmt::{self, Debug}, + hash::{Hash, Hasher}, + sync::Arc, }; pub enum ArcCow<'a, T: ?Sized> { - Borrowed(&'a T), - Owned(Arc), + Borrowed(&'a T), + Owned(Arc), } impl PartialEq for ArcCow<'_, T> { - fn eq(&self, other: &Self) -> bool { - let a = self.as_ref(); - let b = other.as_ref(); - a == b - } + fn eq(&self, other: &Self) -> bool { + let a = self.as_ref(); + let b = other.as_ref(); + a == b + } } impl PartialOrd for ArcCow<'_, T> { - fn partial_cmp(&self, other: &Self) -> Option { - self.as_ref().partial_cmp(other.as_ref()) - } + fn partial_cmp(&self, other: &Self) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } } impl Ord for ArcCow<'_, T> { - fn cmp(&self, other: &Self) -> Ordering { - self.as_ref().cmp(other.as_ref()) - } + fn cmp(&self, other: &Self) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } } impl Eq for ArcCow<'_, T> {} impl Hash for ArcCow<'_, T> { - fn hash(&self, state: &mut H) { - match self { - Self::Borrowed(borrowed) => Hash::hash(borrowed, state), - Self::Owned(owned) => Hash::hash(&**owned, state), - } - } + fn hash(&self, state: &mut H) { + match self { + Self::Borrowed(borrowed) => Hash::hash(borrowed, state), + Self::Owned(owned) => Hash::hash(&**owned, state), + } + } } impl Clone for ArcCow<'_, T> { - fn clone(&self) -> Self { - match self { - Self::Borrowed(borrowed) => Self::Borrowed(borrowed), - Self::Owned(owned) => Self::Owned(owned.clone()), - } - } + fn clone(&self) -> Self { + match self { + Self::Borrowed(borrowed) => Self::Borrowed(borrowed), + Self::Owned(owned) => Self::Owned(owned.clone()), + } + } } impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> { - fn from(s: &'a T) -> Self { - Self::Borrowed(s) - } + fn from(s: &'a T) -> Self { + Self::Borrowed(s) + } } impl From> for ArcCow<'_, T> { - fn from(s: Arc) -> Self { - Self::Owned(s) - } + fn from(s: Arc) -> Self { + Self::Owned(s) + } } impl From<&'_ Arc> for ArcCow<'_, T> { - fn from(s: &'_ Arc) -> Self { - Self::Owned(s.clone()) - } + fn from(s: &'_ Arc) -> Self { + Self::Owned(s.clone()) + } } impl From for ArcCow<'_, str> { - fn from(value: String) -> Self { - Self::Owned(value.into()) - } + fn from(value: String) -> Self { + Self::Owned(value.into()) + } } impl From<&String> for ArcCow<'_, str> { - fn from(value: &String) -> Self { - Self::Owned(value.clone().into()) - } + fn from(value: &String) -> Self { + Self::Owned(value.clone().into()) + } } impl<'a> From> for ArcCow<'a, str> { - fn from(value: Cow<'a, str>) -> Self { - match value { - Cow::Borrowed(borrowed) => Self::Borrowed(borrowed), - Cow::Owned(owned) => Self::Owned(owned.into()), - } - } + fn from(value: Cow<'a, str>) -> Self { + match value { + Cow::Borrowed(borrowed) => Self::Borrowed(borrowed), + Cow::Owned(owned) => Self::Owned(owned.into()), + } + } } impl From> for ArcCow<'_, [T]> { - fn from(vec: Vec) -> Self { - ArcCow::Owned(Arc::from(vec)) - } + fn from(vec: Vec) -> Self { + ArcCow::Owned(Arc::from(vec)) + } } impl<'a> From<&'a str> for ArcCow<'a, [u8]> { - fn from(s: &'a str) -> Self { - ArcCow::Borrowed(s.as_bytes()) - } + fn from(s: &'a str) -> Self { + ArcCow::Borrowed(s.as_bytes()) + } } impl std::borrow::Borrow for ArcCow<'_, T> { - fn borrow(&self) -> &T { - match self { - ArcCow::Borrowed(borrowed) => borrowed, - ArcCow::Owned(owned) => owned.as_ref(), - } - } + fn borrow(&self) -> &T { + match self { + ArcCow::Borrowed(borrowed) => borrowed, + ArcCow::Owned(owned) => owned.as_ref(), + } + } } impl std::ops::Deref for ArcCow<'_, T> { - type Target = T; + type Target = T; - fn deref(&self) -> &Self::Target { - match self { - ArcCow::Borrowed(s) => s, - ArcCow::Owned(s) => s.as_ref(), - } - } + fn deref(&self) -> &Self::Target { + match self { + ArcCow::Borrowed(s) => s, + ArcCow::Owned(s) => s.as_ref(), + } + } } impl AsRef for ArcCow<'_, T> { - fn as_ref(&self) -> &T { - match self { - ArcCow::Borrowed(borrowed) => borrowed, - ArcCow::Owned(owned) => owned.as_ref(), - } - } + fn as_ref(&self) -> &T { + match self { + ArcCow::Borrowed(borrowed) => borrowed, + ArcCow::Owned(owned) => owned.as_ref(), + } + } } impl Debug for ArcCow<'_, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f), - ArcCow::Owned(owned) => Debug::fmt(&**owned, f), - } - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f), + ArcCow::Owned(owned) => Debug::fmt(&**owned, f), + } + } } diff --git a/crates/gpui_ce_util/src/lib.rs b/crates/gpui_ce_util/src/lib.rs index eac1e2559a..6f735fcaaa 100644 --- a/crates/gpui_ce_util/src/lib.rs +++ b/crates/gpui_ce_util/src/lib.rs @@ -2,40 +2,40 @@ // pub use gpui_util::{FutureExt, Timeout, arc_cow::ArcCow}; use std::{ - env, - ops::AddAssign, - panic::Location, - pin::Pin, - sync::OnceLock, - task::{Context, Poll}, - time::Instant, + env, + ops::AddAssign, + panic::Location, + pin::Pin, + sync::OnceLock, + task::{Context, Poll}, + time::Instant, }; pub mod arc_cow; pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { - let prev = *value; - *value += T::from(1); - prev + let prev = *value; + *value += T::from(1); + prev } pub fn measure(label: &str, f: impl FnOnce() -> R) -> R { - static ZED_MEASUREMENTS: OnceLock = OnceLock::new(); - let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| { - env::var("ZED_MEASUREMENTS") - .map(|measurements| measurements == "1" || measurements == "true") - .unwrap_or(false) - }); + static ZED_MEASUREMENTS: OnceLock = OnceLock::new(); + let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| { + env::var("ZED_MEASUREMENTS") + .map(|measurements| measurements == "1" || measurements == "true") + .unwrap_or(false) + }); - if *zed_measurements { - let start = Instant::now(); - let result = f(); - let elapsed = start.elapsed(); - eprintln!("{}: {:?}", label, elapsed); - result - } else { - f() - } + if *zed_measurements { + let start = Instant::now(); + let result = f(); + let elapsed = start.elapsed(); + eprintln!("{}: {:?}", label, elapsed); + result + } else { + f() + } } #[macro_export] @@ -52,11 +52,11 @@ macro_rules! debug_panic { #[track_caller] pub fn some_or_debug_panic(option: Option) -> Option { - #[cfg(debug_assertions)] - if option.is_none() { - panic!("Unexpected None"); - } - option + #[cfg(debug_assertions)] + if option.is_none() { + panic!("Unexpected None"); + } + option } /// Expands to an immediately-invoked function expression. Good for using the ? operator @@ -65,131 +65,131 @@ pub fn some_or_debug_panic(option: Option) -> Option { /// Accepts a normal block, an async block, or an async move block. #[macro_export] macro_rules! maybe { - ($block:block) => { - (|| $block)() - }; - (async $block:block) => { - (async || $block)() - }; - (async move $block:block) => { - (async move || $block)() - }; + ($block:block) => { + (|| $block)() + }; + (async $block:block) => { + (async || $block)() + }; + (async move $block:block) => { + (async move || $block)() + }; } pub trait ResultExt { - type Ok; + type Ok; - fn log_err(self) -> Option; - /// Like [`ResultExt::log_err`], but uses `{:?}` formatting so `anyhow::Error` values emit their - /// full backtrace. Reach for this only when a backtrace is genuinely wanted — most call sites - /// should stick with `log_err` / `warn_on_err`, whose output is a single chained error message. - fn log_err_with_backtrace(self) -> Option - where - E: std::fmt::Debug; - /// Assert that this result should never be an error in development or tests. - fn debug_assert_ok(self, reason: &str) -> Self; - fn warn_on_err(self) -> Option; - fn log_with_level(self, level: log::Level) -> Option; - fn anyhow(self) -> anyhow::Result - where - E: Into; + fn log_err(self) -> Option; + /// Like [`ResultExt::log_err`], but uses `{:?}` formatting so `anyhow::Error` values emit their + /// full backtrace. Reach for this only when a backtrace is genuinely wanted — most call sites + /// should stick with `log_err` / `warn_on_err`, whose output is a single chained error message. + fn log_err_with_backtrace(self) -> Option + where + E: std::fmt::Debug; + /// Assert that this result should never be an error in development or tests. + fn debug_assert_ok(self, reason: &str) -> Self; + fn warn_on_err(self) -> Option; + fn log_with_level(self, level: log::Level) -> Option; + fn anyhow(self) -> anyhow::Result + where + E: Into; } impl ResultExt for Result where - E: std::fmt::Display, + E: std::fmt::Display, { - type Ok = T; + type Ok = T; - #[track_caller] - fn log_err(self) -> Option { - self.log_with_level(log::Level::Error) - } + #[track_caller] + fn log_err(self) -> Option { + self.log_with_level(log::Level::Error) + } - #[track_caller] - fn log_err_with_backtrace(self) -> Option - where - E: std::fmt::Debug, - { - match self { - Ok(value) => Some(value), - Err(error) => { - log_error_with_caller( - *Location::caller(), - DebugAsDisplay(&error), - log::Level::Error, - ); - None - } - } - } + #[track_caller] + fn log_err_with_backtrace(self) -> Option + where + E: std::fmt::Debug, + { + match self { + Ok(value) => Some(value), + Err(error) => { + log_error_with_caller( + *Location::caller(), + DebugAsDisplay(&error), + log::Level::Error, + ); + None + } + } + } - #[track_caller] - fn debug_assert_ok(self, reason: &str) -> Self { - if let Err(error) = &self { - debug_panic!("{reason} - {error:#}"); - } - self - } + #[track_caller] + fn debug_assert_ok(self, reason: &str) -> Self { + if let Err(error) = &self { + debug_panic!("{reason} - {error:#}"); + } + self + } - #[track_caller] - fn warn_on_err(self) -> Option { - self.log_with_level(log::Level::Warn) - } + #[track_caller] + fn warn_on_err(self) -> Option { + self.log_with_level(log::Level::Warn) + } - #[track_caller] - fn log_with_level(self, level: log::Level) -> Option { - match self { - Ok(value) => Some(value), - Err(error) => { - log_error_with_caller(*Location::caller(), error, level); - None - } - } - } + #[track_caller] + fn log_with_level(self, level: log::Level) -> Option { + match self { + Ok(value) => Some(value), + Err(error) => { + log_error_with_caller(*Location::caller(), error, level); + None + } + } + } - fn anyhow(self) -> anyhow::Result - where - E: Into, - { - self.map_err(Into::into) - } + fn anyhow(self) -> anyhow::Result + where + E: Into, + { + self.map_err(Into::into) + } } fn log_error_with_caller(caller: core::panic::Location<'_>, error: E, level: log::Level) where - E: std::fmt::Display, + E: std::fmt::Display, { - #[cfg(not(windows))] - let file = caller.file(); - #[cfg(windows)] - let file = caller.file().replace('\\', "/"); - // In this codebase all crates reside in a `crates` directory, - // so discard the prefix up to that segment to find the crate name - let file = file.split_once("crates/"); - let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/")); + #[cfg(not(windows))] + let file = caller.file(); + #[cfg(windows)] + let file = caller.file().replace('\\', "/"); + // In this codebase all crates reside in a `crates` directory, + // so discard the prefix up to that segment to find the crate name + let file = file.split_once("crates/"); + let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/")); - let module_path = target.map(|(krate, module)| { - if module.starts_with(krate) { - module.trim_end_matches(".rs").replace('/', "::") - } else { - krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::") - } - }); - let file = file.map(|(_, file)| format!("crates/{file}")); - log::logger().log( - &log::Record::builder() - .target(module_path.as_deref().unwrap_or("")) - .module_path(file.as_deref()) - .args(format_args!("{:#}", error)) - .file(Some(caller.file())) - .line(Some(caller.line())) - .level(level) - .build(), - ); + let module_path = target.map(|(krate, module)| { + if module.starts_with(krate) { + module.trim_end_matches(".rs").replace('/', "::") + } else { + krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::") + } + }); + let file = file.map(|(_, file)| format!("crates/{file}")); + log::logger().log( + &log::Record::builder() + .target(module_path.as_deref().unwrap_or("")) + .module_path(file.as_deref()) + .args(format_args!("{:#}", error)) + .file(Some(caller.file())) + .line(Some(caller.line())) + .level(level) + .build(), + ); } pub fn log_err(error: &E) { - log_error_with_caller(*Location::caller(), error, log::Level::Error); + log_error_with_caller(*Location::caller(), error, log::Level::Error); } // Forces `{:?}` formatting through a `Display`-bounded logging helper so `anyhow::Error` emits a @@ -197,104 +197,104 @@ pub fn log_err(error: &E) { struct DebugAsDisplay<'a, E>(&'a E); impl std::fmt::Display for DebugAsDisplay<'_, E> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } } pub trait TryFutureExt { - fn log_err(self) -> LogErrorFuture - where - Self: Sized; + fn log_err(self) -> LogErrorFuture + where + Self: Sized; - fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture - where - Self: Sized; + fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture + where + Self: Sized; - fn warn_on_err(self) -> LogErrorFuture - where - Self: Sized; - fn unwrap(self) -> UnwrapFuture - where - Self: Sized; + fn warn_on_err(self) -> LogErrorFuture + where + Self: Sized; + fn unwrap(self) -> UnwrapFuture + where + Self: Sized; } /// `{:?}`-formatting companion to [`TryFutureExt`]; emits a backtrace for `anyhow::Error`. Prefer /// [`TryFutureExt`] unless a backtrace is genuinely wanted. pub trait TryFutureExtBacktrace { - fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture - where - Self: Sized; + fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture + where + Self: Sized; - fn log_tracked_err_with_backtrace( - self, - location: core::panic::Location<'static>, - ) -> LogErrorWithBacktraceFuture - where - Self: Sized; + fn log_tracked_err_with_backtrace( + self, + location: core::panic::Location<'static>, + ) -> LogErrorWithBacktraceFuture + where + Self: Sized; } impl TryFutureExt for F where - F: Future>, - E: std::fmt::Display, + F: Future>, + E: std::fmt::Display, { - #[track_caller] - fn log_err(self) -> LogErrorFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorFuture(self, log::Level::Error, *location) - } + #[track_caller] + fn log_err(self) -> LogErrorFuture + where + Self: Sized, + { + let location = Location::caller(); + LogErrorFuture(self, log::Level::Error, *location) + } - fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture - where - Self: Sized, - { - LogErrorFuture(self, log::Level::Error, location) - } + fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture + where + Self: Sized, + { + LogErrorFuture(self, log::Level::Error, location) + } - #[track_caller] - fn warn_on_err(self) -> LogErrorFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorFuture(self, log::Level::Warn, *location) - } + #[track_caller] + fn warn_on_err(self) -> LogErrorFuture + where + Self: Sized, + { + let location = Location::caller(); + LogErrorFuture(self, log::Level::Warn, *location) + } - fn unwrap(self) -> UnwrapFuture - where - Self: Sized, - { - UnwrapFuture(self) - } + fn unwrap(self) -> UnwrapFuture + where + Self: Sized, + { + UnwrapFuture(self) + } } impl TryFutureExtBacktrace for F where - F: Future>, - E: std::fmt::Debug, + F: Future>, + E: std::fmt::Debug, { - #[track_caller] - fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorWithBacktraceFuture(self, log::Level::Error, *location) - } + #[track_caller] + fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture + where + Self: Sized, + { + let location = Location::caller(); + LogErrorWithBacktraceFuture(self, log::Level::Error, *location) + } - fn log_tracked_err_with_backtrace( - self, - location: core::panic::Location<'static>, - ) -> LogErrorWithBacktraceFuture - where - Self: Sized, - { - LogErrorWithBacktraceFuture(self, log::Level::Error, location) - } + fn log_tracked_err_with_backtrace( + self, + location: core::panic::Location<'static>, + ) -> LogErrorWithBacktraceFuture + where + Self: Sized, + { + LogErrorWithBacktraceFuture(self, log::Level::Error, location) + } } #[must_use] @@ -302,26 +302,26 @@ pub struct LogErrorFuture(F, log::Level, core::panic::Location<'static>); impl Future for LogErrorFuture where - F: Future>, - E: std::fmt::Display, + F: Future>, + E: std::fmt::Display, { - type Output = Option; + type Output = Option; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let level = self.1; - let location = self.2; - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(output) => Poll::Ready(match output { - Ok(output) => Some(output), - Err(error) => { - log_error_with_caller(location, error, level); - None - } - }), - Poll::Pending => Poll::Pending, - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let level = self.1; + let location = self.2; + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(output) => Poll::Ready(match output { + Ok(output) => Some(output), + Err(error) => { + log_error_with_caller(location, error, level); + None + } + }), + Poll::Pending => Poll::Pending, + } + } } #[must_use] @@ -329,65 +329,65 @@ pub struct LogErrorWithBacktraceFuture(F, log::Level, core::panic::Location<' impl Future for LogErrorWithBacktraceFuture where - F: Future>, - E: std::fmt::Debug, + F: Future>, + E: std::fmt::Debug, { - type Output = Option; + type Output = Option; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let level = self.1; - let location = self.2; - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(output) => Poll::Ready(match output { - Ok(output) => Some(output), - Err(error) => { - log_error_with_caller(location, DebugAsDisplay(&error), level); - None - } - }), - Poll::Pending => Poll::Pending, - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let level = self.1; + let location = self.2; + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(output) => Poll::Ready(match output { + Ok(output) => Some(output), + Err(error) => { + log_error_with_caller(location, DebugAsDisplay(&error), level); + None + } + }), + Poll::Pending => Poll::Pending, + } + } } pub struct UnwrapFuture(F); impl Future for UnwrapFuture where - F: Future>, - E: std::fmt::Debug, + F: Future>, + E: std::fmt::Debug, { - type Output = T; + type Output = T; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(result) => Poll::Ready(result.unwrap()), - Poll::Pending => Poll::Pending, - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(result) => Poll::Ready(result.unwrap()), + Poll::Pending => Poll::Pending, + } + } } pub struct Deferred(Option); impl Deferred { - /// Drop without running the deferred function. - pub fn abort(mut self) { - self.0.take(); - } + /// Drop without running the deferred function. + pub fn abort(mut self) { + self.0.take(); + } } impl Drop for Deferred { - fn drop(&mut self) { - if let Some(f) = self.0.take() { - f() - } - } + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f() + } + } } /// Run the given function when the returned value is dropped (unless it's cancelled). #[must_use] pub fn defer(f: F) -> Deferred { - Deferred(Some(f)) + Deferred(Some(f)) } diff --git a/crates/gpui_collections/src/vecmap.rs b/crates/gpui_collections/src/vecmap.rs index bec6596b92..3def782709 100644 --- a/crates/gpui_collections/src/vecmap.rs +++ b/crates/gpui_collections/src/vecmap.rs @@ -16,177 +16,177 @@ /// value types. #[derive(Default)] pub struct VecMap { - keys: Vec, - values: Vec, + keys: Vec, + values: Vec, } impl VecMap { - pub fn new() -> Self { - Self { - keys: Vec::new(), - values: Vec::new(), - } - } + pub fn new() -> Self { + Self { + keys: Vec::new(), + values: Vec::new(), + } + } - pub fn iter(&self) -> Iter<'_, K, V> { - Iter { - iter: self.keys.iter().zip(self.values.iter()), - } - } + pub fn iter(&self) -> Iter<'_, K, V> { + Iter { + iter: self.keys.iter().zip(self.values.iter()), + } + } } impl VecMap { - pub fn entry(&mut self, key: K) -> Entry<'_, K, V> { - match self.keys.iter().position(|k| k == &key) { - Some(index) => Entry::Occupied(OccupiedEntry { - key: &self.keys[index], - value: &mut self.values[index], - }), - None => Entry::Vacant(VacantEntry { map: self, key }), - } - } + pub fn entry(&mut self, key: K) -> Entry<'_, K, V> { + match self.keys.iter().position(|k| k == &key) { + Some(index) => Entry::Occupied(OccupiedEntry { + key: &self.keys[index], + value: &mut self.values[index], + }), + None => Entry::Vacant(VacantEntry { map: self, key }), + } + } - /// Like [`Self::entry`] but takes its key by reference instead of by value. - /// - /// This can be helpful if you have a key where cloning is expensive, as we - /// can avoid cloning the key until a value is inserted under that entry. - pub fn entry_ref<'a, 'k>(&'a mut self, key: &'k K) -> EntryRef<'k, 'a, K, V> { - match self.keys.iter().position(|k| k == key) { - Some(index) => EntryRef::Occupied(OccupiedEntry { - key: &self.keys[index], - value: &mut self.values[index], - }), - None => EntryRef::Vacant(VacantEntryRef { map: self, key }), - } - } + /// Like [`Self::entry`] but takes its key by reference instead of by value. + /// + /// This can be helpful if you have a key where cloning is expensive, as we + /// can avoid cloning the key until a value is inserted under that entry. + pub fn entry_ref<'a, 'k>(&'a mut self, key: &'k K) -> EntryRef<'k, 'a, K, V> { + match self.keys.iter().position(|k| k == key) { + Some(index) => EntryRef::Occupied(OccupiedEntry { + key: &self.keys[index], + value: &mut self.values[index], + }), + None => EntryRef::Vacant(VacantEntryRef { map: self, key }), + } + } } pub struct Iter<'a, K, V> { - iter: std::iter::Zip, std::slice::Iter<'a, V>>, + iter: std::iter::Zip, std::slice::Iter<'a, V>>, } impl<'a, K, V> Iterator for Iter<'a, K, V> { - type Item = (&'a K, &'a V); + type Item = (&'a K, &'a V); - fn next(&mut self) -> Option { - self.iter.next() - } + fn next(&mut self) -> Option { + self.iter.next() + } } pub enum Entry<'a, K, V> { - Occupied(OccupiedEntry<'a, K, V>), - Vacant(VacantEntry<'a, K, V>), + Occupied(OccupiedEntry<'a, K, V>), + Vacant(VacantEntry<'a, K, V>), } impl<'a, K, V> Entry<'a, K, V> { - pub fn key(&self) -> &K { - match self { - Entry::Occupied(entry) => entry.key, - Entry::Vacant(entry) => &entry.key, - } - } + pub fn key(&self) -> &K { + match self { + Entry::Occupied(entry) => entry.key, + Entry::Vacant(entry) => &entry.key, + } + } - pub fn or_insert_with_key(self, default: F) -> &'a mut V - where - F: FnOnce(&K) -> V, - { - match self { - Entry::Occupied(entry) => entry.value, - Entry::Vacant(entry) => { - entry.map.values.push(default(&entry.key)); - entry.map.keys.push(entry.key); - match entry.map.values.last_mut() { - Some(value) => value, - None => unreachable!("vec empty after pushing to it"), - } - } - } - } + pub fn or_insert_with_key(self, default: F) -> &'a mut V + where + F: FnOnce(&K) -> V, + { + match self { + Entry::Occupied(entry) => entry.value, + Entry::Vacant(entry) => { + entry.map.values.push(default(&entry.key)); + entry.map.keys.push(entry.key); + match entry.map.values.last_mut() { + Some(value) => value, + None => unreachable!("vec empty after pushing to it"), + } + } + } + } - pub fn or_insert_with(self, default: F) -> &'a mut V - where - F: FnOnce() -> V, - { - self.or_insert_with_key(|_| default()) - } + pub fn or_insert_with(self, default: F) -> &'a mut V + where + F: FnOnce() -> V, + { + self.or_insert_with_key(|_| default()) + } - pub fn or_insert(self, value: V) -> &'a mut V { - self.or_insert_with_key(|_| value) - } + pub fn or_insert(self, value: V) -> &'a mut V { + self.or_insert_with_key(|_| value) + } - pub fn or_insert_default(self) -> &'a mut V - where - V: Default, - { - self.or_insert_with_key(|_| Default::default()) - } + pub fn or_insert_default(self) -> &'a mut V + where + V: Default, + { + self.or_insert_with_key(|_| Default::default()) + } } pub struct OccupiedEntry<'a, K, V> { - key: &'a K, - value: &'a mut V, + key: &'a K, + value: &'a mut V, } pub struct VacantEntry<'a, K, V> { - map: &'a mut VecMap, - key: K, + map: &'a mut VecMap, + key: K, } pub enum EntryRef<'key, 'map, K, V> { - Occupied(OccupiedEntry<'map, K, V>), - Vacant(VacantEntryRef<'key, 'map, K, V>), + Occupied(OccupiedEntry<'map, K, V>), + Vacant(VacantEntryRef<'key, 'map, K, V>), } impl<'key, 'map, K, V> EntryRef<'key, 'map, K, V> { - pub fn key(&self) -> &K { - match self { - EntryRef::Occupied(entry) => entry.key, - EntryRef::Vacant(entry) => entry.key, - } - } + pub fn key(&self) -> &K { + match self { + EntryRef::Occupied(entry) => entry.key, + EntryRef::Vacant(entry) => entry.key, + } + } } impl<'key, 'map, K, V> EntryRef<'key, 'map, K, V> where - K: Clone, + K: Clone, { - pub fn or_insert_with_key(self, default: F) -> &'map mut V - where - F: FnOnce(&K) -> V, - { - match self { - EntryRef::Occupied(entry) => entry.value, - EntryRef::Vacant(entry) => { - entry.map.values.push(default(entry.key)); - entry.map.keys.push(entry.key.clone()); - match entry.map.values.last_mut() { - Some(value) => value, - None => unreachable!("vec empty after pushing to it"), - } - } - } - } + pub fn or_insert_with_key(self, default: F) -> &'map mut V + where + F: FnOnce(&K) -> V, + { + match self { + EntryRef::Occupied(entry) => entry.value, + EntryRef::Vacant(entry) => { + entry.map.values.push(default(entry.key)); + entry.map.keys.push(entry.key.clone()); + match entry.map.values.last_mut() { + Some(value) => value, + None => unreachable!("vec empty after pushing to it"), + } + } + } + } - pub fn or_insert_with(self, default: F) -> &'map mut V - where - F: FnOnce() -> V, - { - self.or_insert_with_key(|_| default()) - } + pub fn or_insert_with(self, default: F) -> &'map mut V + where + F: FnOnce() -> V, + { + self.or_insert_with_key(|_| default()) + } - pub fn or_insert(self, value: V) -> &'map mut V { - self.or_insert_with_key(|_| value) - } + pub fn or_insert(self, value: V) -> &'map mut V { + self.or_insert_with_key(|_| value) + } - pub fn or_insert_default(self) -> &'map mut V - where - V: Default, - { - self.or_insert_with_key(|_| Default::default()) - } + pub fn or_insert_default(self) -> &'map mut V + where + V: Default, + { + self.or_insert_with_key(|_| Default::default()) + } } pub struct VacantEntryRef<'key, 'map, K, V> { - map: &'map mut VecMap, - key: &'key K, + map: &'map mut VecMap, + key: &'key K, } diff --git a/crates/gpui_collections/src/vecmap_tests.rs b/crates/gpui_collections/src/vecmap_tests.rs index 1f698f8331..3d9efb68ab 100644 --- a/crates/gpui_collections/src/vecmap_tests.rs +++ b/crates/gpui_collections/src/vecmap_tests.rs @@ -7,92 +7,92 @@ use crate::vecmap::*; #[test] fn test_entry_vacant_or_insert() { - let mut map: VecMap<&str, i32> = VecMap::new(); - let value = map.entry("a").or_insert(1); - assert_eq!(*value, 1); - assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + let value = map.entry("a").or_insert(1); + assert_eq!(*value, 1); + assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); } #[test] fn test_entry_occupied_or_insert_keeps_existing() { - let mut map: VecMap<&str, i32> = VecMap::new(); - map.entry("a").or_insert(1); - let value = map.entry("a").or_insert(99); - assert_eq!(*value, 1); - assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + map.entry("a").or_insert(1); + let value = map.entry("a").or_insert(99); + assert_eq!(*value, 1); + assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); } #[test] fn test_entry_or_insert_with() { - let mut map: VecMap<&str, i32> = VecMap::new(); - map.entry("a").or_insert_with(|| 42); - assert_eq!(map.iter().collect::>(), vec![(&"a", &42)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + map.entry("a").or_insert_with(|| 42); + assert_eq!(map.iter().collect::>(), vec![(&"a", &42)]); } #[test] fn test_entry_or_insert_with_not_called_when_occupied() { - let mut map: VecMap<&str, i32> = VecMap::new(); - map.entry("a").or_insert(1); - map.entry("a") - .or_insert_with(|| panic!("should not be called")); - assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + map.entry("a").or_insert(1); + map.entry("a") + .or_insert_with(|| panic!("should not be called")); + assert_eq!(map.iter().collect::>(), vec![(&"a", &1)]); } #[test] fn test_entry_or_insert_with_key() { - let mut map: VecMap<&str, String> = VecMap::new(); - map.entry("hello").or_insert_with_key(|k| k.to_uppercase()); - assert_eq!( - map.iter().collect::>(), - vec![(&"hello", &"HELLO".to_string())] - ); + let mut map: VecMap<&str, String> = VecMap::new(); + map.entry("hello").or_insert_with_key(|k| k.to_uppercase()); + assert_eq!( + map.iter().collect::>(), + vec![(&"hello", &"HELLO".to_string())] + ); } #[test] fn test_entry_or_insert_default() { - let mut map: VecMap<&str, i32> = VecMap::new(); - map.entry("a").or_insert_default(); - assert_eq!(map.iter().collect::>(), vec![(&"a", &0)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + map.entry("a").or_insert_default(); + assert_eq!(map.iter().collect::>(), vec![(&"a", &0)]); } #[test] fn test_entry_key() { - let mut map: VecMap<&str, i32> = VecMap::new(); - assert_eq!(*map.entry("a").key(), "a"); - map.entry("a").or_insert(1); - assert_eq!(*map.entry("a").key(), "a"); + let mut map: VecMap<&str, i32> = VecMap::new(); + assert_eq!(*map.entry("a").key(), "a"); + map.entry("a").or_insert(1); + assert_eq!(*map.entry("a").key(), "a"); } #[test] fn test_entry_mut_ref_can_be_updated() { - let mut map: VecMap<&str, i32> = VecMap::new(); - let value = map.entry("a").or_insert(0); - *value = 5; - assert_eq!(map.iter().collect::>(), vec![(&"a", &5)]); + let mut map: VecMap<&str, i32> = VecMap::new(); + let value = map.entry("a").or_insert(0); + *value = 5; + assert_eq!(map.iter().collect::>(), vec![(&"a", &5)]); } #[test] fn test_insertion_order_preserved() { - let mut map: VecMap<&str, i32> = VecMap::new(); - map.entry("b").or_insert(2); - map.entry("a").or_insert(1); - map.entry("c").or_insert(3); - assert_eq!( - map.iter().collect::>(), - vec![(&"b", &2), (&"a", &1), (&"c", &3)] - ); + let mut map: VecMap<&str, i32> = VecMap::new(); + map.entry("b").or_insert(2); + map.entry("a").or_insert(1); + map.entry("c").or_insert(3); + assert_eq!( + map.iter().collect::>(), + vec![(&"b", &2), (&"a", &1), (&"c", &3)] + ); } #[test] fn test_multiple_entries_independent() { - let mut map: VecMap = VecMap::new(); - map.entry(1).or_insert(10); - map.entry(2).or_insert(20); - map.entry(3).or_insert(30); - assert_eq!(map.iter().count(), 3); - // Re-inserting does not duplicate keys - map.entry(1).or_insert(99); - assert_eq!(map.iter().count(), 3); + let mut map: VecMap = VecMap::new(); + map.entry(1).or_insert(10); + map.entry(2).or_insert(20); + map.entry(3).or_insert(30); + assert_eq!(map.iter().count(), 3); + // Re-inserting does not duplicate keys + map.entry(1).or_insert(99); + assert_eq!(map.iter().count(), 3); } // entry_ref tests @@ -102,110 +102,110 @@ use std::rc::Rc; #[derive(PartialEq, Eq)] struct CountedKey { - value: String, - clone_count: Rc>, + value: String, + clone_count: Rc>, } impl Clone for CountedKey { - fn clone(&self) -> Self { - self.clone_count.set(self.clone_count.get() + 1); - CountedKey { - value: self.value.clone(), - clone_count: self.clone_count.clone(), - } - } + fn clone(&self) -> Self { + self.clone_count.set(self.clone_count.get() + 1); + CountedKey { + value: self.value.clone(), + clone_count: self.clone_count.clone(), + } + } } #[test] fn test_entry_ref_vacant_or_insert() { - let mut map: VecMap = VecMap::new(); - let key = "a".to_string(); - let value = map.entry_ref(&key).or_insert(1); - assert_eq!(*value, 1); - assert_eq!(map.iter().count(), 1); + let mut map: VecMap = VecMap::new(); + let key = "a".to_string(); + let value = map.entry_ref(&key).or_insert(1); + assert_eq!(*value, 1); + assert_eq!(map.iter().count(), 1); } #[test] fn test_entry_ref_occupied_or_insert_keeps_existing() { - let mut map: VecMap = VecMap::new(); - map.entry_ref(&"a".to_string()).or_insert(1); - let value = map.entry_ref(&"a".to_string()).or_insert(99); - assert_eq!(*value, 1); - assert_eq!(map.iter().count(), 1); + let mut map: VecMap = VecMap::new(); + map.entry_ref(&"a".to_string()).or_insert(1); + let value = map.entry_ref(&"a".to_string()).or_insert(99); + assert_eq!(*value, 1); + assert_eq!(map.iter().count(), 1); } #[test] fn test_entry_ref_key_not_cloned_when_occupied() { - let clone_count = Rc::new(Cell::new(0)); - let key = CountedKey { - value: "a".to_string(), - clone_count: clone_count.clone(), - }; + let clone_count = Rc::new(Cell::new(0)); + let key = CountedKey { + value: "a".to_string(), + clone_count: clone_count.clone(), + }; - let mut map: VecMap = VecMap::new(); - map.entry_ref(&key).or_insert(1); - let clones_after_insert = clone_count.get(); + let mut map: VecMap = VecMap::new(); + map.entry_ref(&key).or_insert(1); + let clones_after_insert = clone_count.get(); - // Looking up an existing key must not clone it. - map.entry_ref(&key).or_insert(99); - assert_eq!(clone_count.get(), clones_after_insert); + // Looking up an existing key must not clone it. + map.entry_ref(&key).or_insert(99); + assert_eq!(clone_count.get(), clones_after_insert); } #[test] fn test_entry_ref_key_cloned_exactly_once_on_vacant_insert() { - let clone_count = Rc::new(Cell::new(0)); - let key = CountedKey { - value: "a".to_string(), - clone_count: clone_count.clone(), - }; + let clone_count = Rc::new(Cell::new(0)); + let key = CountedKey { + value: "a".to_string(), + clone_count: clone_count.clone(), + }; - let mut map: VecMap = VecMap::new(); - map.entry_ref(&key).or_insert(1); - assert_eq!(clone_count.get(), 1); + let mut map: VecMap = VecMap::new(); + map.entry_ref(&key).or_insert(1); + assert_eq!(clone_count.get(), 1); } #[test] fn test_entry_ref_or_insert_with_key() { - let mut map: VecMap = VecMap::new(); - let key = "hello".to_string(); - map.entry_ref(&key).or_insert_with_key(|k| k.to_uppercase()); - assert_eq!( - map.iter().collect::>(), - vec![(&"hello".to_string(), &"HELLO".to_string())] - ); + let mut map: VecMap = VecMap::new(); + let key = "hello".to_string(); + map.entry_ref(&key).or_insert_with_key(|k| k.to_uppercase()); + assert_eq!( + map.iter().collect::>(), + vec![(&"hello".to_string(), &"HELLO".to_string())] + ); } #[test] fn test_entry_ref_or_insert_with_not_called_when_occupied() { - let mut map: VecMap = VecMap::new(); - let key = "a".to_string(); - map.entry_ref(&key).or_insert(1); - map.entry_ref(&key) - .or_insert_with(|| panic!("should not be called")); - assert_eq!(map.iter().collect::>(), vec![(&key, &1)]); + let mut map: VecMap = VecMap::new(); + let key = "a".to_string(); + map.entry_ref(&key).or_insert(1); + map.entry_ref(&key) + .or_insert_with(|| panic!("should not be called")); + assert_eq!(map.iter().collect::>(), vec![(&key, &1)]); } #[test] fn test_entry_ref_or_insert_default() { - let mut map: VecMap = VecMap::new(); - map.entry_ref(&"a".to_string()).or_insert_default(); - assert_eq!(map.iter().collect::>(), vec![(&"a".to_string(), &0)]); + let mut map: VecMap = VecMap::new(); + map.entry_ref(&"a".to_string()).or_insert_default(); + assert_eq!(map.iter().collect::>(), vec![(&"a".to_string(), &0)]); } #[test] fn test_entry_ref_key() { - let mut map: VecMap = VecMap::new(); - let key = "a".to_string(); - assert_eq!(*map.entry_ref(&key).key(), key); - map.entry_ref(&key).or_insert(1); - assert_eq!(*map.entry_ref(&key).key(), key); + let mut map: VecMap = VecMap::new(); + let key = "a".to_string(); + assert_eq!(*map.entry_ref(&key).key(), key); + map.entry_ref(&key).or_insert(1); + assert_eq!(*map.entry_ref(&key).key(), key); } #[test] fn test_entry_ref_mut_ref_can_be_updated() { - let mut map: VecMap = VecMap::new(); - let key = "a".to_string(); - let value = map.entry_ref(&key).or_insert(0); - *value = 5; - assert_eq!(map.iter().collect::>(), vec![(&key, &5)]); + let mut map: VecMap = VecMap::new(); + let key = "a".to_string(); + let value = map.entry_ref(&key).or_insert(0); + *value = 5; + assert_eq!(map.iter().collect::>(), vec![(&key, &5)]); } diff --git a/crates/gpui_derive_refineable/src/derive_refineable.rs b/crates/gpui_derive_refineable/src/derive_refineable.rs index c7c8a91ad9..2d4bd8ddaa 100644 --- a/crates/gpui_derive_refineable/src/derive_refineable.rs +++ b/crates/gpui_derive_refineable/src/derive_refineable.rs @@ -2,547 +2,547 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ - DeriveInput, Field, FieldsNamed, PredicateType, TraitBound, Type, TypeParamBound, WhereClause, - WherePredicate, parse_macro_input, parse_quote, + DeriveInput, Field, FieldsNamed, PredicateType, TraitBound, Type, TypeParamBound, WhereClause, + WherePredicate, parse_macro_input, parse_quote, }; #[proc_macro_derive(Refineable, attributes(refineable))] pub fn derive_refineable(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - attrs, - .. - } = parse_macro_input!(input); + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); - let refineable_attr = attrs.iter().find(|attr| attr.path().is_ident("refineable")); + let refineable_attr = attrs.iter().find(|attr| attr.path().is_ident("refineable")); - let mut impl_debug_on_refinement = false; - let mut derives_serialize = false; - let mut refinement_traits_to_derive = vec![]; + let mut impl_debug_on_refinement = false; + let mut derives_serialize = false; + let mut refinement_traits_to_derive = vec![]; - if let Some(refineable_attr) = refineable_attr { - let _ = refineable_attr.parse_nested_meta(|meta| { - if meta.path.is_ident("Debug") { - impl_debug_on_refinement = true; - } else { - if meta.path.is_ident("Serialize") { - derives_serialize = true; - } - refinement_traits_to_derive.push(meta.path); - } - Ok(()) - }); - } + if let Some(refineable_attr) = refineable_attr { + let _ = refineable_attr.parse_nested_meta(|meta| { + if meta.path.is_ident("Debug") { + impl_debug_on_refinement = true; + } else { + if meta.path.is_ident("Serialize") { + derives_serialize = true; + } + refinement_traits_to_derive.push(meta.path); + } + Ok(()) + }); + } - let refinement_ident = format_ident!("{}Refinement", ident); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let refinement_ident = format_ident!("{}Refinement", ident); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let fields = match data { - syn::Data::Struct(syn::DataStruct { - fields: syn::Fields::Named(FieldsNamed { named, .. }), - .. - }) => named.into_iter().collect::>(), - _ => panic!("This derive macro only supports structs with named fields"), - }; + let fields = match data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(FieldsNamed { named, .. }), + .. + }) => named.into_iter().collect::>(), + _ => panic!("This derive macro only supports structs with named fields"), + }; - let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); - let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect(); - let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect(); + let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); + let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect(); + let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect(); - let field_attributes: Vec = fields - .iter() - .map(|f| { - if derives_serialize { - if is_refineable_field(f) { - quote! { #[serde(default, skip_serializing_if = "::refineable::IsEmpty::is_empty")] } - } else { - quote! { #[serde(skip_serializing_if = "::std::option::Option::is_none")] } - } - } else { - quote! {} - } - }) - .collect(); + let field_attributes: Vec = fields + .iter() + .map(|f| { + if derives_serialize { + if is_refineable_field(f) { + quote! { #[serde(default, skip_serializing_if = "::refineable::IsEmpty::is_empty")] } + } else { + quote! { #[serde(skip_serializing_if = "::std::option::Option::is_none")] } + } + } else { + quote! {} + } + }) + .collect(); - // Create trait bound that each wrapped type must implement Clone - let type_param_bounds: Vec<_> = wrapped_types - .iter() - .map(|ty| { - WherePredicate::Type(PredicateType { - lifetimes: None, - bounded_ty: ty.clone(), - colon_token: Default::default(), - bounds: { - let mut punctuated = syn::punctuated::Punctuated::new(); - punctuated.push_value(TypeParamBound::Trait(TraitBound { - paren_token: None, - modifier: syn::TraitBoundModifier::None, - lifetimes: None, - path: parse_quote!(Clone), - })); + // Create trait bound that each wrapped type must implement Clone + let type_param_bounds: Vec<_> = wrapped_types + .iter() + .map(|ty| { + WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: ty.clone(), + colon_token: Default::default(), + bounds: { + let mut punctuated = syn::punctuated::Punctuated::new(); + punctuated.push_value(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: syn::TraitBoundModifier::None, + lifetimes: None, + path: parse_quote!(Clone), + })); - punctuated - }, - }) - }) - .collect(); + punctuated + }, + }) + }) + .collect(); - // Append to where_clause or create a new one if it doesn't exist - let where_clause = match where_clause.cloned() { - Some(mut where_clause) => { - where_clause.predicates.extend(type_param_bounds); - where_clause.clone() - } - None => WhereClause { - where_token: Default::default(), - predicates: type_param_bounds.into_iter().collect(), - }, - }; + // Append to where_clause or create a new one if it doesn't exist + let where_clause = match where_clause.cloned() { + Some(mut where_clause) => { + where_clause.predicates.extend(type_param_bounds); + where_clause.clone() + } + None => WhereClause { + where_token: Default::default(), + predicates: type_param_bounds.into_iter().collect(), + }, + }; - let refineable_refine_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); + let refineable_refine_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); - if is_refineable { - quote! { - self.#name.refine(&refinement.#name); - } - } else if is_optional { - quote! { - if let Some(value) = &refinement.#name { - self.#name = Some(value.clone()); - } - } - } else { - quote! { - if let Some(value) = &refinement.#name { - self.#name = value.clone(); - } - } - } - }) - .collect(); + if is_refineable { + quote! { + self.#name.refine(&refinement.#name); + } + } else if is_optional { + quote! { + if let Some(value) = &refinement.#name { + self.#name = Some(value.clone()); + } + } + } else { + quote! { + if let Some(value) = &refinement.#name { + self.#name = value.clone(); + } + } + } + }) + .collect(); - let refineable_refined_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); + let refineable_refined_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); - if is_refineable { - quote! { - self.#name = self.#name.refined(refinement.#name); - } - } else if is_optional { - quote! { - if let Some(value) = refinement.#name { - self.#name = Some(value); - } - } - } else { - quote! { - if let Some(value) = refinement.#name { - self.#name = value; - } - } - } - }) - .collect(); + if is_refineable { + quote! { + self.#name = self.#name.refined(refinement.#name); + } + } else if is_optional { + quote! { + if let Some(value) = refinement.#name { + self.#name = Some(value); + } + } + } else { + quote! { + if let Some(value) = refinement.#name { + self.#name = value; + } + } + } + }) + .collect(); - let refinement_refine_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); + let refinement_refine_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); - if is_refineable { - quote! { - self.#name.refine(&refinement.#name); - } - } else { - quote! { - if let Some(value) = &refinement.#name { - self.#name = Some(value.clone()); - } - } - } - }) - .collect(); + if is_refineable { + quote! { + self.#name.refine(&refinement.#name); + } + } else { + quote! { + if let Some(value) = &refinement.#name { + self.#name = Some(value.clone()); + } + } + } + }) + .collect(); - let refinement_refined_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); + let refinement_refined_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); - if is_refineable { - quote! { - self.#name = self.#name.refined(refinement.#name); - } - } else { - quote! { - if let Some(value) = refinement.#name { - self.#name = Some(value); - } - } - } - }) - .collect(); + if is_refineable { + quote! { + self.#name = self.#name.refined(refinement.#name); + } + } else { + quote! { + if let Some(value) = refinement.#name { + self.#name = Some(value); + } + } + } + }) + .collect(); - let from_refinement_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); + let from_refinement_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); - if is_refineable { - quote! { - #name: value.#name.into(), - } - } else if is_optional { - quote! { - #name: value.#name.map(|v| v.into()), - } - } else { - quote! { - #name: value.#name.map(|v| v.into()).unwrap_or_default(), - } - } - }) - .collect(); + if is_refineable { + quote! { + #name: value.#name.into(), + } + } else if is_optional { + quote! { + #name: value.#name.map(|v| v.into()), + } + } else { + quote! { + #name: value.#name.map(|v| v.into()).unwrap_or_default(), + } + } + }) + .collect(); - let debug_impl = if impl_debug_on_refinement { - let refinement_field_debugs: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - quote! { - if self.#name.is_some() { - debug_struct.field(stringify!(#name), &self.#name); - } else { - all_some = false; - } - } - }) - .collect(); + let debug_impl = if impl_debug_on_refinement { + let refinement_field_debugs: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + quote! { + if self.#name.is_some() { + debug_struct.field(stringify!(#name), &self.#name); + } else { + all_some = false; + } + } + }) + .collect(); - quote! { - impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics - #where_clause - { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug_struct = f.debug_struct(stringify!(#refinement_ident)); - let mut all_some = true; - #( #refinement_field_debugs )* - if all_some { - debug_struct.finish() - } else { - debug_struct.finish_non_exhaustive() - } - } - } - } - } else { - quote! {} - }; + quote! { + impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics + #where_clause + { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_struct = f.debug_struct(stringify!(#refinement_ident)); + let mut all_some = true; + #( #refinement_field_debugs )* + if all_some { + debug_struct.finish() + } else { + debug_struct.finish_non_exhaustive() + } + } + } + } + } else { + quote! {} + }; - let refinement_is_empty_conditions: Vec = fields - .iter() - .enumerate() - .map(|(i, field)| { - let name = &field.ident; + let refinement_is_empty_conditions: Vec = fields + .iter() + .enumerate() + .map(|(i, field)| { + let name = &field.ident; - let condition = if is_refineable_field(field) { - quote! { self.#name.is_empty() } - } else { - quote! { self.#name.is_none() } - }; + let condition = if is_refineable_field(field) { + quote! { self.#name.is_empty() } + } else { + quote! { self.#name.is_none() } + }; - if i < fields.len() - 1 { - quote! { #condition && } - } else { - condition - } - }) - .collect(); + if i < fields.len() - 1 { + quote! { #condition && } + } else { + condition + } + }) + .collect(); - let refineable_is_superset_conditions: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); + let refineable_is_superset_conditions: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); - if is_refineable { - quote! { - if !self.#name.is_superset_of(&refinement.#name) { - return false; - } - } - } else if is_optional { - quote! { - if refinement.#name.is_some() && &self.#name != &refinement.#name { - return false; - } - } - } else { - quote! { - if let Some(refinement_value) = &refinement.#name { - if &self.#name != refinement_value { - return false; - } - } - } - } - }) - .collect(); + if is_refineable { + quote! { + if !self.#name.is_superset_of(&refinement.#name) { + return false; + } + } + } else if is_optional { + quote! { + if refinement.#name.is_some() && &self.#name != &refinement.#name { + return false; + } + } + } else { + quote! { + if let Some(refinement_value) = &refinement.#name { + if &self.#name != refinement_value { + return false; + } + } + } + } + }) + .collect(); - let refinement_is_superset_conditions: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); + let refinement_is_superset_conditions: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); - if is_refineable { - quote! { - if !self.#name.is_superset_of(&refinement.#name) { - return false; - } - } - } else { - quote! { - if refinement.#name.is_some() && &self.#name != &refinement.#name { - return false; - } - } - } - }) - .collect(); + if is_refineable { + quote! { + if !self.#name.is_superset_of(&refinement.#name) { + return false; + } + } + } else { + quote! { + if refinement.#name.is_some() && &self.#name != &refinement.#name { + return false; + } + } + } + }) + .collect(); - let refineable_subtract_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); + let refineable_subtract_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); - if is_refineable { - quote! { - #name: self.#name.subtract(&refinement.#name), - } - } else if is_optional { - quote! { - #name: if &self.#name == &refinement.#name { - None - } else { - self.#name.clone() - }, - } - } else { - quote! { - #name: if let Some(refinement_value) = &refinement.#name { - if &self.#name == refinement_value { - None - } else { - Some(self.#name.clone()) - } - } else { - Some(self.#name.clone()) - }, - } - } - }) - .collect(); + if is_refineable { + quote! { + #name: self.#name.subtract(&refinement.#name), + } + } else if is_optional { + quote! { + #name: if &self.#name == &refinement.#name { + None + } else { + self.#name.clone() + }, + } + } else { + quote! { + #name: if let Some(refinement_value) = &refinement.#name { + if &self.#name == refinement_value { + None + } else { + Some(self.#name.clone()) + } + } else { + Some(self.#name.clone()) + }, + } + } + }) + .collect(); - let refinement_subtract_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); + let refinement_subtract_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); - if is_refineable { - quote! { - #name: self.#name.subtract(&refinement.#name), - } - } else { - quote! { - #name: if &self.#name == &refinement.#name { - None - } else { - self.#name.clone() - }, - } - } - }) - .collect(); + if is_refineable { + quote! { + #name: self.#name.subtract(&refinement.#name), + } + } else { + quote! { + #name: if &self.#name == &refinement.#name { + None + } else { + self.#name.clone() + }, + } + } + }) + .collect(); - let mut derive_stream = quote! {}; - for trait_to_derive in refinement_traits_to_derive { - derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) - } + let mut derive_stream = quote! {}; + for trait_to_derive in refinement_traits_to_derive { + derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) + } - let r#gen = quote! { - /// A refinable version of [`#ident`], see that documentation for details. - #[derive(Clone)] - #derive_stream - pub struct #refinement_ident #impl_generics { - #( - #[allow(missing_docs)] - #field_attributes - #field_visibilities #field_names: #wrapped_types - ),* - } + let r#gen = quote! { + /// A refinable version of [`#ident`], see that documentation for details. + #[derive(Clone)] + #derive_stream + pub struct #refinement_ident #impl_generics { + #( + #[allow(missing_docs)] + #field_attributes + #field_visibilities #field_names: #wrapped_types + ),* + } - impl #impl_generics Refineable for #ident #ty_generics - #where_clause - { - type Refinement = #refinement_ident #ty_generics; + impl #impl_generics Refineable for #ident #ty_generics + #where_clause + { + type Refinement = #refinement_ident #ty_generics; - fn refine(&mut self, refinement: &Self::Refinement) { - #( #refineable_refine_assignments )* - } + fn refine(&mut self, refinement: &Self::Refinement) { + #( #refineable_refine_assignments )* + } - fn refined(mut self, refinement: Self::Refinement) -> Self { - #( #refineable_refined_assignments )* - self - } + fn refined(mut self, refinement: Self::Refinement) -> Self { + #( #refineable_refined_assignments )* + self + } - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool - { - #( #refineable_is_superset_conditions )* - true - } + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool + { + #( #refineable_is_superset_conditions )* + true + } - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement - { - #refinement_ident { - #( #refineable_subtract_assignments )* - } - } - } + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement + { + #refinement_ident { + #( #refineable_subtract_assignments )* + } + } + } - impl #impl_generics Refineable for #refinement_ident #ty_generics - #where_clause - { - type Refinement = #refinement_ident #ty_generics; + impl #impl_generics Refineable for #refinement_ident #ty_generics + #where_clause + { + type Refinement = #refinement_ident #ty_generics; - fn refine(&mut self, refinement: &Self::Refinement) { - #( #refinement_refine_assignments )* - } + fn refine(&mut self, refinement: &Self::Refinement) { + #( #refinement_refine_assignments )* + } - fn refined(mut self, refinement: Self::Refinement) -> Self { - #( #refinement_refined_assignments )* - self - } + fn refined(mut self, refinement: Self::Refinement) -> Self { + #( #refinement_refined_assignments )* + self + } - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool - { - #( #refinement_is_superset_conditions )* - true - } + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool + { + #( #refinement_is_superset_conditions )* + true + } - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement - { - #refinement_ident { - #( #refinement_subtract_assignments )* - } - } - } + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement + { + #refinement_ident { + #( #refinement_subtract_assignments )* + } + } + } - impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics - #where_clause - { - fn is_empty(&self) -> bool { - #( #refinement_is_empty_conditions )* - } - } + impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics + #where_clause + { + fn is_empty(&self) -> bool { + #( #refinement_is_empty_conditions )* + } + } - impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics - #where_clause - { - fn from(value: #refinement_ident #ty_generics) -> Self { - Self { - #( #from_refinement_assignments )* - } - } - } + impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics + #where_clause + { + fn from(value: #refinement_ident #ty_generics) -> Self { + Self { + #( #from_refinement_assignments )* + } + } + } - impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics - #where_clause - { - fn default() -> Self { - #refinement_ident { - #( #field_names: Default::default() ),* - } - } - } + impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics + #where_clause + { + fn default() -> Self { + #refinement_ident { + #( #field_names: Default::default() ),* + } + } + } - impl #impl_generics #refinement_ident #ty_generics - #where_clause - { - /// Returns `true` if all fields are `Some` - pub fn is_some(&self) -> bool { - #( - if self.#field_names.is_some() { - return true; - } - )* - false - } - } + impl #impl_generics #refinement_ident #ty_generics + #where_clause + { + /// Returns `true` if all fields are `Some` + pub fn is_some(&self) -> bool { + #( + if self.#field_names.is_some() { + return true; + } + )* + false + } + } - #debug_impl - }; - r#gen.into() + #debug_impl + }; + r#gen.into() } fn is_refineable_field(f: &Field) -> bool { - f.attrs - .iter() - .any(|attr| attr.path().is_ident("refineable")) + f.attrs + .iter() + .any(|attr| attr.path().is_ident("refineable")) } fn is_optional_field(f: &Field) -> bool { - if let Type::Path(typepath) = &f.ty - && typepath.qself.is_none() - { - let segments = &typepath.path.segments; - if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { - return true; - } - } - false + if let Type::Path(typepath) = &f.ty + && typepath.qself.is_none() + { + let segments = &typepath.path.segments; + if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { + return true; + } + } + false } fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type { - if is_refineable_field(field) { - let struct_name = if let Type::Path(tp) = ty { - tp.path.segments.last().unwrap().ident.clone() - } else { - panic!("Expected struct type for a refineable field"); - }; + if is_refineable_field(field) { + let struct_name = if let Type::Path(tp) = ty { + tp.path.segments.last().unwrap().ident.clone() + } else { + panic!("Expected struct type for a refineable field"); + }; - let refinement_struct_name = if struct_name.to_string().ends_with("Refinement") { - format_ident!("{}", struct_name) - } else { - format_ident!("{}Refinement", struct_name) - }; - let generics = if let Type::Path(tp) = ty { - &tp.path.segments.last().unwrap().arguments - } else { - &syn::PathArguments::None - }; - parse_quote!(#refinement_struct_name #generics) - } else if is_optional_field(field) { - ty.clone() - } else { - parse_quote!(Option<#ty>) - } + let refinement_struct_name = if struct_name.to_string().ends_with("Refinement") { + format_ident!("{}", struct_name) + } else { + format_ident!("{}Refinement", struct_name) + }; + let generics = if let Type::Path(tp) = ty { + &tp.path.segments.last().unwrap().arguments + } else { + &syn::PathArguments::None + }; + parse_quote!(#refinement_struct_name #generics) + } else if is_optional_field(field) { + ty.clone() + } else { + parse_quote!(Option<#ty>) + } } diff --git a/crates/gpui_elements/examples/editable_text.rs b/crates/gpui_elements/examples/editable_text.rs index 4fcffea1f6..1d1e4e67cd 100644 --- a/crates/gpui_elements/examples/editable_text.rs +++ b/crates/gpui_elements/examples/editable_text.rs @@ -1,66 +1,66 @@ use gpui::{ - App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, + App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, }; use gpui_elements::editable_text::{ - actions::{DEFAULT_INPUT_CONTEXT, default_bindings}, - text_area, text_input, + actions::{DEFAULT_INPUT_CONTEXT, default_bindings}, + text_area, text_input, }; struct Example; impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .bg(rgb(0x505050)) - .flex() - .flex_col() - .p_2() - .gap_2() - .items_start() - .justify_start() - .child( - text_input("input-field") - .caret_blink_interval_500ms() - .placeholder("some placeholder text") - .border_1() - .rounded_lg() - .border_color(Hsla::white()) // has a border - .p_2() // padding between the text and border - .min_w_10() - .max_w_128() - .min_h_auto() - .max_h_auto() - .whitespace_nowrap(), - ) - .child( - text_area("text-area") - .placeholder("empty text") - .border_1() - .rounded_lg() - .border_color(Hsla::white()) // has a border - .p_2() // padding between the text and border - .w_full() - .min_h_24() - .max_h_128() - .whitespace_normal() // default - .overflow_y_scroll(), - ) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .bg(rgb(0x505050)) + .flex() + .flex_col() + .p_2() + .gap_2() + .items_start() + .justify_start() + .child( + text_input("input-field") + .caret_blink_interval_500ms() + .placeholder("some placeholder text") + .border_1() + .rounded_lg() + .border_color(Hsla::white()) // has a border + .p_2() // padding between the text and border + .min_w_10() + .max_w_128() + .min_h_auto() + .max_h_auto() + .whitespace_nowrap(), + ) + .child( + text_area("text-area") + .placeholder("empty text") + .border_1() + .rounded_lg() + .border_color(Hsla::white()) // has a border + .p_2() // padding between the text and border + .w_full() + .min_h_24() + .max_h_128() + .whitespace_normal() // default + .overflow_y_scroll(), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT))); + gpui_platform::application().run(|cx: &mut App| { + cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT))); - 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(|_| Example), - ) - .unwrap(); - cx.activate(true); - }); + 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(|_| Example), + ) + .unwrap(); + cx.activate(true); + }); } diff --git a/crates/gpui_elements/src/editable_text/actions.rs b/crates/gpui_elements/src/editable_text/actions.rs index 8e540a42d5..139d61a020 100644 --- a/crates/gpui_elements/src/editable_text/actions.rs +++ b/crates/gpui_elements/src/editable_text/actions.rs @@ -6,77 +6,77 @@ use std::{cell::RefCell, rc::Rc}; pub const DEFAULT_INPUT_CONTEXT: &str = "EditableText"; gpui::actions!( - actions, - [ - /// Blur focus from the input. - Escape, - /// Insert a newline at the cursor position. - Enter, - /// Insert a tab character at the cursor position. - Tab, - /// Delete the character before the cursor. - DeleteLeft, - /// Delete the character after the cursor. - DeleteRight, - /// Delete the word before the cursor. - DeleteWordLeft, - /// Delete the word after the cursor. - DeleteWordRight, - /// Delete from the cursor to the beginning of the line. - DeleteToLineStart, - /// Delete from the cursor to the end of the line. - DeleteToLineEnd, - /// Move the cursor one character to the left. - NavLeft, - /// Move the cursor one character to the right. - NavRight, - /// Move the cursor up one visual line. - NavUp, - /// Move the cursor down one visual line. - NavDown, - /// Move cursor to the start of the current line. - NavLineStart, - /// Move cursor to the end of the current line. - NavLineEnd, - /// Move cursor to the beginning of the content. - NavDocumentStart, - /// Move cursor to the end of the content. - NavDocumentEnd, - /// Move cursor one word to the left. - NavWordLeft, - /// Move cursor one word to the right. - NavWordRight, - /// Select all text content. - SelectAll, - /// Extend selection one character to the left. - SelectLeft, - /// Extend selection one character to the right. - SelectRight, - /// Extend selection up one visual line. - SelectUp, - /// Extend selection down one visual line. - SelectDown, - /// Extend selection to the beginning of the content. - SelectDocumentStart, - /// Extend selection to the end of the content. - SelectDocumentEnd, - /// Extend selection one word to the left. - SelectWordLeft, - /// Extend selection one word to the right. - SelectWordRight, - /// Cut selected text to clipboard. - Cut, - /// Copy selected text to clipboard. - Copy, - /// Paste from clipboard at the cursor position. - Paste, - /// Undo the last edit. - Undo, - /// Redo the last undone edit. - Redo, - /// Show the platform character palette. - ShowCharacterPalette, - ] + actions, + [ + /// Blur focus from the input. + Escape, + /// Insert a newline at the cursor position. + Enter, + /// Insert a tab character at the cursor position. + Tab, + /// Delete the character before the cursor. + DeleteLeft, + /// Delete the character after the cursor. + DeleteRight, + /// Delete the word before the cursor. + DeleteWordLeft, + /// Delete the word after the cursor. + DeleteWordRight, + /// Delete from the cursor to the beginning of the line. + DeleteToLineStart, + /// Delete from the cursor to the end of the line. + DeleteToLineEnd, + /// Move the cursor one character to the left. + NavLeft, + /// Move the cursor one character to the right. + NavRight, + /// Move the cursor up one visual line. + NavUp, + /// Move the cursor down one visual line. + NavDown, + /// Move cursor to the start of the current line. + NavLineStart, + /// Move cursor to the end of the current line. + NavLineEnd, + /// Move cursor to the beginning of the content. + NavDocumentStart, + /// Move cursor to the end of the content. + NavDocumentEnd, + /// Move cursor one word to the left. + NavWordLeft, + /// Move cursor one word to the right. + NavWordRight, + /// Select all text content. + SelectAll, + /// Extend selection one character to the left. + SelectLeft, + /// Extend selection one character to the right. + SelectRight, + /// Extend selection up one visual line. + SelectUp, + /// Extend selection down one visual line. + SelectDown, + /// Extend selection to the beginning of the content. + SelectDocumentStart, + /// Extend selection to the end of the content. + SelectDocumentEnd, + /// Extend selection one word to the left. + SelectWordLeft, + /// Extend selection one word to the right. + SelectWordRight, + /// Cut selected text to clipboard. + Cut, + /// Copy selected text to clipboard. + Copy, + /// Paste from clipboard at the cursor position. + Paste, + /// Undo the last edit. + Undo, + /// Redo the last undone edit. + Redo, + /// Show the platform character palette. + ShowCharacterPalette, + ] ); /// Creates a collection of default keystroke bindings for EditableText actions. @@ -122,235 +122,235 @@ gpui::actions!( /// /// TODO: Collection does not supply a way to unbind a default keystroke pub fn default_bindings() -> gpui::ActionBindingCollection { - let mut bindings = gpui::ActionBindingCollection::default() - .with::("backspace") - .with::("delete") - .with::("tab") - .with::("enter") - .with::("left") - .with::("right") - .with::("up") - .with::("down") - .with::("secondary-a") - .with::("shift-left") - .with::("shift-right") - .with::("shift-up") - .with::("shift-down") - .with::("secondary-c") - .with::("secondary-x") - .with::("secondary-v") - .with::("secondary-z") - .with::("secondary-shift-z") - .with::("escape") - .with::("secondary-space"); + let mut bindings = gpui::ActionBindingCollection::default() + .with::("backspace") + .with::("delete") + .with::("tab") + .with::("enter") + .with::("left") + .with::("right") + .with::("up") + .with::("down") + .with::("secondary-a") + .with::("shift-left") + .with::("shift-right") + .with::("shift-up") + .with::("shift-down") + .with::("secondary-c") + .with::("secondary-x") + .with::("secondary-v") + .with::("secondary-z") + .with::("secondary-shift-z") + .with::("escape") + .with::("secondary-space"); - #[cfg(target_os = "macos")] - { - bindings = bindings - .with::("alt-backspace") - .with::("alt-delete") - .with::("cmd-backspace") - .with::("ctrl-k") - // Mac keyboards don't have Home/End keys, so cmd-left/right are standard - .with::("cmd-left") - .with::("cmd-right") - .with::("cmd-up") - .with::("cmd-down") - .with::("cmd-shift-up") - .with::("cmd-shift-down") - .with::("alt-left") - .with::("alt-right") - .with::("alt-shift-left") - .with::("alt-shift-right"); - } + #[cfg(target_os = "macos")] + { + bindings = bindings + .with::("alt-backspace") + .with::("alt-delete") + .with::("cmd-backspace") + .with::("ctrl-k") + // Mac keyboards don't have Home/End keys, so cmd-left/right are standard + .with::("cmd-left") + .with::("cmd-right") + .with::("cmd-up") + .with::("cmd-down") + .with::("cmd-shift-up") + .with::("cmd-shift-down") + .with::("alt-left") + .with::("alt-right") + .with::("alt-shift-left") + .with::("alt-shift-right"); + } - #[cfg(not(target_os = "macos"))] - { - bindings = bindings - .with::("ctrl-backspace") - .with::("ctrl-delete") - .with::("ctrl-shift-backspace") - .with::("ctrl-shift-delete") - .with::("home") - .with::("end") - .with::("ctrl-home") - .with::("ctrl-end") - .with::("ctrl-shift-home") - .with::("ctrl-shift-end") - .with::("ctrl-left") - .with::("ctrl-right") - .with::("ctrl-shift-left") - .with::("ctrl-shift-right"); - } + #[cfg(not(target_os = "macos"))] + { + bindings = bindings + .with::("ctrl-backspace") + .with::("ctrl-delete") + .with::("ctrl-shift-backspace") + .with::("ctrl-shift-delete") + .with::("home") + .with::("end") + .with::("ctrl-home") + .with::("ctrl-end") + .with::("ctrl-shift-home") + .with::("ctrl-shift-end") + .with::("ctrl-left") + .with::("ctrl-right") + .with::("ctrl-shift-left") + .with::("ctrl-shift-right"); + } - bindings + bindings } /// Declares stubs for all editable-text actions that an element's state entity can implement. pub trait EditableTextActionHandler: Sized { - /// Blur focus from the input. - fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Context) {} + /// Blur focus from the input. + fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Context) {} - /// Insert a newline at the cursor position. - fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Context) {} - /// Insert a tab character at the cursor position. - fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut Context) {} + /// Insert a newline at the cursor position. + fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Context) {} + /// Insert a tab character at the cursor position. + fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut Context) {} - /// Delete the character before the cursor. - fn delete_left(&mut self, _: &DeleteLeft, _w: &mut Window, _cx: &mut Context) {} - /// Delete the character after the cursor. - fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, _cx: &mut Context) {} - /// Delete the word before the cursor. - fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut Context) {} - /// Delete the word after the cursor. - fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut Context) {} - /// Delete from the cursor to the beginning of the line. - fn delete_to_line_start(&mut self, _: &DeleteToLineStart, _w: &mut Window, _cx: &mut Context) {} - /// Delete from the cursor to the end of the line. - fn delete_to_line_end(&mut self, _: &DeleteToLineEnd, _w: &mut Window, _cx: &mut Context) {} + /// Delete the character before the cursor. + fn delete_left(&mut self, _: &DeleteLeft, _w: &mut Window, _cx: &mut Context) {} + /// Delete the character after the cursor. + fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, _cx: &mut Context) {} + /// Delete the word before the cursor. + fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut Context) {} + /// Delete the word after the cursor. + fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut Context) {} + /// Delete from the cursor to the beginning of the line. + fn delete_to_line_start(&mut self, _: &DeleteToLineStart, _w: &mut Window, _cx: &mut Context) {} + /// Delete from the cursor to the end of the line. + fn delete_to_line_end(&mut self, _: &DeleteToLineEnd, _w: &mut Window, _cx: &mut Context) {} - /// Move the cursor one character to the left. - fn nav_left(&mut self, _: &NavLeft, _w: &mut Window, _cx: &mut Context) {} - /// Move the cursor one character to the right. - fn nav_right(&mut self, _: &NavRight, _w: &mut Window, _cx: &mut Context) {} - /// Move the cursor up one visual line. - fn nav_up(&mut self, _: &NavUp, _w: &mut Window, _cx: &mut Context) {} - /// Move the cursor down one visual line. - fn nav_down(&mut self, _: &NavDown, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor to the start of the current line. - fn nav_line_start(&mut self, _: &NavLineStart, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor to the end of the current line. - fn nav_line_end(&mut self, _: &NavLineEnd, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor to the start of the document. - fn nav_start(&mut self, _: &NavDocumentStart, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor to the end of the document. - fn nav_end(&mut self, _: &NavDocumentEnd, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor one word to the left. - fn nav_left_word(&mut self, _: &NavWordLeft, _w: &mut Window, _cx: &mut Context) {} - /// Move cursor one word to the right. - fn nav_right_word(&mut self, _: &NavWordRight, _w: &mut Window, _cx: &mut Context) {} + /// Move the cursor one character to the left. + fn nav_left(&mut self, _: &NavLeft, _w: &mut Window, _cx: &mut Context) {} + /// Move the cursor one character to the right. + fn nav_right(&mut self, _: &NavRight, _w: &mut Window, _cx: &mut Context) {} + /// Move the cursor up one visual line. + fn nav_up(&mut self, _: &NavUp, _w: &mut Window, _cx: &mut Context) {} + /// Move the cursor down one visual line. + fn nav_down(&mut self, _: &NavDown, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor to the start of the current line. + fn nav_line_start(&mut self, _: &NavLineStart, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor to the end of the current line. + fn nav_line_end(&mut self, _: &NavLineEnd, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor to the start of the document. + fn nav_start(&mut self, _: &NavDocumentStart, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor to the end of the document. + fn nav_end(&mut self, _: &NavDocumentEnd, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor one word to the left. + fn nav_left_word(&mut self, _: &NavWordLeft, _w: &mut Window, _cx: &mut Context) {} + /// Move cursor one word to the right. + fn nav_right_word(&mut self, _: &NavWordRight, _w: &mut Window, _cx: &mut Context) {} - /// Select the entire document. - fn select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection one character to the left. - fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection one character to the right. - fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection up one visual line. - fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection down one visual line. - fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection to the beginning of the document. - fn select_start(&mut self, _: &SelectDocumentStart, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection to the end of the document. - fn select_end(&mut self, _: &SelectDocumentEnd, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection one word to the left. - fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut Context) {} - /// Extend selection one word to the right. - fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut Context) {} + /// Select the entire document. + fn select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection one character to the left. + fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection one character to the right. + fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection up one visual line. + fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection down one visual line. + fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection to the beginning of the document. + fn select_start(&mut self, _: &SelectDocumentStart, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection to the end of the document. + fn select_end(&mut self, _: &SelectDocumentEnd, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection one word to the left. + fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut Context) {} + /// Extend selection one word to the right. + fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut Context) {} - /// Cut selected text to clipboard. - fn cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut Context) {} - /// Copy selected text to clipboard. - fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut Context) {} - /// Paste from clipboard at the cursor position. - fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut Context) {} + /// Cut selected text to clipboard. + fn cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut Context) {} + /// Copy selected text to clipboard. + fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut Context) {} + /// Paste from clipboard at the cursor position. + fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut Context) {} - /// Undo the last edit. - fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Context) {} - /// Redo the last undone edit. - fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Context) {} + /// Undo the last edit. + fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Context) {} + /// Redo the last undone edit. + fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Context) {} - /// Show the platform character palette. - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - _cx: &mut Context, - ) { - window.show_character_palette(); - } + /// Show the platform character palette. + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _cx: &mut Context, + ) { + window.show_character_palette(); + } - fn on_mouse_down( - &mut self, - _event: &gpui::MouseDownEvent, - _text_position: gpui::Point, - _w: &mut Window, - _cx: &mut Context, - ) { - } - fn on_mouse_up(&mut self, _event: &gpui::MouseUpEvent, _w: &mut Window, _cx: &mut Context) {} - fn on_mouse_move( - &mut self, - _event: &gpui::MouseMoveEvent, - _text_position: gpui::Point, - _w: &mut Window, - _cx: &mut Context, - ) { - } + fn on_mouse_down( + &mut self, + _event: &gpui::MouseDownEvent, + _text_position: gpui::Point, + _w: &mut Window, + _cx: &mut Context, + ) { + } + fn on_mouse_up(&mut self, _event: &gpui::MouseUpEvent, _w: &mut Window, _cx: &mut Context) {} + fn on_mouse_move( + &mut self, + _event: &gpui::MouseMoveEvent, + _text_position: gpui::Point, + _w: &mut Window, + _cx: &mut Context, + ) { + } } /// Registers an handler function of [`EditableTextActionHandler`] /// which is processed via the return value of [`EditableTextActionElement::state_entity_rc`]. macro_rules! register_action { - ($action_element:expr, $func:ident) => {{ - let entity_rc = $action_element.state_entity_rc().clone(); - $action_element - .interactivity() - .on_action(move |action, window, cx| { - let weak_entity = entity_rc.borrow(); - if let Some(entity) = weak_entity.upgrade() { - entity.update(cx, |state, cx| { - state.$func(action, window, cx); - }); - } - }); - }}; + ($action_element:expr, $func:ident) => {{ + let entity_rc = $action_element.state_entity_rc().clone(); + $action_element + .interactivity() + .on_action(move |action, window, cx| { + let weak_entity = entity_rc.borrow(); + if let Some(entity) = weak_entity.upgrade() { + entity.update(cx, |state, cx| { + state.$func(action, window, cx); + }); + } + }); + }}; } /// Generic trait to support an element backed by an internal state entity to bind to all editable-text input actions. pub(super) trait EditableTextActionElement { - fn state_entity_rc(&self) -> &Rc>>; + fn state_entity_rc(&self) -> &Rc>>; - fn register_actions(&mut self) - where - Self: InteractiveElement, - State: for<'app> EditableTextActionHandler>, - State: 'static, - { - register_action!(self, escape); - register_action!(self, insert_enter); - register_action!(self, insert_tab); - register_action!(self, delete_left); - register_action!(self, delete_right); - register_action!(self, delete_word_left); - register_action!(self, delete_word_right); - register_action!(self, delete_to_line_start); - register_action!(self, delete_to_line_end); - register_action!(self, nav_left); - register_action!(self, nav_right); - register_action!(self, nav_up); - register_action!(self, nav_down); - register_action!(self, nav_line_start); - register_action!(self, nav_line_end); - register_action!(self, nav_start); - register_action!(self, nav_end); - register_action!(self, nav_left_word); - register_action!(self, nav_right_word); - register_action!(self, select_all); - register_action!(self, select_left); - register_action!(self, select_right); - register_action!(self, select_up); - register_action!(self, select_down); - register_action!(self, select_start); - register_action!(self, select_end); - register_action!(self, select_left_word); - register_action!(self, select_right_word); - register_action!(self, cut); - register_action!(self, copy); - register_action!(self, paste); - register_action!(self, undo); - register_action!(self, redo); - register_action!(self, show_character_palette); - } + fn register_actions(&mut self) + where + Self: InteractiveElement, + State: for<'app> EditableTextActionHandler>, + State: 'static, + { + register_action!(self, escape); + register_action!(self, insert_enter); + register_action!(self, insert_tab); + register_action!(self, delete_left); + register_action!(self, delete_right); + register_action!(self, delete_word_left); + register_action!(self, delete_word_right); + register_action!(self, delete_to_line_start); + register_action!(self, delete_to_line_end); + register_action!(self, nav_left); + register_action!(self, nav_right); + register_action!(self, nav_up); + register_action!(self, nav_down); + register_action!(self, nav_line_start); + register_action!(self, nav_line_end); + register_action!(self, nav_start); + register_action!(self, nav_end); + register_action!(self, nav_left_word); + register_action!(self, nav_right_word); + register_action!(self, select_all); + register_action!(self, select_left); + register_action!(self, select_right); + register_action!(self, select_up); + register_action!(self, select_down); + register_action!(self, select_start); + register_action!(self, select_end); + register_action!(self, select_left_word); + register_action!(self, select_right_word); + register_action!(self, cut); + register_action!(self, copy); + register_action!(self, paste); + register_action!(self, undo); + register_action!(self, redo); + register_action!(self, show_character_palette); + } } diff --git a/crates/gpui_elements/src/editable_text/caret.rs b/crates/gpui_elements/src/editable_text/caret.rs index 401cfa3ebb..b705e90de0 100644 --- a/crates/gpui_elements/src/editable_text/caret.rs +++ b/crates/gpui_elements/src/editable_text/caret.rs @@ -7,156 +7,156 @@ pub const BLINK_INTERVAL_500MS: Duration = Duration::from_millis(500); /// Events emitted that the [`Caret`] listens to. pub enum CaretNotify { - /// The caret should pause blinking in response to a user-action - PauseBlinking, + /// The caret should pause blinking in response to a user-action + PauseBlinking, } /// State of an EditableText caret cursor, which supports features like blinking. /// Blinking is disabled by default. pub struct Caret { - /// The frequency at which the caret blinks - interval: Duration, - generation: usize, - /// Whether the caret is presently visible in this frame - visible: bool, - /// Whether the caret's EditableText element is currently focused. - /// Caret is only eligible to be blinking if currently focused. - has_focus: bool, - /// true when blinking is active but paused for some number of frames - paused: bool, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 2]>, - /// Tracks whether we were focused on the last update. - was_focused: bool, + /// The frequency at which the caret blinks + interval: Duration, + generation: usize, + /// Whether the caret is presently visible in this frame + visible: bool, + /// Whether the caret's EditableText element is currently focused. + /// Caret is only eligible to be blinking if currently focused. + has_focus: bool, + /// true when blinking is active but paused for some number of frames + paused: bool, + #[allow(dead_code)] + subscriptions: SmallVec<[Subscription; 2]>, + /// Tracks whether we were focused on the last update. + was_focused: bool, } impl Default for Caret { - fn default() -> Self { - Self { - interval: Duration::ZERO, - generation: Default::default(), - visible: false, - has_focus: false, - paused: false, - subscriptions: SmallVec::new(), - was_focused: false, - } - } + fn default() -> Self { + Self { + interval: Duration::ZERO, + generation: Default::default(), + visible: false, + has_focus: false, + paused: false, + subscriptions: SmallVec::new(), + was_focused: false, + } + } } impl Caret { - /// Returns the duration of the current blink interval - pub fn blink_interval(&self) -> Duration { - self.interval - } + /// Returns the duration of the current blink interval + pub fn blink_interval(&self) -> Duration { + self.interval + } - /// Sets the blinking interval of the caret. - pub fn set_blink_interval(&mut self, interval: Duration) { - self.interval = interval; - } + /// Sets the blinking interval of the caret. + pub fn set_blink_interval(&mut self, interval: Duration) { + self.interval = interval; + } - /// Sets the blinking interval of the caret. - pub fn with_blink_interval(mut self, interval: Duration) -> Self { - self.set_blink_interval(interval); - self - } + /// Sets the blinking interval of the caret. + pub fn with_blink_interval(mut self, interval: Duration) -> Self { + self.set_blink_interval(interval); + self + } - /// Sets the blinking interval of the caret to the global "default". - /// The true default of the caret is "do not blink". - pub fn with_blink_interval_500ms(self) -> Self { - self.with_blink_interval(BLINK_INTERVAL_500MS) - } + /// Sets the blinking interval of the caret to the global "default". + /// The true default of the caret is "do not blink". + pub fn with_blink_interval_500ms(self) -> Self { + self.with_blink_interval(BLINK_INTERVAL_500MS) + } - /// Listens for CaretNotify events on an entity (e.g. [`EditableTextState`]). - pub fn subscribe_to(&mut self, emitter: &Entity, cx: &mut Context) - where - E: EventEmitter, - { - let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event { - CaretNotify::PauseBlinking => { - if state.interval.is_zero() || !state.has_focus { - return; - } + /// Listens for CaretNotify events on an entity (e.g. [`EditableTextState`]). + pub fn subscribe_to(&mut self, emitter: &Entity, cx: &mut Context) + where + E: EventEmitter, + { + let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event { + CaretNotify::PauseBlinking => { + if state.interval.is_zero() || !state.has_focus { + return; + } - // Temporarily pauses blinking and leaves the caret visible. Blinking will resume after - // the pre-established interval elapses from the time this is called. - if !state.visible { - state.visible = true; - } - state.paused = true; - state.restart_blink_ticker(cx); - cx.notify(); - } - }); - self.subscriptions.push(handle); - } + // Temporarily pauses blinking and leaves the caret visible. Blinking will resume after + // the pre-established interval elapses from the time this is called. + if !state.visible { + state.visible = true; + } + state.paused = true; + state.restart_blink_ticker(cx); + cx.notify(); + } + }); + self.subscriptions.push(handle); + } - /// Processes updates during prepaint and returns whether the caret is currently visible. - pub(super) fn update_focus(&mut self, is_focused: bool, cx: &mut Context) -> bool { - let was_focused = self.was_focused; - self.was_focused = is_focused; + /// Processes updates during prepaint and returns whether the caret is currently visible. + pub(super) fn update_focus(&mut self, is_focused: bool, cx: &mut Context) -> bool { + let was_focused = self.was_focused; + self.was_focused = is_focused; - // Caret has no blinking interval, it is always visible - if self.interval.is_zero() { - return is_focused; - } + // Caret has no blinking interval, it is always visible + if self.interval.is_zero() { + return is_focused; + } - match (was_focused, is_focused) { - // Caret has a blinking interval, and gained focused. - (false, true) => { - self.has_focus = true; - self.paused = false; + match (was_focused, is_focused) { + // Caret has a blinking interval, and gained focused. + (false, true) => { + self.has_focus = true; + self.paused = false; - // Render in this frame and restart the blinking ticker. - self.visible = true; - self.restart_blink_ticker(cx); - true - } - // Caret has a blinking interval and lost focus - (true, false) => { - self.has_focus = false; - self.visible = false; - self.paused = false; - cx.notify(); - false - } - // Has a blinking interval, but focus has not changed. - // Only render if currently visible (based on blink ticker). - _ => self.visible, - } - } + // Render in this frame and restart the blinking ticker. + self.visible = true; + self.restart_blink_ticker(cx); + true + } + // Caret has a blinking interval and lost focus + (true, false) => { + self.has_focus = false; + self.visible = false; + self.paused = false; + cx.notify(); + false + } + // Has a blinking interval, but focus has not changed. + // Only render if currently visible (based on blink ticker). + _ => self.visible, + } + } - fn restart_blink_ticker(&mut self, cx: &mut Context) { - let generation = self.generation.wrapping_add(1); - self.generation = generation; + fn restart_blink_ticker(&mut self, cx: &mut Context) { + let generation = self.generation.wrapping_add(1); + self.generation = generation; - let interval = self.interval; - cx.spawn(async move |this, cx| { - cx.background_executor().timer(interval).await; + let interval = self.interval; + cx.spawn(async move |this, cx| { + cx.background_executor().timer(interval).await; - let Some(this) = this.upgrade() else { return }; - this.update(cx, |this, cx| { - // If the generation has changed, that means a new task was spawned. - // This one should be no-op since a new task is owning the blinking state. - if this.generation == generation { - // PauseBlinking increments the generation via restart_ticker, - // so we can always unpause the blinking if the generation is unchanged. - this.paused = false; + let Some(this) = this.upgrade() else { return }; + this.update(cx, |this, cx| { + // If the generation has changed, that means a new task was spawned. + // This one should be no-op since a new task is owning the blinking state. + if this.generation == generation { + // PauseBlinking increments the generation via restart_ticker, + // so we can always unpause the blinking if the generation is unchanged. + this.paused = false; - // This was the last tick/blink before we lost focus. - // Should now go inert until focus is regained. - if !this.has_focus { - return; - } + // This was the last tick/blink before we lost focus. + // Should now go inert until focus is regained. + if !this.has_focus { + return; + } - // We still have focus, toggle whether caret is visible and make sure the owning element re-renders. - this.visible = !this.visible; - cx.notify(); + // We still have focus, toggle whether caret is visible and make sure the owning element re-renders. + this.visible = !this.visible; + cx.notify(); - // Start a fresh cycle by respawning the task. - this.restart_blink_ticker(cx); - } - }); - }) - .detach(); - } + // Start a fresh cycle by respawning the task. + this.restart_blink_ticker(cx); + } + }); + }) + .detach(); + } } diff --git a/crates/gpui_elements/src/editable_text/element.rs b/crates/gpui_elements/src/editable_text/element.rs index 48812ba851..416c8e989f 100644 --- a/crates/gpui_elements/src/editable_text/element.rs +++ b/crates/gpui_elements/src/editable_text/element.rs @@ -1,15 +1,15 @@ use crate::editable_text::{ - BLINK_INTERVAL_500MS, Caret, EditableTextState, - actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler}, - layout::{EditableTextLayoutResult, EditableTextLayoutState, TextLineSegment}, + BLINK_INTERVAL_500MS, Caret, EditableTextState, + actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler}, + layout::{EditableTextLayoutResult, EditableTextLayoutState, TextLineSegment}, }; use gpui::{ - App, Bounds, CursorStyle, DispatchPhase, Display, Element, ElementId, ElementInputHandler, - Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement, - Interactivity, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, PaintQuad, Pixels, Point, SharedString, Size, StatefulInteractiveElement, Style, - StyleRefinement, Styled, TextAlign, TextLayout, WeakEntity, Window, WrappedLine, fill, point, - px, size, + App, Bounds, CursorStyle, DispatchPhase, Display, Element, ElementId, ElementInputHandler, + Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement, + Interactivity, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, PaintQuad, Pixels, Point, SharedString, Size, StatefulInteractiveElement, Style, + StyleRefinement, Styled, TextAlign, TextLayout, WeakEntity, Window, WrappedLine, fill, point, + px, size, }; use smallvec::SmallVec; use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc, time::Duration}; @@ -22,665 +22,665 @@ const CARET_RENDER_WIDTH: f32 = 2.0; /// By default it is multiline, and therefore this is semantically equivalent to [`text_area`]. #[track_caller] pub fn editable_text(id: impl Into) -> EditableTextElement { - let mut this = EditableTextElement { - interactivity: Interactivity::default(), - state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())), - supports_multiline: true, - placeholder: None, - accepts_input: true, - colors: EditableTextColors::default(), - caret_blink_interval: None, - }; - this.interactivity.element_id = Some(id.into()); + let mut this = EditableTextElement { + interactivity: Interactivity::default(), + state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())), + supports_multiline: true, + placeholder: None, + accepts_input: true, + colors: EditableTextColors::default(), + caret_blink_interval: None, + }; + this.interactivity.element_id = Some(id.into()); - this = this.key_context(DEFAULT_INPUT_CONTEXT); - this.register_actions(); + this = this.key_context(DEFAULT_INPUT_CONTEXT); + this.register_actions(); - this + this } /// Creates a singleline text input element. /// See [`EditableTextElement`] for usage. #[track_caller] pub fn text_input(id: impl Into) -> EditableTextElement { - editable_text(id).multiline(false) + editable_text(id).multiline(false) } /// Creates a multiline text input element. /// See [`EditableTextElement`] for usage. #[track_caller] pub fn text_area(id: impl Into) -> EditableTextElement { - editable_text(id).multiline(true) + editable_text(id).multiline(true) } /// An input field which users can type text into. pub struct EditableTextElement { - interactivity: Interactivity, - // Populated on first render with an entity stored/attached to the view. - // This reference is shared with the action handlers, which are processed between renders - // and therefore cannot otherwise access state attached to the view. - state_entity: Rc>>, - supports_multiline: bool, - placeholder: Option, - accepts_input: bool, - colors: EditableTextColors, - caret_blink_interval: Option, + interactivity: Interactivity, + // Populated on first render with an entity stored/attached to the view. + // This reference is shared with the action handlers, which are processed between renders + // and therefore cannot otherwise access state attached to the view. + state_entity: Rc>>, + supports_multiline: bool, + placeholder: Option, + accepts_input: bool, + colors: EditableTextColors, + caret_blink_interval: Option, } /// EditableText styling that goes beyond what Style/StyleRefinement supports struct EditableTextColors { - /// Color of the placeholder text when the storage is empty. - /// Could be reconceived as a refinement of text_color when the field is empty - placeholder: Hsla, - /// Color of the selection box. - /// Could be driven by platform-provided styling? - selection: Hsla, - /// Color of the caret / text cursor - caret: Hsla, - /// Color of IME marked underlines - ime_underline: Hsla, + /// Color of the placeholder text when the storage is empty. + /// Could be reconceived as a refinement of text_color when the field is empty + placeholder: Hsla, + /// Color of the selection box. + /// Could be driven by platform-provided styling? + selection: Hsla, + /// Color of the caret / text cursor + caret: Hsla, + /// Color of IME marked underlines + ime_underline: Hsla, } impl Default for EditableTextColors { - fn default() -> Self { - const WHITE_50PC: Hsla = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 0.5, - }; - const WHITE_70PC: Hsla = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 0.7, - }; - // approx rgb(38 79 120) or oklch(41.9% 0.0829 250.4) - const LIGHT_NAVY_BLUE_50PC: Hsla = Hsla { - h: 0.583, - s: 0.519, - l: 0.31, - a: 0.5, - }; - Self { - placeholder: WHITE_50PC, - selection: LIGHT_NAVY_BLUE_50PC, - caret: Hsla::white(), - ime_underline: WHITE_70PC, - } - } + fn default() -> Self { + const WHITE_50PC: Hsla = Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: 0.5, + }; + const WHITE_70PC: Hsla = Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: 0.7, + }; + // approx rgb(38 79 120) or oklch(41.9% 0.0829 250.4) + const LIGHT_NAVY_BLUE_50PC: Hsla = Hsla { + h: 0.583, + s: 0.519, + l: 0.31, + a: 0.5, + }; + Self { + placeholder: WHITE_50PC, + selection: LIGHT_NAVY_BLUE_50PC, + caret: Hsla::white(), + ime_underline: WHITE_70PC, + } + } } impl EditableTextElement { - /// Assigns the underlying state of this element, which should persist across multiple frames. - /// The user should either create the entity once or utilize `Window::use_keyed_state` - /// to create an entity intrinsicly linked to the element. - /// If no state is configured, one will be linked to this element on first render via `Window::use_keyed_state`. - pub fn state(self, state: WeakEntity) -> Self { - *self.state_entity.borrow_mut() = state; - self - } + /// Assigns the underlying state of this element, which should persist across multiple frames. + /// The user should either create the entity once or utilize `Window::use_keyed_state` + /// to create an entity intrinsicly linked to the element. + /// If no state is configured, one will be linked to this element on first render via `Window::use_keyed_state`. + pub fn state(self, state: WeakEntity) -> Self { + *self.state_entity.borrow_mut() = state; + self + } - /// Configures whether the field supports multiple lines of text. - /// Disabling this prevents actions like `enter` and navigating up and down. - /// - /// It doesnt not automatically sanitize inputs from containing newlines (e.g. on paste). - /// This is a limitation of the current state of implementation and requires further iteration. - pub fn multiline(mut self, enabled: bool) -> Self { - self.supports_multiline = enabled; - self - } + /// Configures whether the field supports multiple lines of text. + /// Disabling this prevents actions like `enter` and navigating up and down. + /// + /// It doesnt not automatically sanitize inputs from containing newlines (e.g. on paste). + /// This is a limitation of the current state of implementation and requires further iteration. + pub fn multiline(mut self, enabled: bool) -> Self { + self.supports_multiline = enabled; + self + } - /// Assigns the text that should be displayed when storage of the element is empty. - pub fn placeholder(mut self, text: impl Into) -> Self { - self.placeholder = Some(text.into()); - self - } + /// Assigns the text that should be displayed when storage of the element is empty. + pub fn placeholder(mut self, text: impl Into) -> Self { + self.placeholder = Some(text.into()); + self + } - /// Configures whether the element can accept input (effectively means "is the element currently enabled"). - pub fn accepts_input(mut self, enabled: bool) -> Self { - self.accepts_input = enabled; - self - } + /// Configures whether the element can accept input (effectively means "is the element currently enabled"). + pub fn accepts_input(mut self, enabled: bool) -> Self { + self.accepts_input = enabled; + self + } - /// Sets the blinking interval of the caret. - pub fn caret_blink_interval(mut self, duration: Duration) -> Self { - self.caret_blink_interval = Some(duration); - self - } + /// Sets the blinking interval of the caret. + pub fn caret_blink_interval(mut self, duration: Duration) -> Self { + self.caret_blink_interval = Some(duration); + self + } - /// Sets the blinking interval of the caret to 500ms - pub fn caret_blink_interval_500ms(self) -> Self { - self.caret_blink_interval(BLINK_INTERVAL_500MS) - } + /// Sets the blinking interval of the caret to 500ms + pub fn caret_blink_interval_500ms(self) -> Self { + self.caret_blink_interval(BLINK_INTERVAL_500MS) + } - /// Sets the color of the placeholder text which is rendered when the element's stored text is empty. - /// - /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). - pub fn placeholder_color(mut self, color: Hsla) -> Self { - self.colors.placeholder = color; - self - } + /// Sets the color of the placeholder text which is rendered when the element's stored text is empty. + /// + /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). + pub fn placeholder_color(mut self, color: Hsla) -> Self { + self.colors.placeholder = color; + self + } - /// Sets the color of the box highlighting selected text. - /// - /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). - pub fn selection_color(mut self, color: Hsla) -> Self { - self.colors.selection = color; - self - } + /// Sets the color of the box highlighting selected text. + /// + /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). + pub fn selection_color(mut self, color: Hsla) -> Self { + self.colors.selection = color; + self + } - /// Sets the color of the caret / text-cursor. - /// - /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). - pub fn caret_color(mut self, color: Hsla) -> Self { - self.colors.caret = color; - self - } + /// Sets the color of the caret / text-cursor. + /// + /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). + pub fn caret_color(mut self, color: Hsla) -> Self { + self.colors.caret = color; + self + } - /// Sets the color of the underlines rendered underneath text being editted/marked by InputMethodEditors - /// (for writing Chinese, Japanese, and Korean utf-16). - /// - /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). - pub fn marked_color(mut self, color: Hsla) -> Self { - self.colors.ime_underline = color; - self - } + /// Sets the color of the underlines rendered underneath text being editted/marked by InputMethodEditors + /// (for writing Chinese, Japanese, and Korean utf-16). + /// + /// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style). + pub fn marked_color(mut self, color: Hsla) -> Self { + self.colors.ime_underline = color; + self + } } impl InteractiveElement for EditableTextElement { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } } // forced implementation since the API for the element doesnt use Stateful impl StatefulInteractiveElement for EditableTextElement {} impl Styled for EditableTextElement { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } } impl IntoElement for EditableTextElement { - type Element = Self; - fn into_element(self) -> Self::Element { - self - } + type Element = Self; + fn into_element(self) -> Self::Element { + self + } } impl EditableTextActionElement for EditableTextElement { - fn state_entity_rc(&self) -> &Rc>> { - &self.state_entity - } + fn state_entity_rc(&self) -> &Rc>> { + &self.state_entity + } } struct PrelayoutState { - state: Entity, - prev_layout_state: EditableTextLayoutState, - storage_version: u16, - show_placeholder: bool, - text: Option, - placeholder_color: Hsla, - supports_multiline: bool, - accepts_input: bool, + state: Entity, + prev_layout_state: EditableTextLayoutState, + storage_version: u16, + show_placeholder: bool, + text: Option, + placeholder_color: Hsla, + supports_multiline: bool, + accepts_input: bool, } #[doc(hidden)] pub struct LayoutState { - state: Entity, - caret: Entity, + state: Entity, + caret: Entity, } struct InteractivityPrepaint { - hitbox: Option, - scroll_offset: Point, - inner_bounds: Bounds, - caret_visible: bool, + hitbox: Option, + scroll_offset: Point, + inner_bounds: Bounds, + caret_visible: bool, } /// Internal type containing prepaint information used to paint the element #[doc(hidden)] pub struct PrepaintState { - interactivity: InteractivityPrepaint, - focus_handle: FocusHandle, - elements: PrepaintElements, + interactivity: InteractivityPrepaint, + focus_handle: FocusHandle, + elements: PrepaintElements, } impl Element for EditableTextElement { - type RequestLayoutState = LayoutState; - type PrepaintState = PrepaintState; + type RequestLayoutState = LayoutState; + type PrepaintState = PrepaintState; - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - self.interactivity.source_location() - } + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + self.interactivity.source_location() + } - fn request_layout( - &mut self, - global_id: Option<&gpui::GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let entity = self.find_or_create_state(window, cx); - let caret = self.find_or_create_caret(&entity, window, cx); + fn request_layout( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let entity = self.find_or_create_state(window, cx); + let caret = self.find_or_create_caret(&entity, window, cx); - if let Some(duration) = self.caret_blink_interval.take() - && caret.read(cx).blink_interval() != duration - { - caret.update(cx, |caret, _cx| caret.set_blink_interval(duration)); - } + if let Some(duration) = self.caret_blink_interval.take() + && caret.read(cx).blink_interval() != duration + { + caret.update(cx, |caret, _cx| caret.set_blink_interval(duration)); + } - // Read new state information from the underlying entity. - // Block-wrapped so that the state being read is dropped before continuing. - let (prelayout, next_scroll_offset) = { - let state = entity.read(cx); - let show_placeholder = state.as_str().is_empty(); - let text = match show_placeholder { - false => Some(SharedString::from(state.as_str())), - true => self.placeholder.clone(), - }; + // Read new state information from the underlying entity. + // Block-wrapped so that the state being read is dropped before continuing. + let (prelayout, next_scroll_offset) = { + let state = entity.read(cx); + let show_placeholder = state.as_str().is_empty(); + let text = match show_placeholder { + false => Some(SharedString::from(state.as_str())), + true => self.placeholder.clone(), + }; - let prelayout = PrelayoutState { - state: entity.clone(), - prev_layout_state: state.layout_data.state, - show_placeholder, - storage_version: state.version(), - text, - placeholder_color: self.colors.placeholder, - supports_multiline: self.supports_multiline, - accepts_input: self.accepts_input, - }; - (prelayout, state.layout_data.next_scroll_offset) - }; + let prelayout = PrelayoutState { + state: entity.clone(), + prev_layout_state: state.layout_data.state, + show_placeholder, + storage_version: state.version(), + text, + placeholder_color: self.colors.placeholder, + supports_multiline: self.supports_multiline, + accepts_input: self.accepts_input, + }; + (prelayout, state.layout_data.next_scroll_offset) + }; - // Update the scroll offset of the element when the user's caret goes out of scope. - if let Some(scroll_offset) = next_scroll_offset { - self.interactivity - .set_scroll_offset(global_id, window, -scroll_offset); + // Update the scroll offset of the element when the user's caret goes out of scope. + if let Some(scroll_offset) = next_scroll_offset { + self.interactivity + .set_scroll_offset(global_id, window, -scroll_offset); - // Clear scroll_layout here in the very likely event that we wont need to - // recompute layout, in which case the layout result isnt rebuilt during `perform_text_layout`. - entity.update(cx, |state, _cx| { - state.layout_data.next_scroll_offset = None; - }); - } + // Clear scroll_layout here in the very likely event that we wont need to + // recompute layout, in which case the layout result isnt rebuilt during `perform_text_layout`. + entity.update(cx, |state, _cx| { + state.layout_data.next_scroll_offset = None; + }); + } - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |style, window, cx| { - window.with_text_style(style.text_style().cloned(), move |window| { - let text_layout_id = prelayout.perform_text_layout(window); - window.request_layout(style.clone(), Some(text_layout_id), cx) - }) - }, - ); + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| { + window.with_text_style(style.text_style().cloned(), move |window| { + let text_layout_id = prelayout.perform_text_layout(window); + window.request_layout(style.clone(), Some(text_layout_id), cx) + }) + }, + ); - ( - layout_id, - LayoutState { - state: entity, - caret, - }, - ) - } + ( + layout_id, + LayoutState { + state: entity, + caret, + }, + ) + } - fn prepaint( - &mut self, - global_id: Option<&gpui::GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - // should reflect the text content layout size of the stored text, - // so that scrolling can take it into account during prepaint. - let (content_size, focus_handle) = { - let state = request_layout.state.read(cx); - let content_size = state.layout_data.state.size.unwrap_or_else(|| bounds.size); - let focus_handle = state.focus_handle(cx); - (content_size, focus_handle) - }; + fn prepaint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + // should reflect the text content layout size of the stored text, + // so that scrolling can take it into account during prepaint. + let (content_size, focus_handle) = { + let state = request_layout.state.read(cx); + let content_size = state.layout_data.state.size.unwrap_or_else(|| bounds.size); + let focus_handle = state.focus_handle(cx); + (content_size, focus_handle) + }; - let is_focused = focus_handle.is_focused(window); - let caret_visible = request_layout - .caret - .update(cx, |caret, cx| caret.update_focus(is_focused, cx)); - window.set_focus_handle(&focus_handle, cx); + let is_focused = focus_handle.is_focused(window); + let caret_visible = request_layout + .caret + .update(cx, |caret, cx| caret.update_focus(is_focused, cx)); + window.set_focus_handle(&focus_handle, cx); - let prepaint = self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - content_size, - window, - cx, - |style, scroll_offset, hitbox, window, cx| { - let hitbox = - hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))); - let inner_bounds = { - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); + let prepaint = self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + content_size, + window, + cx, + |style, scroll_offset, hitbox, window, cx| { + let hitbox = + hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))); + let inner_bounds = { + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); - let mut bounds = bounds; - bounds.origin += point(padding.left, padding.top); - bounds.size.width -= padding.left + padding.right; - bounds.size.height -= padding.top + padding.bottom; - bounds - }; - request_layout.state.update(cx, |state, _cx| { - // while gpui tracks scroll_offset with negative values, - // this is converted into positive for usage with bounds - state.layout_data.scroll_bounds = - Bounds::new(-scroll_offset, inner_bounds.size); - }); - InteractivityPrepaint { - hitbox, - scroll_offset, - inner_bounds, - caret_visible, - } - }, - ); + let mut bounds = bounds; + bounds.origin += point(padding.left, padding.top); + bounds.size.width -= padding.left + padding.right; + bounds.size.height -= padding.top + padding.bottom; + bounds + }; + request_layout.state.update(cx, |state, _cx| { + // while gpui tracks scroll_offset with negative values, + // this is converted into positive for usage with bounds + state.layout_data.scroll_bounds = + Bounds::new(-scroll_offset, inner_bounds.size); + }); + InteractivityPrepaint { + hitbox, + scroll_offset, + inner_bounds, + caret_visible, + } + }, + ); - let state = request_layout.state.read(cx); - let elements = PrepaintElements::build_elements(state, &prepaint, &self.colors, window); + let state = request_layout.state.read(cx); + let elements = PrepaintElements::build_elements(state, &prepaint, &self.colors, window); - PrepaintState { - interactivity: prepaint, - focus_handle, - elements, - } - } + PrepaintState { + interactivity: prepaint, + focus_handle, + elements, + } + } - fn paint( - &mut self, - global_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, - ) { - if let Some(hitbox) = &prepaint.interactivity.hitbox { - window.set_cursor_style(CursorStyle::IBeam, hitbox); - } + fn paint( + &mut self, + global_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, + ) { + if let Some(hitbox) = &prepaint.interactivity.hitbox { + window.set_cursor_style(CursorStyle::IBeam, hitbox); + } - let accepts_input = self.accepts_input; - let hitbox = prepaint.interactivity.hitbox.clone(); - let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { - if style.display == Display::None { - return; - } + let accepts_input = self.accepts_input; + let hitbox = prepaint.interactivity.hitbox.clone(); + let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { + if style.display == Display::None { + return; + } - // Register event listeners to the window for the next frame - if accepts_input { - Self::process_frame_events(prepaint, bounds, &request_layout.state, window, cx); - } + // Register event listeners to the window for the next frame + if accepts_input { + Self::process_frame_events(prepaint, bounds, &request_layout.state, window, cx); + } - // Actually draw the elements we constructed during prepaint - let line_h = window.line_height(); - for PrepaintLine { line, point, align } in prepaint.elements.lines.drain(..) { - let _ = line.paint(point, line_h, align, Some(bounds), window, cx); - } - for quad in prepaint.elements.ime_marked.drain(..) { - window.paint_quad(quad); - } - for quad in prepaint.elements.selection.drain(..) { - window.paint_quad(quad); - } - if let Some(quad) = prepaint.elements.caret.take() { - window.paint_quad(quad); - } - }; + // Actually draw the elements we constructed during prepaint + let line_h = window.line_height(); + for PrepaintLine { line, point, align } in prepaint.elements.lines.drain(..) { + let _ = line.paint(point, line_h, align, Some(bounds), window, cx); + } + for quad in prepaint.elements.ime_marked.drain(..) { + window.paint_quad(quad); + } + for quad in prepaint.elements.selection.drain(..) { + window.paint_quad(quad); + } + if let Some(quad) = prepaint.elements.caret.take() { + window.paint_quad(quad); + } + }; - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox.as_ref(), - window, - cx, - perform_paint, - ); - } + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + perform_paint, + ); + } } impl EditableTextElement { - fn find_or_create_state(&self, window: &mut Window, cx: &mut App) -> Entity { - if let Some(entity) = self.state_entity.borrow().upgrade() { - return entity; - } - let Some(element_id) = self.interactivity.element_id.clone() else { - unimplemented!("all input elements must be assigned an id") - }; + fn find_or_create_state(&self, window: &mut Window, cx: &mut App) -> Entity { + if let Some(entity) = self.state_entity.borrow().upgrade() { + return entity; + } + let Some(element_id) = self.interactivity.element_id.clone() else { + unimplemented!("all input elements must be assigned an id") + }; - let state = EditableTextState::use_keyed(element_id, window, cx); - // store a reference to the entity owned by the element for access in action handlers - *self.state_entity_rc().borrow_mut() = state.downgrade(); - state - } + let state = EditableTextState::use_keyed(element_id, window, cx); + // store a reference to the entity owned by the element for access in action handlers + *self.state_entity_rc().borrow_mut() = state.downgrade(); + state + } - fn find_or_create_caret( - &self, - state: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - let Some(element_id) = self.interactivity.element_id.clone() else { - unimplemented!("all input elements must be assigned an id") - }; + fn find_or_create_caret( + &self, + state: &Entity, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let Some(element_id) = self.interactivity.element_id.clone() else { + unimplemented!("all input elements must be assigned an id") + }; - window.use_keyed_state(element_id, cx, |_window, cx| { - let mut caret = Caret::default(); - caret.subscribe_to(state, cx); - caret - }) - } + window.use_keyed_state(element_id, cx, |_window, cx| { + let mut caret = Caret::default(); + caret.subscribe_to(state, cx); + caret + }) + } - fn process_frame_events( - prepaint: &PrepaintState, - bounds: Bounds, - entity: &Entity, - window: &mut Window, - cx: &mut App, - ) { - let inner_bounds = prepaint.interactivity.inner_bounds; - let to_local_position = -(bounds.origin + prepaint.interactivity.scroll_offset); + fn process_frame_events( + prepaint: &PrepaintState, + bounds: Bounds, + entity: &Entity, + window: &mut Window, + cx: &mut App, + ) { + let inner_bounds = prepaint.interactivity.inner_bounds; + let to_local_position = -(bounds.origin + prepaint.interactivity.scroll_offset); - let ime_handler = ElementInputHandler::new(inner_bounds, entity.clone()); - window.handle_input(&prepaint.focus_handle, ime_handler, cx); + let ime_handler = ElementInputHandler::new(inner_bounds, entity.clone()); + window.handle_input(&prepaint.focus_handle, ime_handler, cx); - window.on_mouse_event({ - let focus_handle = prepaint.focus_handle.clone(); - let state = entity.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - if !bounds.contains(&event.position) { - return; - } - if event.button != MouseButton::Left { - return; - } + window.on_mouse_event({ + let focus_handle = prepaint.focus_handle.clone(); + let state = entity.clone(); + move |event: &MouseDownEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + if !bounds.contains(&event.position) { + return; + } + if event.button != MouseButton::Left { + return; + } - cx.stop_propagation(); - window.focus(&focus_handle, cx); + cx.stop_propagation(); + window.focus(&focus_handle, cx); - let text_position = event.position + to_local_position; - state.update(cx, |state, cx| { - state.on_mouse_down(event, text_position, window, cx); - }); - } - }); - window.on_mouse_event({ - let state = entity.clone(); - move |event: &MouseUpEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - if event.button != MouseButton::Left { - return; - } + let text_position = event.position + to_local_position; + state.update(cx, |state, cx| { + state.on_mouse_down(event, text_position, window, cx); + }); + } + }); + window.on_mouse_event({ + let state = entity.clone(); + move |event: &MouseUpEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + if event.button != MouseButton::Left { + return; + } - state.update(cx, |state, cx| { - state.on_mouse_up(event, window, cx); - }); - } - }); - window.on_mouse_event({ - let state = entity.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } + state.update(cx, |state, cx| { + state.on_mouse_up(event, window, cx); + }); + } + }); + window.on_mouse_event({ + let state = entity.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } - let text_position = event.position + to_local_position; - state.update(cx, |state, cx| { - state.on_mouse_move(event, text_position, window, cx); - }); - } - }); - } + let text_position = event.position + to_local_position; + state.update(cx, |state, cx| { + state.on_mouse_move(event, text_position, window, cx); + }); + } + }); + } } impl PrelayoutState { - fn perform_text_layout(self, window: &mut Window) -> LayoutId { - // NOTE: Loosely mirrors TextLayout::layout - 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()), - ); + fn perform_text_layout(self, window: &mut Window) -> LayoutId { + // NOTE: Loosely mirrors TextLayout::layout + 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 color = match self.show_placeholder { - false => text_style.color, - true => self.placeholder_color, - }; + let color = match self.show_placeholder { + false => text_style.color, + true => self.placeholder_color, + }; - let text = self.text.unwrap_or_default(); + let text = self.text.unwrap_or_default(); - window.request_measured_layout( - Default::default(), - // This is invoked sometime in the near future (before prepaint but not immediately), - // so we avoid doing any pre-emptive work until the layout engine is ready. - move |known_dimensions, available_space, window, cx| { - let runs = vec![gpui::TextRun { - len: text.len(), - font: text_style.font(), - color, - background_color: None, - underline: None, - strikethrough: None, - letter_spacing: None, - }]; + window.request_measured_layout( + Default::default(), + // This is invoked sometime in the near future (before prepaint but not immediately), + // so we avoid doing any pre-emptive work until the layout engine is ready. + move |known_dimensions, available_space, window, cx| { + let runs = vec![gpui::TextRun { + len: text.len(), + font: text_style.font(), + color, + background_color: None, + underline: None, + strikethrough: None, + letter_spacing: None, + }]; - let wrap_width = TextLayout::evaluate_wrap_width( - &text_style.white_space, - known_dimensions, - available_space, - ); + let wrap_width = TextLayout::evaluate_wrap_width( + &text_style.white_space, + known_dimensions, + available_space, + ); - let truncation = - TextLayout::evaluate_overflow(&text_style, known_dimensions, available_space); + let truncation = + TextLayout::evaluate_overflow(&text_style, known_dimensions, available_space); - if let Some(size) = self.prev_layout_state.size - && (wrap_width.is_none() || wrap_width == self.prev_layout_state.wrap_width) - && truncation.width.is_none() - && self.storage_version == self.prev_layout_state.last_seen_storage_version - { - return size; - } + if let Some(size) = self.prev_layout_state.size + && (wrap_width.is_none() || wrap_width == self.prev_layout_state.wrap_width) + && truncation.width.is_none() + && self.storage_version == self.prev_layout_state.last_seen_storage_version + { + return size; + } - let (text, runs) = TextLayout::apply_truncation( - text.clone(), - &text_style, - font_size, - wrap_width, - &truncation, - &runs, - cx, - ); - let text_len = text.len(); + let (text, runs) = TextLayout::apply_truncation( + text.clone(), + &text_style, + font_size, + wrap_width, + &truncation, + &runs, + cx, + ); + let text_len = text.len(); - let wrapped_lines = window - .text_system() - .shape_text(text, font_size, &runs, wrap_width, text_style.line_clamp) - .unwrap_or_default(); + let wrapped_lines = window + .text_system() + .shape_text(text, font_size, &runs, wrap_width, text_style.line_clamp) + .unwrap_or_default(); - // Build the size of the text and convert the wrapped_lines into - // lines that will be cached in state and painted. - let mut size: Size = Size::default(); - let mut pos_y = 0; - let mut line_start = 0; - let mut lines = Vec::with_capacity(wrapped_lines.len()); - for line in wrapped_lines { - let line_size = line.size(line_height); - size.height += line_size.height; - size.width = size.width.max(line_size.width).ceil(); + // Build the size of the text and convert the wrapped_lines into + // lines that will be cached in state and painted. + let mut size: Size = Size::default(); + let mut pos_y = 0; + let mut line_start = 0; + let mut lines = Vec::with_capacity(wrapped_lines.len()); + for line in wrapped_lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width).ceil(); - let mut line_len = line.len(); - if line_len < text_len { - // to offset for new-line characters that are - // omitted from WrappedLine range - line_len += 1; - } + let mut line_len = line.len(); + if line_len < text_len { + // to offset for new-line characters that are + // omitted from WrappedLine range + line_len += 1; + } - let segment = TextLineSegment { - text_range: line_start..line_start + line_len, - wrapped_line: Some(Arc::new(line)), - pos_y, - }; - line_start += line_len; - pos_y += segment.row_count(); - lines.push(segment); - } + let segment = TextLineSegment { + text_range: line_start..line_start + line_len, + wrapped_line: Some(Arc::new(line)), + pos_y, + }; + line_start += line_len; + pos_y += segment.row_count(); + lines.push(segment); + } - let layout_data = EditableTextLayoutResult { - supports_multiline: self.supports_multiline, - accepts_input: self.accepts_input, - // updated during prepaint - scroll_bounds: Bounds::default(), - state: EditableTextLayoutState { - wrap_width, - size: Some(size), - last_seen_storage_version: self.storage_version, - }, - lines, - line_height, - next_scroll_offset: None, - }; + let layout_data = EditableTextLayoutResult { + supports_multiline: self.supports_multiline, + accepts_input: self.accepts_input, + // updated during prepaint + scroll_bounds: Bounds::default(), + state: EditableTextLayoutState { + wrap_width, + size: Some(size), + last_seen_storage_version: self.storage_version, + }, + lines, + line_height, + next_scroll_offset: None, + }; - // Update the state for use in prepaint, paint, and action handlers. - // request_measured_layout caches this scope for processing later - // between layout and prepaint, so we cant just copy/move these values to the outer scope. - self.state.update(cx, move |state, _cx| { - state.layout_data = layout_data; - }); + // Update the state for use in prepaint, paint, and action handlers. + // request_measured_layout caches this scope for processing later + // between layout and prepaint, so we cant just copy/move these values to the outer scope. + self.state.update(cx, move |state, _cx| { + state.layout_data = layout_data; + }); - size - }, - ) - } + size + }, + ) + } } struct PrepaintLine { - line: Arc, - point: Point, - align: TextAlign, + line: Arc, + point: Point, + align: TextAlign, } const STACK_ALLOCATED_LINES: usize = 100usize; @@ -689,212 +689,212 @@ const STACK_ALLOCATED_QUADS_IME_MARKED: usize = 2usize; #[derive(Default)] struct PrepaintElements { - lines: SmallVec<[PrepaintLine; STACK_ALLOCATED_LINES]>, - selection: SmallVec<[PaintQuad; STACK_ALLOCATED_QUADS_SELECTION]>, - ime_marked: SmallVec<[PaintQuad; STACK_ALLOCATED_QUADS_IME_MARKED]>, - caret: Option, + lines: SmallVec<[PrepaintLine; STACK_ALLOCATED_LINES]>, + selection: SmallVec<[PaintQuad; STACK_ALLOCATED_QUADS_SELECTION]>, + ime_marked: SmallVec<[PaintQuad; STACK_ALLOCATED_QUADS_IME_MARKED]>, + caret: Option, } impl PrepaintElements { - fn build_quads( - offset_corners: Vec<(Point, Point)>, - origin: Point, - color: Hsla, - ) -> impl Iterator { - offset_corners - .into_iter() - .map(move |(offset_start, offset_end)| { - let bounds = Bounds::from_corners(origin + offset_start, origin + offset_end); - fill(bounds, color) - }) - } + fn build_quads( + offset_corners: Vec<(Point, Point)>, + origin: Point, + color: Hsla, + ) -> impl Iterator { + offset_corners + .into_iter() + .map(move |(offset_start, offset_end)| { + let bounds = Bounds::from_corners(origin + offset_start, origin + offset_end); + fill(bounds, color) + }) + } - fn build_elements( - state: &EditableTextState, - prepaint: &InteractivityPrepaint, - colors: &EditableTextColors, - window: &mut Window, - ) -> PrepaintElements { - let InteractivityPrepaint { - hitbox: _, - scroll_offset, - inner_bounds, - caret_visible, - } = prepaint; + fn build_elements( + state: &EditableTextState, + prepaint: &InteractivityPrepaint, + colors: &EditableTextColors, + window: &mut Window, + ) -> PrepaintElements { + let InteractivityPrepaint { + hitbox: _, + scroll_offset, + inner_bounds, + caret_visible, + } = prepaint; - let caret_pos = state.caret_pos(); - let selection = state.selected_range(); - let ime_range = state.marked_range(); + let caret_pos = state.caret_pos(); + let selection = state.selected_range(); + let ime_range = state.marked_range(); - let mut elements = PrepaintElements::default(); + let mut elements = PrepaintElements::default(); - let line_height = window.line_height(); - let is_range_contained_by_range = - |text_range: &Range, containing_range: &Range| { - if text_range.is_empty() { - containing_range.start <= text_range.start - && containing_range.end > text_range.start - } else { - containing_range.end > text_range.start - && containing_range.start < text_range.end - } - }; - let mut caret_point = None::>; - for segment in &state.layout_data.lines { - let line_distance_from_top = segment.pos_y * line_height; - let line_y = line_distance_from_top + scroll_offset.y; - let line_bottom = line_y + line_height * segment.row_count() as f32; - let line_visible = line_bottom >= Pixels::ZERO && line_y <= inner_bounds.size.height; - if !line_visible { - continue; - } + let line_height = window.line_height(); + let is_range_contained_by_range = + |text_range: &Range, containing_range: &Range| { + if text_range.is_empty() { + containing_range.start <= text_range.start + && containing_range.end > text_range.start + } else { + containing_range.end > text_range.start + && containing_range.start < text_range.end + } + }; + let mut caret_point = None::>; + for segment in &state.layout_data.lines { + let line_distance_from_top = segment.pos_y * line_height; + let line_y = line_distance_from_top + scroll_offset.y; + let line_bottom = line_y + line_height * segment.row_count() as f32; + let line_visible = line_bottom >= Pixels::ZERO && line_y <= inner_bounds.size.height; + if !line_visible { + continue; + } - if let Some(wrapped) = &segment.wrapped_line { - let point = inner_bounds.origin + point(scroll_offset.x, line_y); - elements.lines.push(PrepaintLine { - line: wrapped.clone(), - point, - align: TextAlign::Left, - }); - } + if let Some(wrapped) = &segment.wrapped_line { + let point = inner_bounds.origin + point(scroll_offset.x, line_y); + elements.lines.push(PrepaintLine { + line: wrapped.clone(), + point, + align: TextAlign::Left, + }); + } - let segment_is_empty = segment.text_range.is_empty(); + let segment_is_empty = segment.text_range.is_empty(); - if is_range_contained_by_range(&segment.text_range, &selection) { - if segment_is_empty { - const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.); - elements.selection.push(fill( - Bounds::from_corners( - inner_bounds.origin + point(Pixels::ZERO, line_y), - inner_bounds.origin - + point(EMPTY_LINE_SELECTION_WIDTH, line_y + line_height), - ), - colors.selection, - )); - } else { - let offset_corners = build_quad_over_text( - &selection, - segment, - line_y, - line_height, - Pixels::ZERO, - ); - elements.selection.extend(PrepaintElements::build_quads( - offset_corners, - inner_bounds.origin, - colors.selection, - )); - } - } + if is_range_contained_by_range(&segment.text_range, &selection) { + if segment_is_empty { + const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.); + elements.selection.push(fill( + Bounds::from_corners( + inner_bounds.origin + point(Pixels::ZERO, line_y), + inner_bounds.origin + + point(EMPTY_LINE_SELECTION_WIDTH, line_y + line_height), + ), + colors.selection, + )); + } else { + let offset_corners = build_quad_over_text( + &selection, + segment, + line_y, + line_height, + Pixels::ZERO, + ); + elements.selection.extend(PrepaintElements::build_quads( + offset_corners, + inner_bounds.origin, + colors.selection, + )); + } + } - if !segment_is_empty && let Some(ime_range) = &ime_range { - if !ime_range.is_empty() - && is_range_contained_by_range(&segment.text_range, &ime_range) - { - const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; - let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS); - let underline_offset = line_height - underline_thickness; + if !segment_is_empty && let Some(ime_range) = &ime_range { + if !ime_range.is_empty() + && is_range_contained_by_range(&segment.text_range, &ime_range) + { + const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; + let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS); + let underline_offset = line_height - underline_thickness; - let offset_corners = build_quad_over_text( - &ime_range, - segment, - line_y, - line_height, - underline_offset, - ); - elements.ime_marked.extend(PrepaintElements::build_quads( - offset_corners, - inner_bounds.origin, - colors.ime_underline, - )); - } - } + let offset_corners = build_quad_over_text( + &ime_range, + segment, + line_y, + line_height, + underline_offset, + ); + elements.ime_marked.extend(PrepaintElements::build_quads( + offset_corners, + inner_bounds.origin, + colors.ime_underline, + )); + } + } - let is_cursor_in_line = segment.contains_position(caret_pos, true); - if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line { - let local_offset = caret_pos.saturating_sub(segment.text_range.start); - let caret_px = wrapped - .position_for_index(local_offset, line_height) - .unwrap_or_default(); - caret_point = Some(caret_px + point(scroll_offset.x, line_y)); - } - } + let is_cursor_in_line = segment.contains_position(caret_pos, true); + if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line { + let local_offset = caret_pos.saturating_sub(segment.text_range.start); + let caret_px = wrapped + .position_for_index(local_offset, line_height) + .unwrap_or_default(); + caret_point = Some(caret_px + point(scroll_offset.x, line_y)); + } + } - if *caret_visible && let Some(carent_point) = caret_point { - let quad = fill( - Bounds::new( - inner_bounds.origin + carent_point, - size(gpui::px(CARET_RENDER_WIDTH), line_height), - ), - colors.caret, - ); - elements.caret = Some(quad); - } + if *caret_visible && let Some(carent_point) = caret_point { + let quad = fill( + Bounds::new( + inner_bounds.origin + carent_point, + size(gpui::px(CARET_RENDER_WIDTH), line_height), + ), + colors.caret, + ); + elements.caret = Some(quad); + } - elements - } + elements + } } fn build_quad_over_text( - containing_range: &Range, - segment: &TextLineSegment, - line_y: Pixels, - line_height: Pixels, - offset_y: Pixels, + containing_range: &Range, + segment: &TextLineSegment, + line_y: Pixels, + line_height: Pixels, + offset_y: Pixels, ) -> Vec<(Point, Point)> { - let Some(wrapped) = &segment.wrapped_line else { - return vec![]; - }; + let Some(wrapped) = &segment.wrapped_line else { + return vec![]; + }; - let line_start = segment.text_range.start; - let line_end = segment.text_range.end; + let line_start = segment.text_range.start; + let line_end = segment.text_range.end; - let subrange_start = containing_range.start.max(line_start) - line_start; - let subrange_end = containing_range.end.min(line_end) - line_start; + let subrange_start = containing_range.start.max(line_start) - line_start; + let subrange_end = containing_range.end.min(line_end) - line_start; - let start_pos = wrapped - .position_for_index(subrange_start, line_height) - .unwrap_or_default(); - let end_pos = wrapped - .position_for_index(subrange_end, line_height) - .unwrap_or_else(|| { - let last_line_y = line_height * (segment.row_count() - 1) as f32; - point(wrapped.width(), last_line_y) - }); + let start_pos = wrapped + .position_for_index(subrange_start, line_height) + .unwrap_or_default(); + let end_pos = wrapped + .position_for_index(subrange_end, line_height) + .unwrap_or_else(|| { + let last_line_y = line_height * (segment.row_count() - 1) as f32; + point(wrapped.width(), last_line_y) + }); - let start_visual_line = (start_pos.y / line_height).floor() as usize; - let end_visual_line = (end_pos.y / line_height).floor() as usize; + let start_visual_line = (start_pos.y / line_height).floor() as usize; + let end_visual_line = (end_pos.y / line_height).floor() as usize; - if start_visual_line == end_visual_line { - vec![( - point(start_pos.x, line_y + start_pos.y + offset_y), - point(end_pos.x, line_y + start_pos.y + line_height), - )] - } else { - let line_width = wrapped.width(); - let middle_lines = (start_visual_line + 1)..end_visual_line; - let mut quad_corners = Vec::with_capacity(middle_lines.end - middle_lines.start + 2); + if start_visual_line == end_visual_line { + vec![( + point(start_pos.x, line_y + start_pos.y + offset_y), + point(end_pos.x, line_y + start_pos.y + line_height), + )] + } else { + let line_width = wrapped.width(); + let middle_lines = (start_visual_line + 1)..end_visual_line; + let mut quad_corners = Vec::with_capacity(middle_lines.end - middle_lines.start + 2); - quad_corners.push(( - point(start_pos.x, line_y + start_pos.y + offset_y), - point(line_width, line_y + start_pos.y + line_height), - )); + quad_corners.push(( + point(start_pos.x, line_y + start_pos.y + offset_y), + point(line_width, line_y + start_pos.y + line_height), + )); - // Middle visual lines - for visual_line in (start_visual_line + 1)..end_visual_line { - let y = line_height * visual_line as f32; - quad_corners.push(( - point(Pixels::ZERO, line_y + y + offset_y), - point(line_width, line_y + y + line_height), - )); - } + // Middle visual lines + for visual_line in (start_visual_line + 1)..end_visual_line { + let y = line_height * visual_line as f32; + quad_corners.push(( + point(Pixels::ZERO, line_y + y + offset_y), + point(line_width, line_y + y + line_height), + )); + } - // Last visual line - quad_corners.push(( - point(Pixels::ZERO, line_y + end_pos.y + offset_y), - point(end_pos.x, line_y + end_pos.y + line_height), - )); + // Last visual line + quad_corners.push(( + point(Pixels::ZERO, line_y + end_pos.y + offset_y), + point(end_pos.x, line_y + end_pos.y + line_height), + )); - quad_corners - } + quad_corners + } } diff --git a/crates/gpui_elements/src/editable_text/history.rs b/crates/gpui_elements/src/editable_text/history.rs index 1d1187704b..127f4f6846 100644 --- a/crates/gpui_elements/src/editable_text/history.rs +++ b/crates/gpui_elements/src/editable_text/history.rs @@ -1,7 +1,7 @@ use smallvec::SmallVec; use std::{ - ops::Range, - time::{Duration, Instant}, + ops::Range, + time::{Duration, Instant}, }; /// Maximum number of history entries to keep. @@ -12,147 +12,147 @@ pub const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300); // TODO: Should history get attached directly to storage? currently its per text field and operate both on storage and selection pub struct EditableTextHistory { - /// The maximum duration between changes to `content` that can be grouped together as a single entry in the history log. - grouping_interval: Duration, - /// Stack of previous states for undo. - undo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>, - /// Stack of undone states for redo. - redo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>, + /// The maximum duration between changes to `content` that can be grouped together as a single entry in the history log. + grouping_interval: Duration, + /// Stack of previous states for undo. + undo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>, + /// Stack of undone states for redo. + redo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>, } impl Default for EditableTextHistory { - fn default() -> Self { - Self { - grouping_interval: DEFAULT_GROUP_INTERVAL, - undo_stack: Default::default(), - redo_stack: Default::default(), - } - } + fn default() -> Self { + Self { + grouping_interval: DEFAULT_GROUP_INTERVAL, + undo_stack: Default::default(), + redo_stack: Default::default(), + } + } } /// A patch-based history entry for memory-efficient undo/redo operations. /// Instead of storing the full content, we store only the change needed to reverse the edit. #[derive(Clone, Debug)] pub struct HistoryEntry { - /// The byte range that was modified (after the edit, for undo; before the edit, for redo). - pub range: Range, - /// The text that was replaced (to restore on undo). - pub old_text: String, - /// The length of the new text that replaced old_text (to know how much to remove on undo). - pub new_text_len: usize, - /// The selection range before the edit. - pub selected_range: (usize, usize), - /// Timestamp for grouping consecutive edits. - pub timestamp: Instant, + /// The byte range that was modified (after the edit, for undo; before the edit, for redo). + pub range: Range, + /// The text that was replaced (to restore on undo). + pub old_text: String, + /// The length of the new text that replaced old_text (to know how much to remove on undo). + pub new_text_len: usize, + /// The selection range before the edit. + pub selected_range: (usize, usize), + /// Timestamp for grouping consecutive edits. + pub timestamp: Instant, } #[derive(Clone, Debug)] pub enum HistoryKind { - Undo, - Redo, + Undo, + Redo, } impl EditableTextHistory { - pub fn set_grouping_interval(&mut self, interval: Duration) { - self.grouping_interval = interval; - } + pub fn set_grouping_interval(&mut self, interval: Duration) { + self.grouping_interval = interval; + } - pub fn record( - &mut self, - range: Range, - old_text: &str, - new_text_len: usize, - selected_range: (usize, usize), - ) { - let now = Instant::now(); + pub fn record( + &mut self, + range: Range, + old_text: &str, + new_text_len: usize, + selected_range: (usize, usize), + ) { + let now = Instant::now(); - // Check if we should group with the last entry - if let Some(last) = self.undo_stack.last_mut() - && now.duration_since(last.timestamp) < self.grouping_interval - { - // The change was triggered within group interval timing. - // Try to extend the existing patch (which is a mutation). - // If extending successeds, then we can early-out. Otherwise the mutation is non-contiguous. - if last.extend(&range, new_text_len) { - return; - } - } + // Check if we should group with the last entry + if let Some(last) = self.undo_stack.last_mut() + && now.duration_since(last.timestamp) < self.grouping_interval + { + // The change was triggered within group interval timing. + // Try to extend the existing patch (which is a mutation). + // If extending successeds, then we can early-out. Otherwise the mutation is non-contiguous. + if last.extend(&range, new_text_len) { + return; + } + } - // Limit history size - if self.undo_stack.len() >= MAX_HISTORY_LEN { - self.undo_stack.remove(0); - } + // Limit history size + if self.undo_stack.len() >= MAX_HISTORY_LEN { + self.undo_stack.remove(0); + } - self.push( - HistoryKind::Undo, - HistoryEntry { - range: range.start..range.start + new_text_len, - old_text: old_text.to_string(), - new_text_len, - selected_range, - timestamp: now, - }, - ); + self.push( + HistoryKind::Undo, + HistoryEntry { + range: range.start..range.start + new_text_len, + old_text: old_text.to_string(), + new_text_len, + selected_range, + timestamp: now, + }, + ); - // New edit invalidates redo stack - self.redo_stack.clear(); - } + // New edit invalidates redo stack + self.redo_stack.clear(); + } - fn stack(&self, kind: HistoryKind) -> &SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> { - // NOTE: Could be an internal map - match kind { - HistoryKind::Undo => &self.undo_stack, - HistoryKind::Redo => &self.redo_stack, - } - } + fn stack(&self, kind: HistoryKind) -> &SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> { + // NOTE: Could be an internal map + match kind { + HistoryKind::Undo => &self.undo_stack, + HistoryKind::Redo => &self.redo_stack, + } + } - fn stack_mut(&mut self, kind: HistoryKind) -> &mut SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> { - match kind { - HistoryKind::Undo => &mut self.undo_stack, - HistoryKind::Redo => &mut self.redo_stack, - } - } + fn stack_mut(&mut self, kind: HistoryKind) -> &mut SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> { + match kind { + HistoryKind::Undo => &mut self.undo_stack, + HistoryKind::Redo => &mut self.redo_stack, + } + } - pub fn has_next(&self, kind: HistoryKind) -> bool { - !self.stack(kind).is_empty() - } + pub fn has_next(&self, kind: HistoryKind) -> bool { + !self.stack(kind).is_empty() + } - pub fn push(&mut self, kind: HistoryKind, entry: HistoryEntry) { - self.stack_mut(kind).push(entry); - } + pub fn push(&mut self, kind: HistoryKind, entry: HistoryEntry) { + self.stack_mut(kind).push(entry); + } - pub fn take(&mut self, kind: HistoryKind) -> Option { - self.stack_mut(kind).pop() - } + pub fn take(&mut self, kind: HistoryKind) -> Option { + self.stack_mut(kind).pop() + } } impl HistoryEntry { - fn extend(&mut self, range: &Range, new_text_len: usize) -> bool { - // NOTE: Could be more robust. Currently only supports human-written extensions from start towards end. + fn extend(&mut self, range: &Range, new_text_len: usize) -> bool { + // NOTE: Could be more robust. Currently only supports human-written extensions from start towards end. - // ranges must be contiguous in order to integrate/extend - if self.range.end != range.start { - return false; - } + // ranges must be contiguous in order to integrate/extend + if self.range.end != range.start { + return false; + } - self.range.end = range.start + new_text_len; - self.new_text_len += new_text_len; + self.range.end = range.start + new_text_len; + self.new_text_len += new_text_len; - true - } + true + } - pub fn char_range(&self, max_len: usize) -> Range { - let undo_start = self.range.start; - let undo_end = (self.range.start + self.new_text_len).min(max_len); - undo_start..undo_end - } + pub fn char_range(&self, max_len: usize) -> Range { + let undo_start = self.range.start; + let undo_end = (self.range.start + self.new_text_len).min(max_len); + undo_start..undo_end + } - pub fn as_inverted(self, prev_text_at_range: String) -> Self { - HistoryEntry { - range: self.range.start..self.range.start + self.old_text.len(), - old_text: prev_text_at_range, - new_text_len: self.old_text.len(), - selected_range: self.selected_range, - timestamp: self.timestamp, - } - } + pub fn as_inverted(self, prev_text_at_range: String) -> Self { + HistoryEntry { + range: self.range.start..self.range.start + self.old_text.len(), + old_text: prev_text_at_range, + new_text_len: self.old_text.len(), + selected_range: self.selected_range, + timestamp: self.timestamp, + } + } } diff --git a/crates/gpui_elements/src/editable_text/layout.rs b/crates/gpui_elements/src/editable_text/layout.rs index 693dbbc3f5..e5f506cd05 100644 --- a/crates/gpui_elements/src/editable_text/layout.rs +++ b/crates/gpui_elements/src/editable_text/layout.rs @@ -4,100 +4,100 @@ use std::{ops::Range, sync::Arc}; /// Data used across successive layout requests to gauge whether layout must be recomputed. #[derive(Default, Clone, Copy)] pub(super) struct EditableTextLayoutState { - /// The last known width at which the lines were wrapped. - pub wrap_width: Option, - /// The last known size of the text, as generated during layout. - pub size: Option>, - /// The last seen version of `storage` (for tracking when lines need to be reprocessed during layout) - pub last_seen_storage_version: u16, + /// The last known width at which the lines were wrapped. + pub wrap_width: Option, + /// The last known size of the text, as generated during layout. + pub size: Option>, + /// The last seen version of `storage` (for tracking when lines need to be reprocessed during layout) + pub last_seen_storage_version: u16, } /// Internal state/result after the element has recomputed layout. #[derive(Default)] pub(super) struct EditableTextLayoutResult { - /// Whether the element supports multiple lines of text - pub supports_multiline: bool, - /// Whether the element is currently accepting inputs - pub accepts_input: bool, - /// The last seen scroll position and size of the element - pub scroll_bounds: Bounds, - pub state: EditableTextLayoutState, - /// The `ShapedLine` produced by the painter's `prepaint`. - /// Cached so IME `bounds_for_range` / `character_index_for_point` can evaluate without re-shaping. - pub lines: Vec, - pub line_height: Pixels, - /// The next position the scroll view should move to. - /// Set by the state in response to user actions. - pub next_scroll_offset: Option>, + /// Whether the element supports multiple lines of text + pub supports_multiline: bool, + /// Whether the element is currently accepting inputs + pub accepts_input: bool, + /// The last seen scroll position and size of the element + pub scroll_bounds: Bounds, + pub state: EditableTextLayoutState, + /// The `ShapedLine` produced by the painter's `prepaint`. + /// Cached so IME `bounds_for_range` / `character_index_for_point` can evaluate without re-shaping. + pub lines: Vec, + pub line_height: Pixels, + /// The next position the scroll view should move to. + /// Set by the state in response to user actions. + pub next_scroll_offset: Option>, } /// A segment of text that is a single logical/document line but can take up multiple rows due to wrapping. pub(super) struct TextLineSegment { - /// The utf8 byte range in the content string that this line covers. - pub text_range: Range, - /// The shaped and wrapped text for this line, if available. - pub wrapped_line: Option>, + /// The utf8 byte range in the content string that this line covers. + pub text_range: Range, + /// The shaped and wrapped text for this line, if available. + pub wrapped_line: Option>, - /// The y-coordinate of this segment which can be multiplied by the line_height - /// to get its pixel location relative to the bounds of the text area. - pub pos_y: usize, + /// The y-coordinate of this segment which can be multiplied by the line_height + /// to get its pixel location relative to the bounds of the text area. + pub pos_y: usize, } impl TextLineSegment { - /// The number of visual lines this segment encapsulates, - /// since it can occupy multiple rows due to wrapping. - pub fn row_count(&self) -> usize { - let count = self - .wrapped_line - .as_ref() - .map(|line| line.wrap_boundaries().len()); - count.unwrap_or_default() + 1 - } + /// The number of visual lines this segment encapsulates, + /// since it can occupy multiple rows due to wrapping. + pub fn row_count(&self) -> usize { + let count = self + .wrapped_line + .as_ref() + .map(|line| line.wrap_boundaries().len()); + count.unwrap_or_default() + 1 + } - /// Returns true if the line contains a given position (e.g. for finding the line containing the caret). - /// If `includes_end` is true, the end of the line is treated as inclusive instead of exclusive. - pub fn contains_position(&self, pos: usize, include_end: bool) -> bool { - if self.text_range.is_empty() { - return pos == self.text_range.start; - } + /// Returns true if the line contains a given position (e.g. for finding the line containing the caret). + /// If `includes_end` is true, the end of the line is treated as inclusive instead of exclusive. + pub fn contains_position(&self, pos: usize, include_end: bool) -> bool { + if self.text_range.is_empty() { + return pos == self.text_range.start; + } - if include_end { - (self.text_range.start..=self.text_range.end).contains(&pos) - } else { - self.text_range.contains(&pos) - } - } + if include_end { + (self.text_range.start..=self.text_range.end).contains(&pos) + } else { + self.text_range.contains(&pos) + } + } - /// Returns the index of the character within this segment that is closest - /// to the provided screen space position. - /// The character index returned is in absolute space; it is not relative to this segment. - pub fn character_index_at_point(&self, point: Point, line_height: Pixels) -> usize { - let mut offset = 0usize; - if !self.text_range.is_empty() - && let Some(wrapped) = &self.wrapped_line - { - offset = wrapped - .closest_index_for_position(point, line_height) - .unwrap_or_else(|closest| closest) - .min(wrapped.text.len()); - } - self.text_range.start + offset - } + /// Returns the index of the character within this segment that is closest + /// to the provided screen space position. + /// The character index returned is in absolute space; it is not relative to this segment. + pub fn character_index_at_point(&self, point: Point, line_height: Pixels) -> usize { + let mut offset = 0usize; + if !self.text_range.is_empty() + && let Some(wrapped) = &self.wrapped_line + { + offset = wrapped + .closest_index_for_position(point, line_height) + .unwrap_or_else(|closest| closest) + .min(wrapped.text.len()); + } + self.text_range.start + offset + } - /// Returns the screen space position of the character at the position provided. - /// The position of the character must be absolute to the string this segment - /// partially represents, it is converted to a relative offset internally. - pub fn position_for_index( - &self, - character_index: usize, - line_height: Pixels, - ) -> Option> { - let wrapped = self.wrapped_line.as_ref()?; - // the position in the text relative to this line segment - let relative_text_pos = character_index - .saturating_sub(self.text_range.start) - .min(wrapped.text.len()); - // the screen position of the character in this line segment - wrapped.position_for_index(relative_text_pos, line_height) - } + /// Returns the screen space position of the character at the position provided. + /// The position of the character must be absolute to the string this segment + /// partially represents, it is converted to a relative offset internally. + pub fn position_for_index( + &self, + character_index: usize, + line_height: Pixels, + ) -> Option> { + let wrapped = self.wrapped_line.as_ref()?; + // the position in the text relative to this line segment + let relative_text_pos = character_index + .saturating_sub(self.text_range.start) + .min(wrapped.text.len()); + // the screen position of the character in this line segment + wrapped.position_for_index(relative_text_pos, line_height) + } } diff --git a/crates/gpui_elements/src/editable_text/state.rs b/crates/gpui_elements/src/editable_text/state.rs index c0b1838cfb..832abf5248 100644 --- a/crates/gpui_elements/src/editable_text/state.rs +++ b/crates/gpui_elements/src/editable_text/state.rs @@ -1,13 +1,13 @@ use crate::editable_text::{ - StringStorage, TextBoundary, UnicodeTextStorage, - actions::EditableTextActionHandler, - caret::CaretNotify, - history::EditableTextHistory, - layout::{EditableTextLayoutResult, TextLineSegment}, + StringStorage, TextBoundary, UnicodeTextStorage, + actions::EditableTextActionHandler, + caret::CaretNotify, + history::EditableTextHistory, + layout::{EditableTextLayoutResult, TextLineSegment}, }; use gpui::{ - App, Bounds, ClipboardItem, Context, ElementId, Entity, EntityInputHandler, EventEmitter, - FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection, Window, point, + App, Bounds, ClipboardItem, Context, ElementId, Entity, EntityInputHandler, EventEmitter, + FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection, Window, point, }; use std::{borrow::Cow, ops::Range}; @@ -19,76 +19,76 @@ const CARET_PIXELS_EPSILON: Pixels = gpui::px(4.); /// Diverges from the semantics/expectations of the Range type (since `Range` is incoherent if start > end). #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CaretSelection { - start: usize, - end: usize, + start: usize, + end: usize, } impl From for CaretSelection { - fn from(value: usize) -> Self { - Self { - start: value, - end: value, - } - } + fn from(value: usize) -> Self { + Self { + start: value, + end: value, + } + } } impl From<(usize, usize)> for CaretSelection { - fn from((start, end): (usize, usize)) -> Self { - Self { start, end } - } + fn from((start, end): (usize, usize)) -> Self { + Self { start, end } + } } impl From> for CaretSelection { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } + fn from(value: Range) -> Self { + Self { + start: value.start, + end: value.end, + } + } } impl Into<(usize, usize)> for CaretSelection { - fn into(self) -> (usize, usize) { - (self.start, self.end) - } + fn into(self) -> (usize, usize) { + (self.start, self.end) + } } impl CaretSelection { - fn is_empty(&self) -> bool { - self.start == self.end - } + fn is_empty(&self) -> bool { + self.start == self.end + } - fn range(&self) -> Range { - self.start.min(self.end)..self.start.max(self.end) - } + fn range(&self) -> Range { + self.start.min(self.end)..self.start.max(self.end) + } } /// Internal state for EditableText elements. pub struct EditableTextState { - /// The storage medium backing this element-state. Hypothetically supports both - /// std String and other crates (e.g. long document text). - storage: Box, + /// The storage medium backing this element-state. Hypothetically supports both + /// std String and other crates (e.g. long document text). + storage: Box, - /// The utf-8 character range that is currently selected by the user. - /// Valid both when start < end and start > end (which dictates the direction of the selection). - /// Empty when start==end. The start of this range is always the current position of the caret (input cursor). - /// This means it breaks the semantics/expectations of the Range type. - /// - /// NOTE: because each input has its own selection state, its trivial for users to have - /// multiple selections active across multiple inputs at the same time. - /// This could be considered undesirable behavior, and could prompt the question of - /// whether there should be a mechanism to clear selection when focus is lost. - selected_range: CaretSelection, + /// The utf-8 character range that is currently selected by the user. + /// Valid both when start < end and start > end (which dictates the direction of the selection). + /// Empty when start==end. The start of this range is always the current position of the caret (input cursor). + /// This means it breaks the semantics/expectations of the Range type. + /// + /// NOTE: because each input has its own selection state, its trivial for users to have + /// multiple selections active across multiple inputs at the same time. + /// This could be considered undesirable behavior, and could prompt the question of + /// whether there should be a mechanism to clear selection when focus is lost. + selected_range: CaretSelection, - /// The utf-8 character range of `storage` which is being composed by IME - marked_range: Option>, + /// The utf-8 character range of `storage` which is being composed by IME + marked_range: Option>, - /// True while the user is in the act of highlighting a section of the text (e.g. during mouse pressed & dragging). - is_selecting: bool, - /// The last ui location relative to the element that the user clicked. Used to filter when a user clicks multiple times in the same area. - last_click_position: Option>, - /// The number of times the user has clicked `last_click_position`. Used to determine which click behavior to trigger, depending on single, double, or triple clicks. - click_count: usize, + /// True while the user is in the act of highlighting a section of the text (e.g. during mouse pressed & dragging). + is_selecting: bool, + /// The last ui location relative to the element that the user clicked. Used to filter when a user clicks multiple times in the same area. + last_click_position: Option>, + /// The number of times the user has clicked `last_click_position`. Used to determine which click behavior to trigger, depending on single, double, or triple clicks. + click_count: usize, - focus_handle: FocusHandle, - history: Option, + focus_handle: FocusHandle, + history: Option, - pub(super) layout_data: EditableTextLayoutResult, + pub(super) layout_data: EditableTextLayoutResult, } impl EventEmitter for EditableTextState {} @@ -100,1049 +100,1049 @@ pub struct TextChanged; impl EventEmitter for EditableTextState {} impl Focusable for EditableTextState { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl AsRef for EditableTextState { - fn as_ref(&self) -> &str { - self.as_str() - } + fn as_ref(&self) -> &str { + self.as_str() + } } impl EditableTextState { - /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. - /// If the state does not yet exist, a new one is created using the default [`UnicodeTextStorage`] medium. - pub fn use_keyed(key: impl Into, window: &mut Window, cx: &mut App) -> Entity { - Self::use_keyed_init(key, window, cx, |_, _| StringStorage::default()) - } + /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. + /// If the state does not yet exist, a new one is created using the default [`UnicodeTextStorage`] medium. + pub fn use_keyed(key: impl Into, window: &mut Window, cx: &mut App) -> Entity { + Self::use_keyed_init(key, window, cx, |_, _| StringStorage::default()) + } - /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. - /// If the state does not yet exist, a new one is created calling `init` to create a [`UnicodeTextStorage`] medium. - /// - /// ``` - /// # use gpui::{RenderOnce, Window, App, IntoElement, ElementId}; - /// # use gpui_elements::editable_text::{EditableTextState, StringStorage, editable_text}; - /// pub struct Form; - /// impl RenderOnce for Form { - /// fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - /// let field_a_id = ElementId::from("field_a"); - /// let field_a = EditableTextState::use_keyed_init(field_a_id.clone(), window, cx, - /// |_window, _cx| StringStorage::from("this is some default editable text content")); - /// editable_text(field_a_id).state(field_a.downgrade()) - /// } - /// } - /// ``` - pub fn use_keyed_init( - key: impl Into, - window: &mut Window, - cx: &mut App, - init: F, - ) -> Entity - where - F: 'static + Fn(&mut Window, &mut Context<'_, EditableTextState>) -> StorageType, - StorageType: 'static + UnicodeTextStorage, - { - window.use_keyed_state(key, cx, |window, cx| Self::new(init(window, cx), cx)) - } + /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. + /// If the state does not yet exist, a new one is created calling `init` to create a [`UnicodeTextStorage`] medium. + /// + /// ``` + /// # use gpui::{RenderOnce, Window, App, IntoElement, ElementId}; + /// # use gpui_elements::editable_text::{EditableTextState, StringStorage, editable_text}; + /// pub struct Form; + /// impl RenderOnce for Form { + /// fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + /// let field_a_id = ElementId::from("field_a"); + /// let field_a = EditableTextState::use_keyed_init(field_a_id.clone(), window, cx, + /// |_window, _cx| StringStorage::from("this is some default editable text content")); + /// editable_text(field_a_id).state(field_a.downgrade()) + /// } + /// } + /// ``` + pub fn use_keyed_init( + key: impl Into, + window: &mut Window, + cx: &mut App, + init: F, + ) -> Entity + where + F: 'static + Fn(&mut Window, &mut Context<'_, EditableTextState>) -> StorageType, + StorageType: 'static + UnicodeTextStorage, + { + window.use_keyed_state(key, cx, |window, cx| Self::new(init(window, cx), cx)) + } - /// Creates a new EditableText state with a given storage medium. - /// - /// Does not intrinsicly handle the state being attached to an element - /// over multiple frames (e.g. via [`RenderOnce`]). Use [`use_keyed`] or [`use_keyed_init`] for that. - /// - /// Expected to be called via [`AppContext::new`] such as: - /// ``` - /// # use gpui::{AppContext, Window, App, Entity}; - /// # use gpui_elements::editable_text::{StringStorage, EditableTextState}; - /// # fn new(_window: &mut Window, cx: &mut App) -> Entity { - /// cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)) - /// # } - /// ``` - pub fn new(storage: impl UnicodeTextStorage + 'static, cx: &mut Context) -> Self { - Self { - storage: Box::new(storage), + /// Creates a new EditableText state with a given storage medium. + /// + /// Does not intrinsicly handle the state being attached to an element + /// over multiple frames (e.g. via [`RenderOnce`]). Use [`use_keyed`] or [`use_keyed_init`] for that. + /// + /// Expected to be called via [`AppContext::new`] such as: + /// ``` + /// # use gpui::{AppContext, Window, App, Entity}; + /// # use gpui_elements::editable_text::{StringStorage, EditableTextState}; + /// # fn new(_window: &mut Window, cx: &mut App) -> Entity { + /// cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)) + /// # } + /// ``` + pub fn new(storage: impl UnicodeTextStorage + 'static, cx: &mut Context) -> Self { + Self { + storage: Box::new(storage), - selected_range: 0.into(), - marked_range: None, + selected_range: 0.into(), + marked_range: None, - is_selecting: false, - last_click_position: None, - click_count: 0, + is_selecting: false, + last_click_position: None, + click_count: 0, - focus_handle: cx.focus_handle(), - // TODO: what is the best way to give users access to configure this via element - history: Some(EditableTextHistory::default()), + focus_handle: cx.focus_handle(), + // TODO: what is the best way to give users access to configure this via element + history: Some(EditableTextHistory::default()), - layout_data: EditableTextLayoutResult::default(), - } - } + layout_data: EditableTextLayoutResult::default(), + } + } - /// Returns the current contents of [`storage`] as a string slice. - pub fn as_str(&self) -> &str { - self.storage.content_utf8() - } + /// Returns the current contents of [`storage`] as a string slice. + pub fn as_str(&self) -> &str { + self.storage.content_utf8() + } - pub fn version(&self) -> u16 { - self.storage.version() - } + pub fn version(&self) -> u16 { + self.storage.version() + } - /// Replaces the contents of the stored text with the provided string slice. - pub fn emplace(&mut self, content: &str, cx: &mut Context) { - let len = self.storage.content_utf8().len(); - self.replace_text(0..len, content); - self.emit_text_changed(cx); - cx.notify(); - } + /// Replaces the contents of the stored text with the provided string slice. + pub fn emplace(&mut self, content: &str, cx: &mut Context) { + let len = self.storage.content_utf8().len(); + self.replace_text(0..len, content); + self.emit_text_changed(cx); + cx.notify(); + } - /// Returns the utf-8 character range that is currently selected within the current state of the text. - /// Internally converts the stored direction-aware range into a canonical range. - pub(super) fn selected_range(&self) -> Range { - self.selected_range.range() - } + /// Returns the utf-8 character range that is currently selected within the current state of the text. + /// Internally converts the stored direction-aware range into a canonical range. + pub(super) fn selected_range(&self) -> Range { + self.selected_range.range() + } - pub(super) fn selection_direction(&self) -> Option { - match self.selected_range.start.cmp(&self.selected_range.end) { - std::cmp::Ordering::Less => Some(NavigationDirection::Forward), - std::cmp::Ordering::Equal => None, - std::cmp::Ordering::Greater => Some(NavigationDirection::Back), - } - } + pub(super) fn selection_direction(&self) -> Option { + match self.selected_range.start.cmp(&self.selected_range.end) { + std::cmp::Ordering::Less => Some(NavigationDirection::Forward), + std::cmp::Ordering::Equal => None, + std::cmp::Ordering::Greater => Some(NavigationDirection::Back), + } + } - /// Returns the position of the caret in utf8 character space. - pub(super) fn caret_pos(&self) -> usize { - self.selected_range.start - } + /// Returns the position of the caret in utf8 character space. + pub(super) fn caret_pos(&self) -> usize { + self.selected_range.start + } - /// Returns the IME marked range for character operations. - pub(super) fn marked_range(&self) -> Option> { - self.marked_range.clone() - } + /// Returns the IME marked range for character operations. + pub(super) fn marked_range(&self) -> Option> { + self.marked_range.clone() + } } impl EditableTextState { - /// Validates/sanitizes incoming text according to the rules of the field. - fn validate_incoming_text<'text>( - &self, - _range: &Range, - text_to_insert: &'text str, - ) -> Cow<'text, str> { - // TODO: Apply text sanitization, ideally using externally-sourced implementations. - // example optional/opt-in sanitations include: - // - single-line fields should prune /n & /r - // - maximum utf8 length - // - numbers only - // should also consider validation support, for features such as: - // - total syntax evaluation (e.g. passwords) - // - conforms to regex or math (e.g. ssn, phone number, email, etc) - let mut text_to_insert = Cow::Borrowed(text_to_insert); + /// Validates/sanitizes incoming text according to the rules of the field. + fn validate_incoming_text<'text>( + &self, + _range: &Range, + text_to_insert: &'text str, + ) -> Cow<'text, str> { + // TODO: Apply text sanitization, ideally using externally-sourced implementations. + // example optional/opt-in sanitations include: + // - single-line fields should prune /n & /r + // - maximum utf8 length + // - numbers only + // should also consider validation support, for features such as: + // - total syntax evaluation (e.g. passwords) + // - conforms to regex or math (e.g. ssn, phone number, email, etc) + let mut text_to_insert = Cow::Borrowed(text_to_insert); - if !self.layout_data.supports_multiline { - text_to_insert = Cow::Owned(text_to_insert.replace("\n", "").replace("\r", "")); - } + if !self.layout_data.supports_multiline { + text_to_insert = Cow::Owned(text_to_insert.replace("\n", "").replace("\r", "")); + } - /* A sample implementation of max-length sampled from gpuikit - // Decide the effective new text up front (honouring `max_length`). - // This avoids the "apply, then truncate" path which would leave the caret past the end. - let max_length = None::; - if let Some(cap) = max_length { - let existing_len = self.as_str().len() - (range.end - range.start); - let room = cap.saturating_sub(existing_len); - text_to_insert = &text_to_insert[..text_to_insert.len().min(room)]; - } - */ + /* A sample implementation of max-length sampled from gpuikit + // Decide the effective new text up front (honouring `max_length`). + // This avoids the "apply, then truncate" path which would leave the caret past the end. + let max_length = None::; + if let Some(cap) = max_length { + let existing_len = self.as_str().len() - (range.end - range.start); + let room = cap.saturating_sub(existing_len); + text_to_insert = &text_to_insert[..text_to_insert.len().min(room)]; + } + */ - // for now, this function is no-op - text_to_insert - } + // for now, this function is no-op + text_to_insert + } - /// Internal method to record historical changes and perform text replacement in storage. - /// Selection is moved to the end of the inserted text and ime marked range is cleared. - fn replace_text(&mut self, range: Range, text_to_insert: &str) { - let end_pos = range.start + text_to_insert.len(); - self.record_history(range.clone(), text_to_insert.len()); - self.storage.replace_range(range, text_to_insert); - self.selected_range = end_pos.into(); - self.marked_range = None; - } + /// Internal method to record historical changes and perform text replacement in storage. + /// Selection is moved to the end of the inserted text and ime marked range is cleared. + fn replace_text(&mut self, range: Range, text_to_insert: &str) { + let end_pos = range.start + text_to_insert.len(); + self.record_history(range.clone(), text_to_insert.len()); + self.storage.replace_range(range, text_to_insert); + self.selected_range = end_pos.into(); + self.marked_range = None; + } - fn emit_text_changed(&self, cx: &mut Context) { - cx.emit(TextChanged); - } + fn emit_text_changed(&self, cx: &mut Context) { + cx.emit(TextChanged); + } } /// Parameters used to find a desired TextLineSegment from layout data. enum TextSegmentQuery { - /// Find the line containing a character at a position (relative to the document, not a given line). - CharacterPosition(usize), - /// Find the line that most closely contains the provided screen position. - ScreenPosition { - point: Point, - line_height: Pixels, - }, - /// Find the line containing a visual row of text, which may be wrapped - /// (relative to the document, not a given line). - Row(usize), + /// Find the line containing a character at a position (relative to the document, not a given line). + CharacterPosition(usize), + /// Find the line that most closely contains the provided screen position. + ScreenPosition { + point: Point, + line_height: Pixels, + }, + /// Find the line containing a visual row of text, which may be wrapped + /// (relative to the document, not a given line). + Row(usize), } // Screen space (text layout engine output) & String space transformers impl EditableTextState { - /// Attempts to find a text segment based on the provided query parameters. - /// If found, the returned tuple is the segment & the number of preceding visual rows. - /// If not found, the resulting "err" is the total number of visual rows of all line segments. - fn find_segment(&self, query: TextSegmentQuery) -> Result<(&TextLineSegment, usize), usize> { - let mut row_count = 0; - for segment in &self.layout_data.lines { - let found = match &query { - TextSegmentQuery::CharacterPosition(pos) => segment.contains_position(*pos, false), - TextSegmentQuery::ScreenPosition { point, line_height } => { - let segment_start_pos_y = segment.pos_y * *line_height; - let segment_height = *line_height * segment.row_count() as f32; - point.y >= segment_start_pos_y && point.y < segment_start_pos_y + segment_height - } - TextSegmentQuery::Row(row_index) => *row_index < row_count + segment.row_count(), - }; - if found { - return Ok((segment, row_count)); - } - row_count += segment.row_count(); - } - Err(row_count) - } + /// Attempts to find a text segment based on the provided query parameters. + /// If found, the returned tuple is the segment & the number of preceding visual rows. + /// If not found, the resulting "err" is the total number of visual rows of all line segments. + fn find_segment(&self, query: TextSegmentQuery) -> Result<(&TextLineSegment, usize), usize> { + let mut row_count = 0; + for segment in &self.layout_data.lines { + let found = match &query { + TextSegmentQuery::CharacterPosition(pos) => segment.contains_position(*pos, false), + TextSegmentQuery::ScreenPosition { point, line_height } => { + let segment_start_pos_y = segment.pos_y * *line_height; + let segment_height = *line_height * segment.row_count() as f32; + point.y >= segment_start_pos_y && point.y < segment_start_pos_y + segment_height + } + TextSegmentQuery::Row(row_index) => *row_index < row_count + segment.row_count(), + }; + if found { + return Ok((segment, row_count)); + } + row_count += segment.row_count(); + } + Err(row_count) + } - /// Returns the utf-8 character position of the start of the line that contains the provided pixel-point. - fn index_for_pixel_point(&self, point: Point, line_height: Pixels) -> usize { - let storage_len_utf8 = self.as_str().len(); - if storage_len_utf8 == 0 { - return 0; - } + /// Returns the utf-8 character position of the start of the line that contains the provided pixel-point. + fn index_for_pixel_point(&self, point: Point, line_height: Pixels) -> usize { + let storage_len_utf8 = self.as_str().len(); + if storage_len_utf8 == 0 { + return 0; + } - let segment = self.find_segment(TextSegmentQuery::ScreenPosition { point, line_height }); - let Ok((segment, _preceding_row_count)) = segment else { - return storage_len_utf8; - }; + let segment = self.find_segment(TextSegmentQuery::ScreenPosition { point, line_height }); + let Ok((segment, _preceding_row_count)) = segment else { + return storage_len_utf8; + }; - // the screen position of the caret relative to the text segment - let relative_point = point - gpui::point(Pixels::ZERO, segment.pos_y * line_height); + // the screen position of the caret relative to the text segment + let relative_point = point - gpui::point(Pixels::ZERO, segment.pos_y * line_height); - return segment.character_index_at_point(relative_point, line_height); - } + return segment.character_index_at_point(relative_point, line_height); + } - fn find_position_in_vertical_direction( - &self, - direction: i32, - line_height: Pixels, - ) -> Option { - let (caret_line_index, caret_point) = self.line_index_and_point_at_caret(line_height); - let target_line_index = caret_line_index.saturating_add_signed(direction as isize); + fn find_position_in_vertical_direction( + &self, + direction: i32, + line_height: Pixels, + ) -> Option { + let (caret_line_index, caret_point) = self.line_index_and_point_at_caret(line_height); + let target_line_index = caret_line_index.saturating_add_signed(direction as isize); - let segment = self.find_segment(TextSegmentQuery::Row(target_line_index)); - let Ok((segment, preceding_row_count)) = segment else { - return (direction > 0).then(|| self.as_str().len()); - }; + let segment = self.find_segment(TextSegmentQuery::Row(target_line_index)); + let Ok((segment, preceding_row_count)) = segment else { + return (direction > 0).then(|| self.as_str().len()); + }; - // calculate the screen space position of the row we are navigating to, - // relative to the y-position of the segment. - let row_index = (target_line_index - preceding_row_count) as f32; - let relative_point = gpui::point(caret_point.x, row_index * line_height); + // calculate the screen space position of the row we are navigating to, + // relative to the y-position of the segment. + let row_index = (target_line_index - preceding_row_count) as f32; + let relative_point = gpui::point(caret_point.x, row_index * line_height); - Some(segment.character_index_at_point(relative_point, line_height)) - } + Some(segment.character_index_at_point(relative_point, line_height)) + } - fn line_index_and_point_at_caret(&self, line_height: Pixels) -> (usize, Point) { - if self.layout_data.lines.is_empty() { - return (0, Point::default()); - } + fn line_index_and_point_at_caret(&self, line_height: Pixels) -> (usize, Point) { + if self.layout_data.lines.is_empty() { + return (0, Point::default()); + } - let caret_pos = self.caret_pos(); - let segment = self.find_segment(TextSegmentQuery::CharacterPosition(caret_pos)); - let (segment, preceding_row_count) = match segment { - Ok(segment) => segment, - Err(total_row_count) => return (total_row_count.saturating_sub(1), Point::default()), - }; + let caret_pos = self.caret_pos(); + let segment = self.find_segment(TextSegmentQuery::CharacterPosition(caret_pos)); + let (segment, preceding_row_count) = match segment { + Ok(segment) => segment, + Err(total_row_count) => return (total_row_count.saturating_sub(1), Point::default()), + }; - // Find the screen position relative to this segment where the caret is at. - let relative_point = segment.position_for_index(caret_pos, line_height); + // Find the screen position relative to this segment where the caret is at. + let relative_point = segment.position_for_index(caret_pos, line_height); - let point = relative_point.unwrap_or_default(); - // the visual row offset from the start of the segment - let row_offset = (point.y / line_height).floor() as usize; - (preceding_row_count + row_offset, point) - } + let point = relative_point.unwrap_or_default(); + // the visual row offset from the start of the segment + let row_offset = (point.y / line_height).floor() as usize; + (preceding_row_count + row_offset, point) + } - fn find_point_for_character_position(&self, character_pos: usize) -> Point { - let segment = self.find_segment(TextSegmentQuery::CharacterPosition(character_pos)); - let Ok((segment, preceding_row_count)) = segment else { - return Point::default(); - }; - let line_height = self.layout_data.line_height; + fn find_point_for_character_position(&self, character_pos: usize) -> Point { + let segment = self.find_segment(TextSegmentQuery::CharacterPosition(character_pos)); + let Ok((segment, preceding_row_count)) = segment else { + return Point::default(); + }; + let line_height = self.layout_data.line_height; - // Find the screen position relative to this segment where the caret is at. - let relative_point = segment.position_for_index(character_pos, line_height); + // Find the screen position relative to this segment where the caret is at. + let relative_point = segment.position_for_index(character_pos, line_height); - let line_origin = point(Pixels::ZERO, preceding_row_count as f32 * line_height); - return line_origin + relative_point.unwrap_or_default(); - } + let line_origin = point(Pixels::ZERO, preceding_row_count as f32 * line_height); + return line_origin + relative_point.unwrap_or_default(); + } } // Internal user action / logical processors impl EditableTextState { - fn scroll_to_caret(&mut self) { - if self.layout_data.scroll_bounds.is_empty() { - return; - } - let Some(content_size) = self.layout_data.state.size else { - return; - }; + fn scroll_to_caret(&mut self) { + if self.layout_data.scroll_bounds.is_empty() { + return; + } + let Some(content_size) = self.layout_data.state.size else { + return; + }; - // point will be relative to content_size, and may or may not be within the current scroll_bounds - let point = self.find_point_for_character_position(self.caret_pos()); + // point will be relative to content_size, and may or may not be within the current scroll_bounds + let point = self.find_point_for_character_position(self.caret_pos()); - // this scroll_offset diverges from the rest of gpui, as it is stored in the - // positive real number space (interactivity stores it in the negatives) - let mut scroll_offset = Cow::Borrowed(&self.layout_data.scroll_bounds.origin); + // this scroll_offset diverges from the rest of gpui, as it is stored in the + // positive real number space (interactivity stores it in the negatives) + let mut scroll_offset = Cow::Borrowed(&self.layout_data.scroll_bounds.origin); - if self.layout_data.scroll_bounds.contains(&point) { - return; - } + if self.layout_data.scroll_bounds.contains(&point) { + return; + } - // No existing "shift bounds origin so is contained", but that is effectively what this does - if point.x < self.layout_data.scroll_bounds.left() { - scroll_offset.to_mut().x = point.x; - } - if point.y < self.layout_data.scroll_bounds.top() { - scroll_offset.to_mut().y = point.y; - } - let right = self.layout_data.scroll_bounds.right(); - if point.x > right { - scroll_offset.to_mut().x += point.x - right; - } - let bottom = self.layout_data.scroll_bounds.bottom(); - let point_bottom = point.y + self.layout_data.line_height; - if point_bottom > bottom { - let delta = point_bottom - bottom; - scroll_offset.to_mut().y += delta; - } + // No existing "shift bounds origin so is contained", but that is effectively what this does + if point.x < self.layout_data.scroll_bounds.left() { + scroll_offset.to_mut().x = point.x; + } + if point.y < self.layout_data.scroll_bounds.top() { + scroll_offset.to_mut().y = point.y; + } + let right = self.layout_data.scroll_bounds.right(); + if point.x > right { + scroll_offset.to_mut().x += point.x - right; + } + let bottom = self.layout_data.scroll_bounds.bottom(); + let point_bottom = point.y + self.layout_data.line_height; + if point_bottom > bottom { + let delta = point_bottom - bottom; + scroll_offset.to_mut().y += delta; + } - if let Cow::Owned(mut offset) = scroll_offset { - offset.x = offset.x.clamp(Pixels::ZERO, content_size.width); - offset.y = offset.y.clamp(Pixels::ZERO, content_size.height); - println!("shift to {offset:?}"); - self.layout_data.next_scroll_offset = Some(offset); - } - } + if let Cow::Owned(mut offset) = scroll_offset { + offset.x = offset.x.clamp(Pixels::ZERO, content_size.width); + offset.y = offset.y.clamp(Pixels::ZERO, content_size.height); + println!("shift to {offset:?}"); + self.layout_data.next_scroll_offset = Some(offset); + } + } - /// Moves the caret to the provided position. - /// - /// Will cause the current scroll position/offset to update on the next frame, - /// if the line the carent is on is out of view. - pub fn move_to(&mut self, caret_pos: usize, cx: &mut Context) { - cx.emit(CaretNotify::PauseBlinking); - let caret_pos = caret_pos.min(self.storage.content_utf8().len()); - self.selected_range = caret_pos.into(); - self.scroll_to_caret(); - cx.notify(); - } + /// Moves the caret to the provided position. + /// + /// Will cause the current scroll position/offset to update on the next frame, + /// if the line the carent is on is out of view. + pub fn move_to(&mut self, caret_pos: usize, cx: &mut Context) { + cx.emit(CaretNotify::PauseBlinking); + let caret_pos = caret_pos.min(self.storage.content_utf8().len()); + self.selected_range = caret_pos.into(); + self.scroll_to_caret(); + cx.notify(); + } - /// Changes the current selection to extend to the provided position. - /// - /// Will cause the current scroll position/offset to update on the next frame, - /// if the line the carent is on is out of view. - pub fn select_to(&mut self, caret_pos: usize, cx: &mut Context) { - cx.emit(CaretNotify::PauseBlinking); - let caret_pos = caret_pos.min(self.as_str().len()); - self.selected_range.start = caret_pos; - self.scroll_to_caret(); - cx.notify(); - } + /// Changes the current selection to extend to the provided position. + /// + /// Will cause the current scroll position/offset to update on the next frame, + /// if the line the carent is on is out of view. + pub fn select_to(&mut self, caret_pos: usize, cx: &mut Context) { + cx.emit(CaretNotify::PauseBlinking); + let caret_pos = caret_pos.min(self.as_str().len()); + self.selected_range.start = caret_pos; + self.scroll_to_caret(); + cx.notify(); + } - /// Removes a chunk of text at the cursor/selection. - /// No-op if the element is currently not accepting input. - /// - /// If there is a selection of multiple characters, the slice of text represented - /// by range is replaced with an empty string. - /// If there is no selection, `direction` and `boundary` are used to determine the slice of text to remove. - /// - /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the caret. - /// - /// [`NavigationDirection::Forward`] represents scanning later in the text string from the caret. - /// - /// [`TextBoundary`] describes how far to jump from the caret. - pub fn delete_linear( - &mut self, - direction: NavigationDirection, - boundary: TextBoundary, - cx: &mut Context, - ) { - if !self.layout_data.accepts_input { - return; - } + /// Removes a chunk of text at the cursor/selection. + /// No-op if the element is currently not accepting input. + /// + /// If there is a selection of multiple characters, the slice of text represented + /// by range is replaced with an empty string. + /// If there is no selection, `direction` and `boundary` are used to determine the slice of text to remove. + /// + /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the caret. + /// + /// [`NavigationDirection::Forward`] represents scanning later in the text string from the caret. + /// + /// [`TextBoundary`] describes how far to jump from the caret. + pub fn delete_linear( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + cx: &mut Context, + ) { + if !self.layout_data.accepts_input { + return; + } - let range = self.selected_range(); - let range = match range.is_empty() { - false => range, - true => self - .storage - .range_from_caret(self.caret_pos(), direction, boundary), - }; - let storage_len_utf8 = self.storage.content_utf8().len(); - let start = range.start.min(storage_len_utf8); - let end = range.end.max(start).min(storage_len_utf8); + let range = self.selected_range(); + let range = match range.is_empty() { + false => range, + true => self + .storage + .range_from_caret(self.caret_pos(), direction, boundary), + }; + let storage_len_utf8 = self.storage.content_utf8().len(); + let start = range.start.min(storage_len_utf8); + let end = range.end.max(start).min(storage_len_utf8); - self.replace_text(start..end, ""); + self.replace_text(start..end, ""); - self.emit_text_changed(cx); - cx.notify(); - } + self.emit_text_changed(cx); + cx.notify(); + } - /// Moves the caret somewhere relative to its current location, according to `direction` and `boundary`. - /// - /// If there is currently a selection, the cursor will jump to the start/end of that selection based on `direction`. - /// - /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret. - /// - /// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret. - /// - /// [`TextBoundary`] describes how far to jump from the current caret - pub fn nav_linear( - &mut self, - direction: NavigationDirection, - boundary: TextBoundary, - cx: &mut Context, - ) { - let caret_pos = match self.selected_range.is_empty() { - false => match direction { - NavigationDirection::Back => self.selected_range.start, - NavigationDirection::Forward => self.selected_range.end, - }, - true => self - .storage - .offset_from_caret(self.caret_pos(), direction, boundary), - }; - self.move_to(caret_pos, cx); - } + /// Moves the caret somewhere relative to its current location, according to `direction` and `boundary`. + /// + /// If there is currently a selection, the cursor will jump to the start/end of that selection based on `direction`. + /// + /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret. + /// + /// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret. + /// + /// [`TextBoundary`] describes how far to jump from the current caret + pub fn nav_linear( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + cx: &mut Context, + ) { + let caret_pos = match self.selected_range.is_empty() { + false => match direction { + NavigationDirection::Back => self.selected_range.start, + NavigationDirection::Forward => self.selected_range.end, + }, + true => self + .storage + .offset_from_caret(self.caret_pos(), direction, boundary), + }; + self.move_to(caret_pos, cx); + } - /// Sets the current selection to be the entire text in the storage medium - pub fn select_document(&mut self, cx: &mut Context) { - self.selected_range = (0, self.storage.content_utf8().len()).into(); - cx.notify(); - } + /// Sets the current selection to be the entire text in the storage medium + pub fn select_document(&mut self, cx: &mut Context) { + self.selected_range = (0, self.storage.content_utf8().len()).into(); + cx.notify(); + } - /// Extends the current selection to include some amount of textrelative the current - /// location of the caret, according to `direction` and `boundary`. - /// - /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret. - /// - /// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret. - /// - /// [`TextBoundary`] describes how far to jump from the current caret - pub fn select_linear( - &mut self, - direction: NavigationDirection, - boundary: TextBoundary, - cx: &mut Context, - ) { - let caret_pos = self - .storage - .offset_from_caret(self.caret_pos(), direction, boundary); - self.select_to(caret_pos, cx); - } + /// Extends the current selection to include some amount of textrelative the current + /// location of the caret, according to `direction` and `boundary`. + /// + /// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret. + /// + /// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret. + /// + /// [`TextBoundary`] describes how far to jump from the current caret + pub fn select_linear( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + cx: &mut Context, + ) { + let caret_pos = self + .storage + .offset_from_caret(self.caret_pos(), direction, boundary); + self.select_to(caret_pos, cx); + } - /// Updates the mouse-click tracker so we can detect when a mouse click results in different actions. - fn apply_click(&mut self, click_count: usize, text_position: Point) { - let should_continue_click = - click_count > 1 && self.is_position_nearly_at_previous_click(text_position); - self.click_count = if should_continue_click { - click_count - } else { - 1 - }; - self.last_click_position = Some(text_position); - } + /// Updates the mouse-click tracker so we can detect when a mouse click results in different actions. + fn apply_click(&mut self, click_count: usize, text_position: Point) { + let should_continue_click = + click_count > 1 && self.is_position_nearly_at_previous_click(text_position); + self.click_count = if should_continue_click { + click_count + } else { + 1 + }; + self.last_click_position = Some(text_position); + } - fn is_position_nearly_at_previous_click(&self, point: Point) -> bool { - match self.last_click_position { - None => false, - Some(previous_pos) => point.is_nearly_eq(&previous_pos, CARET_PIXELS_EPSILON), - } - } + fn is_position_nearly_at_previous_click(&self, point: Point) -> bool { + match self.last_click_position { + None => false, + Some(previous_pos) => point.is_nearly_eq(&previous_pos, CARET_PIXELS_EPSILON), + } + } - fn select_word_at(&mut self, caret_pos: usize, cx: &mut Context) { - self.selected_range = self.storage.word_range_at(caret_pos).into(); - cx.notify(); - } + fn select_word_at(&mut self, caret_pos: usize, cx: &mut Context) { + self.selected_range = self.storage.word_range_at(caret_pos).into(); + cx.notify(); + } - fn select_line_at(&mut self, caret_pos: usize, cx: &mut Context) { - use NavigationDirection::*; - use TextBoundary::*; + fn select_line_at(&mut self, caret_pos: usize, cx: &mut Context) { + use NavigationDirection::*; + use TextBoundary::*; - let line_start = self.storage.offset_from_caret(caret_pos, Back, Line); - let line_end = self.storage.offset_from_caret(caret_pos, Forward, Line); - let line_end_with_newline = if line_end < self.storage.content_utf8().len() { - line_end + 1 - } else { - line_end - }; - self.selected_range = (line_start, line_end_with_newline).into(); - cx.notify(); - } + let line_start = self.storage.offset_from_caret(caret_pos, Back, Line); + let line_end = self.storage.offset_from_caret(caret_pos, Forward, Line); + let line_end_with_newline = if line_end < self.storage.content_utf8().len() { + line_end + 1 + } else { + line_end + }; + self.selected_range = (line_start, line_end_with_newline).into(); + cx.notify(); + } } // History management impl EditableTextState { - /// Returns the history log of the element, which is the data that supports undo/redo operations. - pub fn history(&self) -> Option<&EditableTextHistory> { - self.history.as_ref() - } + /// Returns the history log of the element, which is the data that supports undo/redo operations. + pub fn history(&self) -> Option<&EditableTextHistory> { + self.history.as_ref() + } - fn record_history(&mut self, range: Range, new_text_len: usize) { - // Don't record during IME composition - if self.marked_range.is_some() { - return; - } + fn record_history(&mut self, range: Range, new_text_len: usize) { + // Don't record during IME composition + if self.marked_range.is_some() { + return; + } - let Some(history) = &mut self.history else { - return; - }; + let Some(history) = &mut self.history else { + return; + }; - // Capture the text that will be replaced - let old_text = &self.storage.content_utf8()[range.clone()]; - history.record(range, old_text, new_text_len, self.selected_range.into()); - } + // Capture the text that will be replaced + let old_text = &self.storage.content_utf8()[range.clone()]; + history.record(range, old_text, new_text_len, self.selected_range.into()); + } - fn apply_from_history(&mut self, src: HistoryKind, dst: HistoryKind, cx: &mut Context) { - let Some(history) = &mut self.history else { - return; - }; - let Some(entry) = history.take(src) else { - return; - }; + fn apply_from_history(&mut self, src: HistoryKind, dst: HistoryKind, cx: &mut Context) { + let Some(history) = &mut self.history else { + return; + }; + let Some(entry) = history.take(src) else { + return; + }; - let range = entry.char_range(self.storage.content_utf8().len()); - // Snapshot the sub-slice that is being replaced - let removed_text = self.storage.content_utf8()[range.clone()].to_string(); + let range = entry.char_range(self.storage.content_utf8().len()); + // Snapshot the sub-slice that is being replaced + let removed_text = self.storage.content_utf8()[range.clone()].to_string(); - // Replace the slice with the history value - self.storage.replace_range(range, &entry.old_text); - self.selected_range = entry.selected_range.into(); + // Replace the slice with the history value + self.storage.replace_range(range, &entry.old_text); + self.selected_range = entry.selected_range.into(); - // Push the entry onto the redo stack so the undo can be undone - history.push(dst, entry.as_inverted(removed_text)); + // Push the entry onto the redo stack so the undo can be undone + history.push(dst, entry.as_inverted(removed_text)); - self.scroll_to_caret(); - cx.notify(); - } + self.scroll_to_caret(); + cx.notify(); + } } impl EditableTextState { - fn ime_resolve_range(&self, range_utf16: Option>) -> Range { - // Use a series of fallbacks to pick the range to operate on. - // Fallback order: IME provided range, active IME marked range, selection - let range = range_utf16.map(|range_utf16| self.storage.utf_range_16to8(&range_utf16)); - let range = range.or_else(|| self.marked_range.clone()); - let range = range.unwrap_or_else(|| self.selected_range()); + fn ime_resolve_range(&self, range_utf16: Option>) -> Range { + // Use a series of fallbacks to pick the range to operate on. + // Fallback order: IME provided range, active IME marked range, selection + let range = range_utf16.map(|range_utf16| self.storage.utf_range_16to8(&range_utf16)); + let range = range.or_else(|| self.marked_range.clone()); + let range = range.unwrap_or_else(|| self.selected_range()); - let storage_len_utf8 = self.as_str().len(); - range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8) - } + let storage_len_utf8 = self.as_str().len(); + range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8) + } - fn ime_mark_text_in_range(&mut self, range: &Range, text_len: usize) { - self.marked_range = match text_len { - 0 => None, - _ => Some(range.start..range.start + text_len), - }; - } + fn ime_mark_text_in_range(&mut self, range: &Range, text_len: usize) { + self.marked_range = match text_len { + 0 => None, + _ => Some(range.start..range.start + text_len), + }; + } - fn ime_mark_selected_range( - &mut self, - range_overwritten: &Range, - new_selected_range_utf16: &Option>, - text_len: usize, - ) { - // NOTE: Differs from yororen-ui - // https://github.com/MeowLynxSea/yororen-ui/blob/346502ac654b77fdaff3be2d7444fca8783acfc9/crates/yororen-ui-core/src/headless/text_input_core.rs#L359-L371 - self.selected_range = { - let new_range = new_selected_range_utf16.as_ref(); - let new_range = new_range.map(|range_utf16| self.storage.utf_range_16to8(range_utf16)); - let new_range = new_range.map(|new_range| { - new_range.start + range_overwritten.start..new_range.end + range_overwritten.start - }); - let new_range = new_range.unwrap_or_else(|| { - range_overwritten.start + text_len..range_overwritten.start + text_len - }); - new_range.into() - }; - } + fn ime_mark_selected_range( + &mut self, + range_overwritten: &Range, + new_selected_range_utf16: &Option>, + text_len: usize, + ) { + // NOTE: Differs from yororen-ui + // https://github.com/MeowLynxSea/yororen-ui/blob/346502ac654b77fdaff3be2d7444fca8783acfc9/crates/yororen-ui-core/src/headless/text_input_core.rs#L359-L371 + self.selected_range = { + let new_range = new_selected_range_utf16.as_ref(); + let new_range = new_range.map(|range_utf16| self.storage.utf_range_16to8(range_utf16)); + let new_range = new_range.map(|new_range| { + new_range.start + range_overwritten.start..new_range.end + range_overwritten.start + }); + let new_range = new_range.unwrap_or_else(|| { + range_overwritten.start + text_len..range_overwritten.start + text_len + }); + new_range.into() + }; + } } // IME handler impl EntityInputHandler for EditableTextState { - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let range = self.storage.utf_range_16to8(&range_utf16); - let storage_len_utf8 = self.storage.content_utf8().len(); - let clamped_range = range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8); - adjusted_range.replace(self.storage.utf_range_8to16(&clamped_range)); - Some(self.storage.content_utf8()[clamped_range].to_string()) - } + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let range = self.storage.utf_range_16to8(&range_utf16); + let storage_len_utf8 = self.storage.content_utf8().len(); + let clamped_range = range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8); + adjusted_range.replace(self.storage.utf_range_8to16(&clamped_range)); + Some(self.storage.content_utf8()[clamped_range].to_string()) + } - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let selection_range = self.selected_range(); - let direction = self.selection_direction(); - Some(UTF16Selection { - range: self.storage.utf_range_8to16(&selection_range), - reversed: direction == Some(NavigationDirection::Back), - }) - } + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let selection_range = self.selected_range(); + let direction = self.selection_direction(); + Some(UTF16Selection { + range: self.storage.utf_range_8to16(&selection_range), + reversed: direction == Some(NavigationDirection::Back), + }) + } - fn marked_text_range( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - self.marked_range - .as_ref() - .map(|range| self.storage.utf_range_8to16(range)) - } + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + self.marked_range + .as_ref() + .map(|range| self.storage.utf_range_8to16(range)) + } - fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { - self.marked_range = None; - } + 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>, - text_to_insert: &str, - _window: &mut Window, - cx: &mut Context, - ) { - let range_utf8 = self.ime_resolve_range(range_utf16); - let text_to_insert = self.validate_incoming_text(&range_utf8, text_to_insert); - self.replace_text(range_utf8, text_to_insert.as_ref()); - cx.emit(CaretNotify::PauseBlinking); - self.emit_text_changed(cx); - cx.notify(); - } + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text_to_insert: &str, + _window: &mut Window, + cx: &mut Context, + ) { + let range_utf8 = self.ime_resolve_range(range_utf16); + let text_to_insert = self.validate_incoming_text(&range_utf8, text_to_insert); + self.replace_text(range_utf8, text_to_insert.as_ref()); + cx.emit(CaretNotify::PauseBlinking); + self.emit_text_changed(cx); + cx.notify(); + } - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - text_to_insert: &str, - new_selected_range_utf16: Option>, - _window: &mut Window, - cx: &mut Context, - ) { - let range = self.ime_resolve_range(range_utf16); - let text_to_insert = self.validate_incoming_text(&range, text_to_insert); - self.replace_text(range.clone(), text_to_insert.as_ref()); - self.ime_mark_text_in_range(&range, text_to_insert.len()); - self.ime_mark_selected_range(&range, &new_selected_range_utf16, text_to_insert.len()); - self.emit_text_changed(cx); - cx.notify(); - } + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text_to_insert: &str, + new_selected_range_utf16: Option>, + _window: &mut Window, + cx: &mut Context, + ) { + let range = self.ime_resolve_range(range_utf16); + let text_to_insert = self.validate_incoming_text(&range, text_to_insert); + self.replace_text(range.clone(), text_to_insert.as_ref()); + self.ime_mark_text_in_range(&range, text_to_insert.len()); + self.ime_mark_selected_range(&range, &new_selected_range_utf16, text_to_insert.len()); + self.emit_text_changed(cx); + cx.notify(); + } - fn bounds_for_range( - &mut self, - range_utf16: Range, - bounds: Bounds, - window: &mut Window, - _cx: &mut Context, - ) -> Option> { - let range = self.storage.utf_range_16to8(&range_utf16); - let line_height = window.line_height(); + fn bounds_for_range( + &mut self, + range_utf16: Range, + bounds: Bounds, + window: &mut Window, + _cx: &mut Context, + ) -> Option> { + let range = self.storage.utf_range_16to8(&range_utf16); + let line_height = window.line_height(); - for line in &self.layout_data.lines { - // The vertical offset of the text-segment from the start of the virtual box (which could be scrolled). - // Scrolling is not relevant here, so we are just operating on the internal virtualized space. - let y_offset = line.pos_y * line_height; - // The start of the line in screen space as if there was no scrolling. - let line_origin = bounds.origin + point(Pixels::ZERO, y_offset); - if line.text_range.is_empty() { - if range.start == line.text_range.start { - return Some(Bounds::from_corners( - line_origin, - line_origin + point(CARET_PIXELS_EPSILON, line_height), - )); - } - } else if line.text_range.contains(&range.start) - && let Some(wrapped) = &line.wrapped_line - { - let local_start = range.start - line.text_range.start; - let local_end = (range.end - line.text_range.start).min(wrapped.text.len()); + for line in &self.layout_data.lines { + // The vertical offset of the text-segment from the start of the virtual box (which could be scrolled). + // Scrolling is not relevant here, so we are just operating on the internal virtualized space. + let y_offset = line.pos_y * line_height; + // The start of the line in screen space as if there was no scrolling. + let line_origin = bounds.origin + point(Pixels::ZERO, y_offset); + if line.text_range.is_empty() { + if range.start == line.text_range.start { + return Some(Bounds::from_corners( + line_origin, + line_origin + point(CARET_PIXELS_EPSILON, line_height), + )); + } + } else if line.text_range.contains(&range.start) + && let Some(wrapped) = &line.wrapped_line + { + let local_start = range.start - line.text_range.start; + let local_end = (range.end - line.text_range.start).min(wrapped.text.len()); - // The start of the line in screen-space pixels - let line_start_screen_pos = wrapped - .position_for_index(local_start, line_height) - .unwrap_or_default(); - // The end of the line in screen-space pixels - let line_end_screen_pos = wrapped - .position_for_index(local_end, line_height) - .unwrap_or_else(|| { - // the y-height/position of the last line - let last_line_y = line_height * (line.row_count() - 1) as f32; - point(wrapped.width(), last_line_y) - }); + // The start of the line in screen-space pixels + let line_start_screen_pos = wrapped + .position_for_index(local_start, line_height) + .unwrap_or_default(); + // The end of the line in screen-space pixels + let line_end_screen_pos = wrapped + .position_for_index(local_end, line_height) + .unwrap_or_else(|| { + // the y-height/position of the last line + let last_line_y = line_height * (line.row_count() - 1) as f32; + point(wrapped.width(), last_line_y) + }); - // The number of rows this text-segment spans - let line_height_range = (line_start_screen_pos.y / line_height).floor() as usize - ..(line_end_screen_pos.y / line_height).floor() as usize; - // The width of the line-segment that may span multiple rows - let width = match line_height_range.is_empty() { - true => line_end_screen_pos.x, - false => wrapped.width(), - }; - return Some(Bounds::from_corners( - line_origin + line_start_screen_pos, - line_origin + point(width, line_start_screen_pos.y + line_height), - )); - } - } - None - } + // The number of rows this text-segment spans + let line_height_range = (line_start_screen_pos.y / line_height).floor() as usize + ..(line_end_screen_pos.y / line_height).floor() as usize; + // The width of the line-segment that may span multiple rows + let width = match line_height_range.is_empty() { + true => line_end_screen_pos.x, + false => wrapped.width(), + }; + return Some(Bounds::from_corners( + line_origin + line_start_screen_pos, + line_origin + point(width, line_start_screen_pos.y + line_height), + )); + } + } + None + } - fn character_index_for_point( - &mut self, - point: Point, - window: &mut Window, - _cx: &mut Context, - ) -> Option { - let index = self.index_for_pixel_point(point, window.line_height()); - Some(self.storage.utf_offset_8to16(index)) - } + fn character_index_for_point( + &mut self, + point: Point, + window: &mut Window, + _cx: &mut Context, + ) -> Option { + let index = self.index_for_pixel_point(point, window.line_height()); + Some(self.storage.utf_offset_8to16(index)) + } } // Input Action handler use super::{actions::*, history::HistoryKind}; impl<'app> EditableTextActionHandler> for EditableTextState { - fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<'app, Self>) { - self.selected_range = 0.into(); - cx.notify(); + fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<'app, Self>) { + self.selected_range = 0.into(); + cx.notify(); - window.blur(); - } + window.blur(); + } - fn insert_enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.supports_multiline { - return; - } - if !self.layout_data.accepts_input { - return; - } - self.replace_text_in_range(None, "\n", window, cx); - } + fn insert_enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.supports_multiline { + return; + } + if !self.layout_data.accepts_input { + return; + } + self.replace_text_in_range(None, "\n", window, cx); + } - fn insert_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.accepts_input { - return; - } - self.replace_text_in_range(None, "\t", window, cx); - } + fn insert_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.accepts_input { + return; + } + self.replace_text_in_range(None, "\t", window, cx); + } - fn delete_left(&mut self, _: &DeleteLeft, _: &mut Window, cx: &mut Context<'app, Self>) { - self.delete_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); - } + fn delete_left(&mut self, _: &DeleteLeft, _: &mut Window, cx: &mut Context<'app, Self>) { + self.delete_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } - fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.delete_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); - } + fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.delete_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } - fn delete_word_left( - &mut self, - _: &DeleteWordLeft, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.delete_linear(NavigationDirection::Back, TextBoundary::Word, cx); - } + fn delete_word_left( + &mut self, + _: &DeleteWordLeft, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.delete_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } - fn delete_word_right( - &mut self, - _: &DeleteWordRight, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.delete_linear(NavigationDirection::Forward, TextBoundary::Word, cx); - } + fn delete_word_right( + &mut self, + _: &DeleteWordRight, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.delete_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } - fn delete_to_line_start( - &mut self, - _: &DeleteToLineStart, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.delete_linear(NavigationDirection::Back, TextBoundary::Line, cx); - } + fn delete_to_line_start( + &mut self, + _: &DeleteToLineStart, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.delete_linear(NavigationDirection::Back, TextBoundary::Line, cx); + } - fn delete_to_line_end( - &mut self, - _: &DeleteToLineEnd, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.delete_linear(NavigationDirection::Forward, TextBoundary::Line, cx); - } + fn delete_to_line_end( + &mut self, + _: &DeleteToLineEnd, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.delete_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + } - fn nav_left(&mut self, _: &NavLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); - } + fn nav_left(&mut self, _: &NavLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } - fn nav_right(&mut self, _: &NavRight, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); - } + fn nav_right(&mut self, _: &NavRight, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } - fn nav_up(&mut self, _: &NavUp, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.supports_multiline { - // semantically equivalent to line - self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); - return; - } + fn nav_up(&mut self, _: &NavUp, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.supports_multiline { + // semantically equivalent to line + self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); + return; + } - if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height()) - { - self.move_to(caret_pos, cx); - } - } + if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height()) + { + self.move_to(caret_pos, cx); + } + } - fn nav_down(&mut self, _: &NavDown, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.supports_multiline { - // semantically equivalent to line - self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); - return; - } + fn nav_down(&mut self, _: &NavDown, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.supports_multiline { + // semantically equivalent to line + self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + return; + } - if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) { - self.move_to(caret_pos, cx); - } - } + if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) { + self.move_to(caret_pos, cx); + } + } - fn nav_line_start(&mut self, _: &NavLineStart, _w: &mut Window, cx: &mut Context<'app, Self>) { - // [when not multiline] semantically equivalent to document - self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); - } + fn nav_line_start(&mut self, _: &NavLineStart, _w: &mut Window, cx: &mut Context<'app, Self>) { + // [when not multiline] semantically equivalent to document + self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); + } - fn nav_line_end(&mut self, _: &NavLineEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { - // [when not multiline] semantically equivalent to document - self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); - } + fn nav_line_end(&mut self, _: &NavLineEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { + // [when not multiline] semantically equivalent to document + self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + } - fn nav_start(&mut self, _: &NavDocumentStart, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Back, TextBoundary::Document, cx); - } + fn nav_start(&mut self, _: &NavDocumentStart, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } - fn nav_end(&mut self, _: &NavDocumentEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx); - } + fn nav_end(&mut self, _: &NavDocumentEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } - fn nav_left_word(&mut self, _: &NavWordLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Back, TextBoundary::Word, cx); - } + fn nav_left_word(&mut self, _: &NavWordLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } - fn nav_right_word(&mut self, _: &NavWordRight, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx); - } + fn nav_right_word(&mut self, _: &NavWordRight, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } - fn select_all(&mut self, _: &SelectAll, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.select_document(cx); - } + fn select_all(&mut self, _: &SelectAll, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.select_document(cx); + } - fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); - } + fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } - fn select_right(&mut self, _: &SelectRight, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); - } + fn select_right(&mut self, _: &SelectRight, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } - fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.supports_multiline { - // semantically equivalent to select document - self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx); - return; - } + fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.supports_multiline { + // semantically equivalent to select document + self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx); + return; + } - if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height()) - { - self.select_to(caret_pos, cx); - } - } + if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height()) + { + self.select_to(caret_pos, cx); + } + } - fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.supports_multiline { - // semantically equivalent to select document - self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); - return; - } + fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.supports_multiline { + // semantically equivalent to select document + self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + return; + } - if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) { - self.select_to(caret_pos, cx); - } - } + if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) { + self.select_to(caret_pos, cx); + } + } - fn select_start( - &mut self, - _: &SelectDocumentStart, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx); - } + fn select_start( + &mut self, + _: &SelectDocumentStart, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } - fn select_end(&mut self, _: &SelectDocumentEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); - } + fn select_end(&mut self, _: &SelectDocumentEnd, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } - fn select_left_word( - &mut self, - _: &SelectWordLeft, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.select_linear(NavigationDirection::Back, TextBoundary::Word, cx); - } + fn select_left_word( + &mut self, + _: &SelectWordLeft, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.select_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } - fn select_right_word( - &mut self, - _: &SelectWordRight, - _w: &mut Window, - cx: &mut Context<'app, Self>, - ) { - self.select_linear(NavigationDirection::Forward, TextBoundary::Word, cx); - } + fn select_right_word( + &mut self, + _: &SelectWordRight, + _w: &mut Window, + cx: &mut Context<'app, Self>, + ) { + self.select_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } - fn cut(&mut self, _: &Cut, _w: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.accepts_input { - return; - } + fn cut(&mut self, _: &Cut, _w: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.accepts_input { + return; + } - let range_to_cut = match self.selected_range.is_empty() { - // selection is more than a caret, use that range of text - false => self.selected_range.range(), - // No selection: cut the entire current line (including newline) - true => { - use NavigationDirection::*; - use TextBoundary::*; + let range_to_cut = match self.selected_range.is_empty() { + // selection is more than a caret, use that range of text + false => self.selected_range.range(), + // No selection: cut the entire current line (including newline) + true => { + use NavigationDirection::*; + use TextBoundary::*; - let caret = self.caret_pos(); - let line_start = self.storage.offset_from_caret(caret, Back, Line); - let line_end = self.storage.offset_from_caret(caret, Forward, Line); - let storage_len_utf8 = self.as_str().len(); + let caret = self.caret_pos(); + let line_start = self.storage.offset_from_caret(caret, Back, Line); + let line_end = self.storage.offset_from_caret(caret, Forward, Line); + let storage_len_utf8 = self.as_str().len(); - // Include the newline character if there is one after the line - let cut_end = if line_end < storage_len_utf8 { - line_end + 1 // Include the newline - } else if line_start > 0 { - // Last line with no trailing newline - include preceding newline instead - line_end - } else { - line_end - }; + // Include the newline character if there is one after the line + let cut_end = if line_end < storage_len_utf8 { + line_end + 1 // Include the newline + } else if line_start > 0 { + // Last line with no trailing newline - include preceding newline instead + line_end + } else { + line_end + }; - // For last line, also remove the preceding newline if it exists - let cut_start = if line_end >= storage_len_utf8 && line_start > 0 { - line_start - 1 // Include preceding newline for last line - } else { - line_start - }; + // For last line, also remove the preceding newline if it exists + let cut_start = if line_end >= storage_len_utf8 && line_start > 0 { + line_start - 1 // Include preceding newline for last line + } else { + line_start + }; - cut_start..cut_end - } - }; + cut_start..cut_end + } + }; - // Cut selected text - let slice = &self.storage.content_utf8()[range_to_cut.clone()]; - cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); - self.replace_text(range_to_cut, ""); + // Cut selected text + let slice = &self.storage.content_utf8()[range_to_cut.clone()]; + cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); + self.replace_text(range_to_cut, ""); - self.emit_text_changed(cx); - cx.notify(); - } + self.emit_text_changed(cx); + cx.notify(); + } - fn copy(&mut self, _: &Copy, _w: &mut Window, cx: &mut Context<'app, Self>) { - if !self.selected_range.is_empty() { - let slice = &self.storage.content_utf8()[self.selected_range.range()]; - cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); - } - } + fn copy(&mut self, _: &Copy, _w: &mut Window, cx: &mut Context<'app, Self>) { + if !self.selected_range.is_empty() { + let slice = &self.storage.content_utf8()[self.selected_range.range()]; + cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); + } + } - fn paste(&mut self, _: &Paste, _w: &mut Window, cx: &mut Context<'app, Self>) { - if !self.layout_data.accepts_input { - return; - } + fn paste(&mut self, _: &Paste, _w: &mut Window, cx: &mut Context<'app, Self>) { + if !self.layout_data.accepts_input { + return; + } - let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { - return; - }; + let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + return; + }; - let range = self.ime_resolve_range(None); - let text_to_insert = self.validate_incoming_text(&range, &text); - self.replace_text(range, text_to_insert.as_ref()); - self.emit_text_changed(cx); - cx.notify(); - } + let range = self.ime_resolve_range(None); + let text_to_insert = self.validate_incoming_text(&range, &text); + self.replace_text(range, text_to_insert.as_ref()); + self.emit_text_changed(cx); + cx.notify(); + } - fn undo(&mut self, _: &Undo, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.apply_from_history(HistoryKind::Undo, HistoryKind::Redo, cx); - } + fn undo(&mut self, _: &Undo, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.apply_from_history(HistoryKind::Undo, HistoryKind::Redo, cx); + } - fn redo(&mut self, _: &Redo, _w: &mut Window, cx: &mut Context<'app, Self>) { - self.apply_from_history(HistoryKind::Redo, HistoryKind::Undo, cx); - } + fn redo(&mut self, _: &Redo, _w: &mut Window, cx: &mut Context<'app, Self>) { + self.apply_from_history(HistoryKind::Redo, HistoryKind::Undo, cx); + } - fn on_mouse_down( - &mut self, - event: &gpui::MouseDownEvent, - text_position: Point, - window: &mut Window, - cx: &mut Context<'app, Self>, - ) { - const DOUBLE_CLICK: usize = 2; - const TRIPLE_CLICK: usize = 3; + fn on_mouse_down( + &mut self, + event: &gpui::MouseDownEvent, + text_position: Point, + window: &mut Window, + cx: &mut Context<'app, Self>, + ) { + const DOUBLE_CLICK: usize = 2; + const TRIPLE_CLICK: usize = 3; - let caret_pos = self.index_for_pixel_point(text_position, window.line_height()); + let caret_pos = self.index_for_pixel_point(text_position, window.line_height()); - self.is_selecting = true; - self.apply_click(event.click_count, text_position); + self.is_selecting = true; + self.apply_click(event.click_count, text_position); - match self.click_count { - DOUBLE_CLICK => self.select_word_at(caret_pos, cx), - TRIPLE_CLICK => self.select_line_at(caret_pos, cx), - _ if event.modifiers.shift => self.select_to(caret_pos, cx), - _ => self.move_to(caret_pos, cx), - } - } + match self.click_count { + DOUBLE_CLICK => self.select_word_at(caret_pos, cx), + TRIPLE_CLICK => self.select_line_at(caret_pos, cx), + _ if event.modifiers.shift => self.select_to(caret_pos, cx), + _ => self.move_to(caret_pos, cx), + } + } - fn on_mouse_up( - &mut self, - _event: &gpui::MouseUpEvent, - _w: &mut Window, - _cx: &mut Context<'app, Self>, - ) { - self.is_selecting = false; - } + fn on_mouse_up( + &mut self, + _event: &gpui::MouseUpEvent, + _w: &mut Window, + _cx: &mut Context<'app, Self>, + ) { + self.is_selecting = false; + } - fn on_mouse_move( - &mut self, - _event: &gpui::MouseMoveEvent, - text_position: Point, - window: &mut Window, - cx: &mut Context<'app, Self>, - ) { - if self.is_selecting && self.click_count == 1 { - let character_pos = self.index_for_pixel_point(text_position, window.line_height()); - self.select_to(character_pos, cx); - } - } + fn on_mouse_move( + &mut self, + _event: &gpui::MouseMoveEvent, + text_position: Point, + window: &mut Window, + cx: &mut Context<'app, Self>, + ) { + if self.is_selecting && self.click_count == 1 { + let character_pos = self.index_for_pixel_point(text_position, window.line_height()); + self.select_to(character_pos, cx); + } + } } /// Backlog: @@ -1150,1643 +1150,1643 @@ impl<'app> EditableTextActionHandler> for EditableTextState /// permutations of: single and multiline fields, wrap vs no-wrap, overflow scroll vs no scroll #[cfg(test)] mod tests { - use std::time::Duration; - - use super::*; - use crate::editable_text::StringStorage; - use gpui::{AppContext, Entity, IntoElement, Render, TestAppContext, WindowHandle, div}; - - struct TestView { - input: Entity, - } - - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - } - } - - fn default_state(content: &str, cx: &mut Context) -> EditableTextState { - EditableTextState::new(StringStorage::from(content), cx) - } - - fn create_test_input( - cx: &mut TestAppContext, - content: &str, - range: impl Into, - ) -> WindowHandle { - cx.add_window(|_window, cx| { - let input = cx.new(|cx| { - let mut input = default_state(content, cx); - input.selected_range = range.into(); - input.layout_data.accepts_input = true; - input - }); - TestView { input } - }) - } - - // Disable grouping for predictable test behavior - fn without_history_grouping(state: &mut EditableTextState) { - state - .history - .get_or_insert_default() - .set_grouping_interval(Duration::from_secs(0)); - } - - fn is_history_kind_available(state: &EditableTextState, kind: HistoryKind) -> bool { - state - .history() - .map(|history| history.has_next(kind)) - .unwrap_or_default() - } - - // ============================================================ - // BASIC MOVEMENT - // ============================================================ - - #[gpui::test] - fn test_left_at_start_of_content(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_left_moves_by_grapheme(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 3); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 2.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_left_collapses_selection_to_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", (1, 4)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 1.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_left_stops_at_end_of_line(cx: &mut TestAppContext) { - // "ab\ncd" - cursor at position 3 (start of "cd", after newline) - // Pressing left should move to position 2 (end of "ab", before newline) - let view = create_test_input(cx, "ab\ncd", 3); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 2.into()); // cursor at end of line 1 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_right_at_end_of_content(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_right_moves_by_grapheme(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 2); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 3.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_right_collapses_selection_to_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", (1, 4)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 4.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_right_stops_at_end_of_line(cx: &mut TestAppContext) { - // "ab\ncd" - cursor at position 1 (after 'a') - // Pressing right should move to position 2 (end of "ab", before newline) - let view = create_test_input(cx, "ab\ncd", 1); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 2.into()); // cursor at end of line 1 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_right_crosses_newline(cx: &mut TestAppContext) { - // "ab\ncd" - cursor at position 2 (end of "ab", before newline) - // Pressing right should move to position 3 (after newline, start of "cd") - let view = create_test_input(cx, "ab\ncd", 2); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 3.into()); // cursor at start of line 2 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_left_crosses_newline(cx: &mut TestAppContext) { - // "ab\ncd" - cursor at position 2 (end of "ab", before newline) - // Pressing left should move to position 1 (after 'a') - let view = create_test_input(cx, "ab\ncd", 2); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 1.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_home_moves_to_line_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "first\nsecond", 9); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_line_start(&NavLineStart, window, cx); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_end_moves_to_line_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "first\nsecond", 8); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_line_end(&NavLineEnd, window, cx); - assert_eq!(input.selected_range, 12.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_move_to_beginning(cx: &mut TestAppContext) { - let view = create_test_input(cx, "first\nsecond\nthird", 9); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_start(&NavDocumentStart, window, cx); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_move_to_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "first\nsecond\nthird", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_end(&NavDocumentEnd, window, cx); - assert_eq!(input.selected_range, 18.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // WORD MOVEMENT - // ============================================================ - - #[gpui::test] - fn test_word_left_at_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left_word(&NavWordLeft, window, cx); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_word_left_stops_at_boundary(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world test", 11); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left_word(&NavWordLeft, window, cx); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_word_right_at_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 11); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right_word(&NavWordRight, window, cx); - assert_eq!(input.selected_range, 11.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_word_right_stops_at_boundary(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world test", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right_word(&NavWordRight, window, cx); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // SELECTION - // ============================================================ - - #[gpui::test] - fn test_select_left_extends_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 3); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_left(&SelectLeft, window, cx); - assert_eq!(input.selected_range, (2, 3).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_select_right_extends_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 2..2); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_right(&SelectRight, window, cx); - assert_eq!(input.selected_range, (3, 2).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_select_all(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello\nworld", 3); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_all(&SelectAll, window, cx); - assert_eq!(input.selected_range, (0, 11).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_select_to_beginning(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 6); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_start(&SelectDocumentStart, window, cx); - assert_eq!(input.selected_range, (0, 6).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_select_to_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 6); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_end(&SelectDocumentEnd, window, cx); - assert_eq!(input.selected_range, (11, 6).into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // EDITING - BACKSPACE - // ============================================================ - - #[gpui::test] - fn test_backspace_deletes_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (6, 11)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "hello "); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_deletes_previous_grapheme(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "hell"); - assert_eq!(input.selected_range, 4.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_at_start_does_nothing(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "hello"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_deletes_entire_emoji(cx: &mut TestAppContext) { - let view = create_test_input(cx, "Hi 👋", 7); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "Hi "); - assert_eq!(input.selected_range, 3.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // EDITING - DELETE - // ============================================================ - - #[gpui::test] - fn test_delete_deletes_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_deletes_next_grapheme(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), "ello"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_at_end_does_nothing(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), "hello"); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // EDITING - ENTER - // ============================================================ - - #[gpui::test] - fn test_enter_inserts_newline(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.layout_data.supports_multiline = true; - input.insert_enter(&Enter, window, cx); - assert_eq!(input.as_str(), "hello\n world"); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_enter_replaces_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (5, 6)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.layout_data.supports_multiline = true; - input.insert_enter(&Enter, window, cx); - assert_eq!(input.as_str(), "hello\nworld"); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // CLIPBOARD - // ============================================================ - - #[gpui::test] - fn test_copy_with_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (6, 11)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.copy(&Copy, window, cx); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert!(clipboard.is_some()); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("world")); - } - - #[gpui::test] - fn test_cut_with_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("hello")); - } - - #[gpui::test] - fn test_paste_inserts_text(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - cx.write_to_clipboard(ClipboardItem::new_string(" there".to_string())); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.paste(&Paste, window, cx); - assert_eq!(input.as_str(), "hello there world"); - assert_eq!(input.selected_range, 11.into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // UNICODE / GRAPHEME HANDLING - // ============================================================ - - #[gpui::test] - fn test_movement_with_multibyte_utf8(cx: &mut TestAppContext) { - let view = create_test_input(cx, "café", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 1.into()); - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 2.into()); - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 3.into()); - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_movement_with_emoji(cx: &mut TestAppContext) { - let view = create_test_input(cx, "a👋b", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 1.into()); - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 5.into()); - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 6.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_selection_with_multibyte_characters(cx: &mut TestAppContext) { - let view = create_test_input(cx, "日本語", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_right(&SelectRight, window, cx); - assert_eq!(input.selected_range, (3, 0).into()); - input.select_right(&SelectRight, window, cx); - assert_eq!(input.selected_range, (6, 0).into()); - input.select_right(&SelectRight, window, cx); - assert_eq!(input.selected_range, (9, 0).into()); - }); - }) - .unwrap(); - } - - // ============================================================ - // NEWLINE HANDLING - // ============================================================ - - #[gpui::test] - fn test_find_line_start_and_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "first\nsecond\nthird", 0); - view.update(cx, |view, _window, cx| { - view.input.update(cx, |input, _cx| { - use NavigationDirection::*; - use TextBoundary::*; - let storage = &input.storage; - - assert_eq!(storage.offset_from_caret(0, Back, Line), 0); - assert_eq!(storage.offset_from_caret(3, Back, Line), 0); - assert_eq!(storage.offset_from_caret(6, Back, Line), 6); - assert_eq!(storage.offset_from_caret(13, Back, Line), 13); - - assert_eq!(storage.offset_from_caret(0, Forward, Line), 5); - assert_eq!(storage.offset_from_caret(6, Forward, Line), 12); - assert_eq!(storage.offset_from_caret(13, Forward, Line), 18); - }); - }) - .unwrap(); - } - - // ============================================================ - // EDGE CASES - // ============================================================ - - #[gpui::test] - fn test_operations_on_empty_content(cx: &mut TestAppContext) { - let view = create_test_input(cx, "", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range, 0.into()); - - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range, 0.into()); - - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), ""); - - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), ""); - - input.select_all(&SelectAll, window, cx); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_set_content_resets_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (3, 8)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.marked_range = Some(5..7); - input.replace_text_in_range(Some(0..11), "new content", window, cx); - assert_eq!(input.as_str(), "new content"); - assert_eq!(input.selected_range, 11.into()); - assert_eq!(input.marked_range, None); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_cursor_clamped_to_content_length(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 100); - view.update(cx, |view, _window, cx| { - view.input.update(cx, |input, cx| { - input.move_to(1000, cx); - assert_eq!(input.selected_range, 5.into()); - - input.selected_range = 0.into(); - input.select_to(1000, cx); - assert_eq!(input.selected_range, (5, 0).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_previous_boundary_at_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, _window, cx| { - view.input.update(cx, |input, _cx| { - use NavigationDirection::*; - use TextBoundary::*; - assert_eq!(input.storage.offset_from_caret(0, Back, Graphmeme), 0); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_next_boundary_at_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, _window, cx| { - view.input.update(cx, |input, _cx| { - use NavigationDirection::*; - use TextBoundary::*; - let storage = &input.storage; - assert_eq!(storage.offset_from_caret(5, Forward, Graphmeme), 5); - assert_eq!(storage.offset_from_caret(100, Forward, Graphmeme), 5); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_word_range_at_boundary(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 0); - view.update(cx, |view, _window, cx| { - view.input.update(cx, |input, _cx| { - let range = input.storage.word_range_at(5); - assert_eq!(range.start, 0); - assert_eq!(range.end, 5); - - let range = input.storage.word_range_at(8); - assert_eq!(range.start, 6); - assert_eq!(range.end, 11); - }); - }) - .unwrap(); - } - - // ============================================================ - // EMOJI & GRAPHEME CLUSTERS - // ============================================================ - - #[gpui::test] - fn test_simple_emoji_navigation(cx: &mut TestAppContext) { - // 😀 is 4 bytes in UTF-8 - let view = create_test_input(cx, "a😀b", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - // Move right through: a -> 😀 -> b - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range.start, 1); // after 'a' - - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range.start, 5); // after 😀 (1 + 4 bytes) - - input.nav_right(&NavRight, window, cx); - assert_eq!(input.selected_range.start, 6); // after 'b' - - // Move left back - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range.start, 5); // before 'b' - - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range.start, 1); // before 😀 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_emoji_with_skin_tone_modifier(cx: &mut TestAppContext) { - // 👋🏽 = 👋 (U+1F44B, 4 bytes) + 🏽 (U+1F3FD, 4 bytes) = 8 bytes total - let emoji = "👋🏽"; - assert_eq!(emoji.len(), 8); - - let view = create_test_input(cx, &format!("a{}b", emoji), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'a' - assert_eq!(input.selected_range.start, 1); - - input.nav_right(&NavRight, window, cx); // past entire emoji with modifier - assert_eq!(input.selected_range.start, 9); // 1 + 8 - - input.nav_left(&NavLeft, window, cx); // back before emoji - assert_eq!(input.selected_range.start, 1); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_zwj_family_emoji(cx: &mut TestAppContext) { - // 👨‍👩‍👧 = man + ZWJ + woman + ZWJ + girl - // Each person emoji is 4 bytes, ZWJ is 3 bytes - // Total: 4 + 3 + 4 + 3 + 4 = 18 bytes - let family = "👨‍👩‍👧"; - assert_eq!(family.len(), 18); - - let view = create_test_input(cx, &format!("x{}y", family), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'x' - assert_eq!(input.selected_range.start, 1); - - input.nav_right(&NavRight, window, cx); // past entire ZWJ sequence - assert_eq!(input.selected_range.start, 19); // 1 + 18 - - input.nav_right(&NavRight, window, cx); // past 'y' - assert_eq!(input.selected_range.start, 20); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_deletes_emoji_between_ascii(cx: &mut TestAppContext) { - let view = create_test_input(cx, "a😀b", 5); // cursor after emoji - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "ab"); - assert_eq!(input.selected_range.start, 1); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_deletes_zwj_sequence(cx: &mut TestAppContext) { - let family = "👨‍👩‍👧"; - let content = format!("a{}b", family); - let cursor_pos = 1 + family.len(); // after the family emoji - - let view = create_test_input(cx, &content, cursor_pos..cursor_pos); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "ab"); - assert_eq!(input.selected_range.start, 1); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_removes_entire_emoji(cx: &mut TestAppContext) { - let view = create_test_input(cx, "a😀b", 1); // cursor before emoji - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), "ab"); - assert_eq!(input.selected_range.start, 1); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_flag_emoji_navigation(cx: &mut TestAppContext) { - // 🇯🇵 = Regional Indicator J (4 bytes) + Regional Indicator P (4 bytes) - let flag = "🇯🇵"; - assert_eq!(flag.len(), 8); - - let view = create_test_input(cx, &format!("x{}y", flag), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'x' - input.nav_right(&NavRight, window, cx); // past flag (should be single grapheme) - assert_eq!(input.selected_range.start, 9); // 1 + 8 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_combining_diacritical_marks(cx: &mut TestAppContext) { - // é as e + combining acute accent (U+0301) - let combining = "e\u{0301}"; // 1 + 2 = 3 bytes - assert_eq!(combining.len(), 3); - - let view = create_test_input(cx, &format!("a{}b", combining), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'a' - assert_eq!(input.selected_range.start, 1); - - input.nav_right(&NavRight, window, cx); // past e + combining mark (single grapheme) - assert_eq!(input.selected_range.start, 4); // 1 + 3 - - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range.start, 1); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_multiple_combining_marks(cx: &mut TestAppContext) { - // ë́ = e + combining diaeresis (U+0308) + combining acute (U+0301) - let multi_combining = "e\u{0308}\u{0301}"; // 1 + 2 + 2 = 5 bytes - assert_eq!(multi_combining.len(), 5); - - let view = create_test_input(cx, &format!("x{}y", multi_combining), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'x' - input.nav_right(&NavRight, window, cx); // past entire combined character - assert_eq!(input.selected_range.start, 6); // 1 + 5 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_select_emoji_with_shift(cx: &mut TestAppContext) { - let view = create_test_input(cx, "a😀b", 1); // cursor before emoji - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_right(&SelectRight, window, cx); - assert_eq!(input.selected_range, (5, 1).into()); // selected the entire emoji - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_cjk_characters(cx: &mut TestAppContext) { - // 你好 - each character is 3 bytes in UTF-8 - let view = create_test_input(cx, "a你好b", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'a' - assert_eq!(input.selected_range.start, 1); - - input.nav_right(&NavRight, window, cx); // past 你 - assert_eq!(input.selected_range.start, 4); // 1 + 3 - - input.nav_right(&NavRight, window, cx); // past 好 - assert_eq!(input.selected_range.start, 7); // 4 + 3 - - input.nav_right(&NavRight, window, cx); // past 'b' - assert_eq!(input.selected_range.start, 8); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_mixed_script_text(cx: &mut TestAppContext) { - // Mix of ASCII, CJK, and emoji - let view = create_test_input(cx, "Hi你😀", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'H' - assert_eq!(input.selected_range.start, 1); - - input.nav_right(&NavRight, window, cx); // past 'i' - assert_eq!(input.selected_range.start, 2); - - input.nav_right(&NavRight, window, cx); // past 你 (3 bytes) - assert_eq!(input.selected_range.start, 5); - - input.nav_right(&NavRight, window, cx); // past 😀 (4 bytes) - assert_eq!(input.selected_range.start, 9); - - // Now go back - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range.start, 5); - - input.nav_left(&NavLeft, window, cx); - assert_eq!(input.selected_range.start, 2); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_variation_selector_emoji(cx: &mut TestAppContext) { - // ☺️ = ☺ (U+263A, 3 bytes) + variation selector-16 (U+FE0F, 3 bytes) - let emoji_presentation = "☺\u{FE0F}"; - assert_eq!(emoji_presentation.len(), 6); - - let view = create_test_input(cx, &format!("a{}b", emoji_presentation), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'a' - input.nav_right(&NavRight, window, cx); // past emoji with variation selector - assert_eq!(input.selected_range.start, 7); // 1 + 6 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_keycap_emoji(cx: &mut TestAppContext) { - // 1️⃣ = 1 + variation selector + combining enclosing keycap - let keycap = "1\u{FE0F}\u{20E3}"; - - let view = create_test_input(cx, &format!("x{}y", keycap), 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_right(&NavRight, window, cx); // past 'x' - input.nav_right(&NavRight, window, cx); // past keycap sequence - let expected_pos = 1 + keycap.len(); - assert_eq!(input.selected_range.start, expected_pos); - }); - }) - .unwrap(); - } - - // Single-line input tests - - fn create_single_line_input( - cx: &mut TestAppContext, - content: &str, - selected_range: impl Into, - ) -> WindowHandle { - cx.add_window(|_window, cx| { - let input = cx.new(|cx| { - let mut input = default_state(content, cx); - input.selected_range = selected_range.into(); - input - }); - TestView { input } - }) - } - - #[gpui::test] - fn test_single_line_enter_does_nothing(cx: &mut TestAppContext) { - let view = create_single_line_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.insert_enter(&Enter, window, cx); - assert_eq!(input.as_str(), "hello"); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_single_line_up_moves_to_start(cx: &mut TestAppContext) { - let view = create_single_line_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_up(&NavUp, window, cx); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_single_line_down_moves_to_end(cx: &mut TestAppContext) { - let view = create_single_line_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.nav_down(&NavDown, window, cx); - assert_eq!(input.selected_range, 11.into()); // "hello world".len() == 11 - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_single_line_select_up_selects_to_start(cx: &mut TestAppContext) { - let view = create_single_line_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_up(&SelectUp, window, cx); - assert_eq!(input.selected_range, (0, 5).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_single_line_select_down_selects_to_end(cx: &mut TestAppContext) { - let view = create_single_line_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.select_down(&SelectDown, window, cx); - assert_eq!(input.selected_range, (11, 5).into()); // "hello world".len() == 11 - }); - }) - .unwrap(); - } - - // ============================================================ - // UNDO / REDO - // ============================================================ - - #[gpui::test] - fn test_undo_restores_content(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - // Make an edit - input.replace_text_in_range(None, " world", window, cx); - assert_eq!(input.as_str(), "hello world"); - - // Undo should restore original content - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_redo_restores_undone_content(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.replace_text_in_range(None, " world", window, cx); - assert_eq!(input.as_str(), "hello world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - - input.redo(&Redo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_undo_with_no_history_does_nothing(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - assert!(!is_history_kind_available(input, HistoryKind::Undo)); - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_redo_with_no_history_does_nothing(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - assert!(!is_history_kind_available(input, HistoryKind::Redo)); - input.redo(&Redo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_undo_restores_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - // Delete selection - input.replace_text_in_range(None, "", window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - - // Undo should restore content and selection - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - assert_eq!(input.selected_range, (0, 5).into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_multiple_undo_redo(cx: &mut TestAppContext) { - let view = create_test_input(cx, "", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.replace_text_in_range(None, "a", window, cx); - input.replace_text_in_range(None, "b", window, cx); - input.replace_text_in_range(None, "c", window, cx); - assert_eq!(input.as_str(), "abc"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "ab"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "a"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), ""); - - input.redo(&Redo, window, cx); - assert_eq!(input.as_str(), "a"); - - input.redo(&Redo, window, cx); - assert_eq!(input.as_str(), "ab"); - - input.redo(&Redo, window, cx); - assert_eq!(input.as_str(), "abc"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_new_edit_clears_redo_stack(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.replace_text_in_range(None, " world", window, cx); - assert_eq!(input.as_str(), "hello world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - assert!(is_history_kind_available(input, HistoryKind::Redo)); - - // New edit should clear redo stack - input.replace_text_in_range(None, "!", window, cx); - assert_eq!(input.as_str(), "hello!"); - assert!(!is_history_kind_available(input, HistoryKind::Redo)); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_can_undo_can_redo(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - assert!(!is_history_kind_available(input, HistoryKind::Undo)); - assert!(!is_history_kind_available(input, HistoryKind::Redo)); - - input.replace_text_in_range(None, "!", window, cx); - assert!(is_history_kind_available(input, HistoryKind::Undo)); - assert!(!is_history_kind_available(input, HistoryKind::Redo)); - - input.undo(&Undo, window, cx); - assert!(!is_history_kind_available(input, HistoryKind::Undo)); - assert!(is_history_kind_available(input, HistoryKind::Redo)); - - input.redo(&Redo, window, cx); - assert!(is_history_kind_available(input, HistoryKind::Undo)); - assert!(!is_history_kind_available(input, HistoryKind::Redo)); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_backspace_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_left(&DeleteLeft, window, cx); - assert_eq!(input.as_str(), "hell"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_right(&DeleteRight, window, cx); - assert_eq!(input.as_str(), "ello"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_cut_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), " world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_cut_line_with_no_selection(cx: &mut TestAppContext) { - // Cursor in middle line, no selection - should cut entire line including newline - let view = create_test_input(cx, "line1\nline2\nline3", 8); // cursor in "line2" - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), "line1\nline3"); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("line2\n")); - } - - #[gpui::test] - fn test_cut_first_line_with_no_selection(cx: &mut TestAppContext) { - // Cursor on first line, no selection - let view = create_test_input(cx, "line1\nline2\nline3", 2); // cursor in "line1" - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), "line2\nline3"); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("line1\n")); - } - - #[gpui::test] - fn test_cut_last_line_with_no_selection(cx: &mut TestAppContext) { - // Cursor on last line, no selection - should include preceding newline - let view = create_test_input(cx, "line1\nline2\nline3", 14); // cursor in "line3" - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), "line1\nline2"); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("\nline3")); - } - - #[gpui::test] - fn test_cut_empty_line(cx: &mut TestAppContext) { - // Cursor on empty line - should remove that line - let view = create_test_input(cx, "line1\n\nline3", 6); // cursor on empty line - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), "line1\nline3"); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("\n")); - } - - #[gpui::test] - fn test_cut_only_line_with_no_selection(cx: &mut TestAppContext) { - // Single line content, no selection - should cut entire content - let view = create_test_input(cx, "hello", 2); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), ""); - }); - }) - .unwrap(); - - let clipboard = cx.read_from_clipboard(); - assert_eq!(clipboard.unwrap().text().as_deref(), Some("hello")); - } - - #[gpui::test] - fn test_cut_line_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "line1\nline2\nline3", 8); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.cut(&Cut, window, cx); - assert_eq!(input.as_str(), "line1\nline3"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "line1\nline2\nline3"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_paste_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello", 5); - cx.write_to_clipboard(ClipboardItem::new_string(" world".to_string())); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.paste(&Paste, window, cx); - assert_eq!(input.as_str(), "hello world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_enter_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - input.layout_data.supports_multiline = true; - - input.insert_enter(&Enter, window, cx); - assert_eq!(input.as_str(), "hello\n world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_left(cx: &mut TestAppContext) { - // Cursor at end of "hello" in "hello world" - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_left(&DeleteWordLeft, window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_left_with_selection(cx: &mut TestAppContext) { - // Selection from 0 to 5 ("hello") - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_left(&DeleteWordLeft, window, cx); - assert_eq!(input.as_str(), " world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_left_at_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_left(&DeleteWordLeft, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_right(cx: &mut TestAppContext) { - // Cursor at start - let view = create_test_input(cx, "hello world", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_right(&DeleteWordRight, window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_right_with_selection(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", (0, 5)); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_right(&DeleteWordRight, window, cx); - assert_eq!(input.as_str(), " world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_right_at_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 11); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_word_right(&DeleteWordRight, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_start(&DeleteToLineStart, window, cx); - assert_eq!(input.as_str(), " world"); - assert_eq!(input.selected_range, 0.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_beginning_of_line_multiline(cx: &mut TestAppContext) { - // Cursor at position 8 (middle of "line2") - let view = create_test_input(cx, "line1\nline2\nline3", 8); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_start(&DeleteToLineStart, window, cx); - assert_eq!(input.as_str(), "line1\nne2\nline3"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_beginning_of_line_at_start(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 0); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_start(&DeleteToLineStart, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_end_of_line(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_end(&DeleteToLineEnd, window, cx); - assert_eq!(input.as_str(), "hello"); - assert_eq!(input.selected_range, 5.into()); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_end_of_line_multiline(cx: &mut TestAppContext) { - // Cursor at position 8 (middle of "line2") - let view = create_test_input(cx, "line1\nline2\nline3", 8); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_end(&DeleteToLineEnd, window, cx); - assert_eq!(input.as_str(), "line1\nli\nline3"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_end_of_line_at_end(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 11); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - input.delete_to_line_end(&DeleteToLineEnd, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_left_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_word_left(&DeleteWordLeft, window, cx); - assert_eq!(input.as_str(), " world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_word_right_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 6); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_word_right(&DeleteWordRight, window, cx); - assert_eq!(input.as_str(), "hello "); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_beginning_of_line_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_to_line_start(&DeleteToLineStart, window, cx); - assert_eq!(input.as_str(), " world"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } - - #[gpui::test] - fn test_delete_to_end_of_line_is_undoable(cx: &mut TestAppContext) { - let view = create_test_input(cx, "hello world", 5); - view.update(cx, |view, window, cx| { - view.input.update(cx, |input, cx| { - without_history_grouping(input); - - input.delete_to_line_end(&DeleteToLineEnd, window, cx); - assert_eq!(input.as_str(), "hello"); - - input.undo(&Undo, window, cx); - assert_eq!(input.as_str(), "hello world"); - }); - }) - .unwrap(); - } + use std::time::Duration; + + use super::*; + use crate::editable_text::StringStorage; + use gpui::{AppContext, Entity, IntoElement, Render, TestAppContext, WindowHandle, div}; + + struct TestView { + input: Entity, + } + + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } + } + + fn default_state(content: &str, cx: &mut Context) -> EditableTextState { + EditableTextState::new(StringStorage::from(content), cx) + } + + fn create_test_input( + cx: &mut TestAppContext, + content: &str, + range: impl Into, + ) -> WindowHandle { + cx.add_window(|_window, cx| { + let input = cx.new(|cx| { + let mut input = default_state(content, cx); + input.selected_range = range.into(); + input.layout_data.accepts_input = true; + input + }); + TestView { input } + }) + } + + // Disable grouping for predictable test behavior + fn without_history_grouping(state: &mut EditableTextState) { + state + .history + .get_or_insert_default() + .set_grouping_interval(Duration::from_secs(0)); + } + + fn is_history_kind_available(state: &EditableTextState, kind: HistoryKind) -> bool { + state + .history() + .map(|history| history.has_next(kind)) + .unwrap_or_default() + } + + // ============================================================ + // BASIC MOVEMENT + // ============================================================ + + #[gpui::test] + fn test_left_at_start_of_content(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_left_moves_by_grapheme(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 3); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 2.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_left_collapses_selection_to_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", (1, 4)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 1.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_left_stops_at_end_of_line(cx: &mut TestAppContext) { + // "ab\ncd" - cursor at position 3 (start of "cd", after newline) + // Pressing left should move to position 2 (end of "ab", before newline) + let view = create_test_input(cx, "ab\ncd", 3); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 2.into()); // cursor at end of line 1 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_right_at_end_of_content(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_right_moves_by_grapheme(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 2); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 3.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_right_collapses_selection_to_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", (1, 4)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 4.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_right_stops_at_end_of_line(cx: &mut TestAppContext) { + // "ab\ncd" - cursor at position 1 (after 'a') + // Pressing right should move to position 2 (end of "ab", before newline) + let view = create_test_input(cx, "ab\ncd", 1); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 2.into()); // cursor at end of line 1 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_right_crosses_newline(cx: &mut TestAppContext) { + // "ab\ncd" - cursor at position 2 (end of "ab", before newline) + // Pressing right should move to position 3 (after newline, start of "cd") + let view = create_test_input(cx, "ab\ncd", 2); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 3.into()); // cursor at start of line 2 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_left_crosses_newline(cx: &mut TestAppContext) { + // "ab\ncd" - cursor at position 2 (end of "ab", before newline) + // Pressing left should move to position 1 (after 'a') + let view = create_test_input(cx, "ab\ncd", 2); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 1.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_home_moves_to_line_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "first\nsecond", 9); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_line_start(&NavLineStart, window, cx); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_end_moves_to_line_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "first\nsecond", 8); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_line_end(&NavLineEnd, window, cx); + assert_eq!(input.selected_range, 12.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_move_to_beginning(cx: &mut TestAppContext) { + let view = create_test_input(cx, "first\nsecond\nthird", 9); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_start(&NavDocumentStart, window, cx); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_move_to_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "first\nsecond\nthird", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_end(&NavDocumentEnd, window, cx); + assert_eq!(input.selected_range, 18.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // WORD MOVEMENT + // ============================================================ + + #[gpui::test] + fn test_word_left_at_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left_word(&NavWordLeft, window, cx); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_word_left_stops_at_boundary(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world test", 11); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left_word(&NavWordLeft, window, cx); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_word_right_at_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 11); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right_word(&NavWordRight, window, cx); + assert_eq!(input.selected_range, 11.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_word_right_stops_at_boundary(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world test", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right_word(&NavWordRight, window, cx); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // SELECTION + // ============================================================ + + #[gpui::test] + fn test_select_left_extends_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 3); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_left(&SelectLeft, window, cx); + assert_eq!(input.selected_range, (2, 3).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_select_right_extends_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 2..2); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_right(&SelectRight, window, cx); + assert_eq!(input.selected_range, (3, 2).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_select_all(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello\nworld", 3); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_all(&SelectAll, window, cx); + assert_eq!(input.selected_range, (0, 11).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_select_to_beginning(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 6); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_start(&SelectDocumentStart, window, cx); + assert_eq!(input.selected_range, (0, 6).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_select_to_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 6); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_end(&SelectDocumentEnd, window, cx); + assert_eq!(input.selected_range, (11, 6).into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // EDITING - BACKSPACE + // ============================================================ + + #[gpui::test] + fn test_backspace_deletes_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (6, 11)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "hello "); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_deletes_previous_grapheme(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "hell"); + assert_eq!(input.selected_range, 4.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_at_start_does_nothing(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "hello"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_deletes_entire_emoji(cx: &mut TestAppContext) { + let view = create_test_input(cx, "Hi 👋", 7); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "Hi "); + assert_eq!(input.selected_range, 3.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // EDITING - DELETE + // ============================================================ + + #[gpui::test] + fn test_delete_deletes_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_deletes_next_grapheme(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), "ello"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_at_end_does_nothing(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), "hello"); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // EDITING - ENTER + // ============================================================ + + #[gpui::test] + fn test_enter_inserts_newline(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.layout_data.supports_multiline = true; + input.insert_enter(&Enter, window, cx); + assert_eq!(input.as_str(), "hello\n world"); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_enter_replaces_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (5, 6)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.layout_data.supports_multiline = true; + input.insert_enter(&Enter, window, cx); + assert_eq!(input.as_str(), "hello\nworld"); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // CLIPBOARD + // ============================================================ + + #[gpui::test] + fn test_copy_with_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (6, 11)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.copy(&Copy, window, cx); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert!(clipboard.is_some()); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("world")); + } + + #[gpui::test] + fn test_cut_with_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("hello")); + } + + #[gpui::test] + fn test_paste_inserts_text(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + cx.write_to_clipboard(ClipboardItem::new_string(" there".to_string())); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.paste(&Paste, window, cx); + assert_eq!(input.as_str(), "hello there world"); + assert_eq!(input.selected_range, 11.into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // UNICODE / GRAPHEME HANDLING + // ============================================================ + + #[gpui::test] + fn test_movement_with_multibyte_utf8(cx: &mut TestAppContext) { + let view = create_test_input(cx, "café", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 1.into()); + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 2.into()); + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 3.into()); + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_movement_with_emoji(cx: &mut TestAppContext) { + let view = create_test_input(cx, "a👋b", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 1.into()); + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 5.into()); + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 6.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_selection_with_multibyte_characters(cx: &mut TestAppContext) { + let view = create_test_input(cx, "日本語", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_right(&SelectRight, window, cx); + assert_eq!(input.selected_range, (3, 0).into()); + input.select_right(&SelectRight, window, cx); + assert_eq!(input.selected_range, (6, 0).into()); + input.select_right(&SelectRight, window, cx); + assert_eq!(input.selected_range, (9, 0).into()); + }); + }) + .unwrap(); + } + + // ============================================================ + // NEWLINE HANDLING + // ============================================================ + + #[gpui::test] + fn test_find_line_start_and_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "first\nsecond\nthird", 0); + view.update(cx, |view, _window, cx| { + view.input.update(cx, |input, _cx| { + use NavigationDirection::*; + use TextBoundary::*; + let storage = &input.storage; + + assert_eq!(storage.offset_from_caret(0, Back, Line), 0); + assert_eq!(storage.offset_from_caret(3, Back, Line), 0); + assert_eq!(storage.offset_from_caret(6, Back, Line), 6); + assert_eq!(storage.offset_from_caret(13, Back, Line), 13); + + assert_eq!(storage.offset_from_caret(0, Forward, Line), 5); + assert_eq!(storage.offset_from_caret(6, Forward, Line), 12); + assert_eq!(storage.offset_from_caret(13, Forward, Line), 18); + }); + }) + .unwrap(); + } + + // ============================================================ + // EDGE CASES + // ============================================================ + + #[gpui::test] + fn test_operations_on_empty_content(cx: &mut TestAppContext) { + let view = create_test_input(cx, "", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range, 0.into()); + + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range, 0.into()); + + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), ""); + + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), ""); + + input.select_all(&SelectAll, window, cx); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_set_content_resets_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (3, 8)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.marked_range = Some(5..7); + input.replace_text_in_range(Some(0..11), "new content", window, cx); + assert_eq!(input.as_str(), "new content"); + assert_eq!(input.selected_range, 11.into()); + assert_eq!(input.marked_range, None); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_cursor_clamped_to_content_length(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 100); + view.update(cx, |view, _window, cx| { + view.input.update(cx, |input, cx| { + input.move_to(1000, cx); + assert_eq!(input.selected_range, 5.into()); + + input.selected_range = 0.into(); + input.select_to(1000, cx); + assert_eq!(input.selected_range, (5, 0).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_previous_boundary_at_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, _window, cx| { + view.input.update(cx, |input, _cx| { + use NavigationDirection::*; + use TextBoundary::*; + assert_eq!(input.storage.offset_from_caret(0, Back, Graphmeme), 0); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_next_boundary_at_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, _window, cx| { + view.input.update(cx, |input, _cx| { + use NavigationDirection::*; + use TextBoundary::*; + let storage = &input.storage; + assert_eq!(storage.offset_from_caret(5, Forward, Graphmeme), 5); + assert_eq!(storage.offset_from_caret(100, Forward, Graphmeme), 5); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_word_range_at_boundary(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 0); + view.update(cx, |view, _window, cx| { + view.input.update(cx, |input, _cx| { + let range = input.storage.word_range_at(5); + assert_eq!(range.start, 0); + assert_eq!(range.end, 5); + + let range = input.storage.word_range_at(8); + assert_eq!(range.start, 6); + assert_eq!(range.end, 11); + }); + }) + .unwrap(); + } + + // ============================================================ + // EMOJI & GRAPHEME CLUSTERS + // ============================================================ + + #[gpui::test] + fn test_simple_emoji_navigation(cx: &mut TestAppContext) { + // 😀 is 4 bytes in UTF-8 + let view = create_test_input(cx, "a😀b", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + // Move right through: a -> 😀 -> b + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range.start, 1); // after 'a' + + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range.start, 5); // after 😀 (1 + 4 bytes) + + input.nav_right(&NavRight, window, cx); + assert_eq!(input.selected_range.start, 6); // after 'b' + + // Move left back + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range.start, 5); // before 'b' + + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range.start, 1); // before 😀 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_emoji_with_skin_tone_modifier(cx: &mut TestAppContext) { + // 👋🏽 = 👋 (U+1F44B, 4 bytes) + 🏽 (U+1F3FD, 4 bytes) = 8 bytes total + let emoji = "👋🏽"; + assert_eq!(emoji.len(), 8); + + let view = create_test_input(cx, &format!("a{}b", emoji), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'a' + assert_eq!(input.selected_range.start, 1); + + input.nav_right(&NavRight, window, cx); // past entire emoji with modifier + assert_eq!(input.selected_range.start, 9); // 1 + 8 + + input.nav_left(&NavLeft, window, cx); // back before emoji + assert_eq!(input.selected_range.start, 1); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_zwj_family_emoji(cx: &mut TestAppContext) { + // 👨‍👩‍👧 = man + ZWJ + woman + ZWJ + girl + // Each person emoji is 4 bytes, ZWJ is 3 bytes + // Total: 4 + 3 + 4 + 3 + 4 = 18 bytes + let family = "👨‍👩‍👧"; + assert_eq!(family.len(), 18); + + let view = create_test_input(cx, &format!("x{}y", family), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'x' + assert_eq!(input.selected_range.start, 1); + + input.nav_right(&NavRight, window, cx); // past entire ZWJ sequence + assert_eq!(input.selected_range.start, 19); // 1 + 18 + + input.nav_right(&NavRight, window, cx); // past 'y' + assert_eq!(input.selected_range.start, 20); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_deletes_emoji_between_ascii(cx: &mut TestAppContext) { + let view = create_test_input(cx, "a😀b", 5); // cursor after emoji + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "ab"); + assert_eq!(input.selected_range.start, 1); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_deletes_zwj_sequence(cx: &mut TestAppContext) { + let family = "👨‍👩‍👧"; + let content = format!("a{}b", family); + let cursor_pos = 1 + family.len(); // after the family emoji + + let view = create_test_input(cx, &content, cursor_pos..cursor_pos); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "ab"); + assert_eq!(input.selected_range.start, 1); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_removes_entire_emoji(cx: &mut TestAppContext) { + let view = create_test_input(cx, "a😀b", 1); // cursor before emoji + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), "ab"); + assert_eq!(input.selected_range.start, 1); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_flag_emoji_navigation(cx: &mut TestAppContext) { + // 🇯🇵 = Regional Indicator J (4 bytes) + Regional Indicator P (4 bytes) + let flag = "🇯🇵"; + assert_eq!(flag.len(), 8); + + let view = create_test_input(cx, &format!("x{}y", flag), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'x' + input.nav_right(&NavRight, window, cx); // past flag (should be single grapheme) + assert_eq!(input.selected_range.start, 9); // 1 + 8 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_combining_diacritical_marks(cx: &mut TestAppContext) { + // é as e + combining acute accent (U+0301) + let combining = "e\u{0301}"; // 1 + 2 = 3 bytes + assert_eq!(combining.len(), 3); + + let view = create_test_input(cx, &format!("a{}b", combining), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'a' + assert_eq!(input.selected_range.start, 1); + + input.nav_right(&NavRight, window, cx); // past e + combining mark (single grapheme) + assert_eq!(input.selected_range.start, 4); // 1 + 3 + + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range.start, 1); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_multiple_combining_marks(cx: &mut TestAppContext) { + // ë́ = e + combining diaeresis (U+0308) + combining acute (U+0301) + let multi_combining = "e\u{0308}\u{0301}"; // 1 + 2 + 2 = 5 bytes + assert_eq!(multi_combining.len(), 5); + + let view = create_test_input(cx, &format!("x{}y", multi_combining), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'x' + input.nav_right(&NavRight, window, cx); // past entire combined character + assert_eq!(input.selected_range.start, 6); // 1 + 5 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_select_emoji_with_shift(cx: &mut TestAppContext) { + let view = create_test_input(cx, "a😀b", 1); // cursor before emoji + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_right(&SelectRight, window, cx); + assert_eq!(input.selected_range, (5, 1).into()); // selected the entire emoji + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_cjk_characters(cx: &mut TestAppContext) { + // 你好 - each character is 3 bytes in UTF-8 + let view = create_test_input(cx, "a你好b", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'a' + assert_eq!(input.selected_range.start, 1); + + input.nav_right(&NavRight, window, cx); // past 你 + assert_eq!(input.selected_range.start, 4); // 1 + 3 + + input.nav_right(&NavRight, window, cx); // past 好 + assert_eq!(input.selected_range.start, 7); // 4 + 3 + + input.nav_right(&NavRight, window, cx); // past 'b' + assert_eq!(input.selected_range.start, 8); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_mixed_script_text(cx: &mut TestAppContext) { + // Mix of ASCII, CJK, and emoji + let view = create_test_input(cx, "Hi你😀", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'H' + assert_eq!(input.selected_range.start, 1); + + input.nav_right(&NavRight, window, cx); // past 'i' + assert_eq!(input.selected_range.start, 2); + + input.nav_right(&NavRight, window, cx); // past 你 (3 bytes) + assert_eq!(input.selected_range.start, 5); + + input.nav_right(&NavRight, window, cx); // past 😀 (4 bytes) + assert_eq!(input.selected_range.start, 9); + + // Now go back + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range.start, 5); + + input.nav_left(&NavLeft, window, cx); + assert_eq!(input.selected_range.start, 2); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_variation_selector_emoji(cx: &mut TestAppContext) { + // ☺️ = ☺ (U+263A, 3 bytes) + variation selector-16 (U+FE0F, 3 bytes) + let emoji_presentation = "☺\u{FE0F}"; + assert_eq!(emoji_presentation.len(), 6); + + let view = create_test_input(cx, &format!("a{}b", emoji_presentation), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'a' + input.nav_right(&NavRight, window, cx); // past emoji with variation selector + assert_eq!(input.selected_range.start, 7); // 1 + 6 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_keycap_emoji(cx: &mut TestAppContext) { + // 1️⃣ = 1 + variation selector + combining enclosing keycap + let keycap = "1\u{FE0F}\u{20E3}"; + + let view = create_test_input(cx, &format!("x{}y", keycap), 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_right(&NavRight, window, cx); // past 'x' + input.nav_right(&NavRight, window, cx); // past keycap sequence + let expected_pos = 1 + keycap.len(); + assert_eq!(input.selected_range.start, expected_pos); + }); + }) + .unwrap(); + } + + // Single-line input tests + + fn create_single_line_input( + cx: &mut TestAppContext, + content: &str, + selected_range: impl Into, + ) -> WindowHandle { + cx.add_window(|_window, cx| { + let input = cx.new(|cx| { + let mut input = default_state(content, cx); + input.selected_range = selected_range.into(); + input + }); + TestView { input } + }) + } + + #[gpui::test] + fn test_single_line_enter_does_nothing(cx: &mut TestAppContext) { + let view = create_single_line_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.insert_enter(&Enter, window, cx); + assert_eq!(input.as_str(), "hello"); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_single_line_up_moves_to_start(cx: &mut TestAppContext) { + let view = create_single_line_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_up(&NavUp, window, cx); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_single_line_down_moves_to_end(cx: &mut TestAppContext) { + let view = create_single_line_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.nav_down(&NavDown, window, cx); + assert_eq!(input.selected_range, 11.into()); // "hello world".len() == 11 + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_single_line_select_up_selects_to_start(cx: &mut TestAppContext) { + let view = create_single_line_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_up(&SelectUp, window, cx); + assert_eq!(input.selected_range, (0, 5).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_single_line_select_down_selects_to_end(cx: &mut TestAppContext) { + let view = create_single_line_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.select_down(&SelectDown, window, cx); + assert_eq!(input.selected_range, (11, 5).into()); // "hello world".len() == 11 + }); + }) + .unwrap(); + } + + // ============================================================ + // UNDO / REDO + // ============================================================ + + #[gpui::test] + fn test_undo_restores_content(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + // Make an edit + input.replace_text_in_range(None, " world", window, cx); + assert_eq!(input.as_str(), "hello world"); + + // Undo should restore original content + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_redo_restores_undone_content(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.replace_text_in_range(None, " world", window, cx); + assert_eq!(input.as_str(), "hello world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + + input.redo(&Redo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_undo_with_no_history_does_nothing(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + assert!(!is_history_kind_available(input, HistoryKind::Undo)); + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_redo_with_no_history_does_nothing(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + assert!(!is_history_kind_available(input, HistoryKind::Redo)); + input.redo(&Redo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_undo_restores_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + // Delete selection + input.replace_text_in_range(None, "", window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + + // Undo should restore content and selection + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + assert_eq!(input.selected_range, (0, 5).into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_multiple_undo_redo(cx: &mut TestAppContext) { + let view = create_test_input(cx, "", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.replace_text_in_range(None, "a", window, cx); + input.replace_text_in_range(None, "b", window, cx); + input.replace_text_in_range(None, "c", window, cx); + assert_eq!(input.as_str(), "abc"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "ab"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "a"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), ""); + + input.redo(&Redo, window, cx); + assert_eq!(input.as_str(), "a"); + + input.redo(&Redo, window, cx); + assert_eq!(input.as_str(), "ab"); + + input.redo(&Redo, window, cx); + assert_eq!(input.as_str(), "abc"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_new_edit_clears_redo_stack(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.replace_text_in_range(None, " world", window, cx); + assert_eq!(input.as_str(), "hello world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + assert!(is_history_kind_available(input, HistoryKind::Redo)); + + // New edit should clear redo stack + input.replace_text_in_range(None, "!", window, cx); + assert_eq!(input.as_str(), "hello!"); + assert!(!is_history_kind_available(input, HistoryKind::Redo)); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_can_undo_can_redo(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + assert!(!is_history_kind_available(input, HistoryKind::Undo)); + assert!(!is_history_kind_available(input, HistoryKind::Redo)); + + input.replace_text_in_range(None, "!", window, cx); + assert!(is_history_kind_available(input, HistoryKind::Undo)); + assert!(!is_history_kind_available(input, HistoryKind::Redo)); + + input.undo(&Undo, window, cx); + assert!(!is_history_kind_available(input, HistoryKind::Undo)); + assert!(is_history_kind_available(input, HistoryKind::Redo)); + + input.redo(&Redo, window, cx); + assert!(is_history_kind_available(input, HistoryKind::Undo)); + assert!(!is_history_kind_available(input, HistoryKind::Redo)); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_backspace_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_left(&DeleteLeft, window, cx); + assert_eq!(input.as_str(), "hell"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_right(&DeleteRight, window, cx); + assert_eq!(input.as_str(), "ello"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_cut_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), " world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_cut_line_with_no_selection(cx: &mut TestAppContext) { + // Cursor in middle line, no selection - should cut entire line including newline + let view = create_test_input(cx, "line1\nline2\nline3", 8); // cursor in "line2" + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), "line1\nline3"); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("line2\n")); + } + + #[gpui::test] + fn test_cut_first_line_with_no_selection(cx: &mut TestAppContext) { + // Cursor on first line, no selection + let view = create_test_input(cx, "line1\nline2\nline3", 2); // cursor in "line1" + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), "line2\nline3"); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("line1\n")); + } + + #[gpui::test] + fn test_cut_last_line_with_no_selection(cx: &mut TestAppContext) { + // Cursor on last line, no selection - should include preceding newline + let view = create_test_input(cx, "line1\nline2\nline3", 14); // cursor in "line3" + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), "line1\nline2"); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("\nline3")); + } + + #[gpui::test] + fn test_cut_empty_line(cx: &mut TestAppContext) { + // Cursor on empty line - should remove that line + let view = create_test_input(cx, "line1\n\nline3", 6); // cursor on empty line + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), "line1\nline3"); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("\n")); + } + + #[gpui::test] + fn test_cut_only_line_with_no_selection(cx: &mut TestAppContext) { + // Single line content, no selection - should cut entire content + let view = create_test_input(cx, "hello", 2); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), ""); + }); + }) + .unwrap(); + + let clipboard = cx.read_from_clipboard(); + assert_eq!(clipboard.unwrap().text().as_deref(), Some("hello")); + } + + #[gpui::test] + fn test_cut_line_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "line1\nline2\nline3", 8); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.cut(&Cut, window, cx); + assert_eq!(input.as_str(), "line1\nline3"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "line1\nline2\nline3"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_paste_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello", 5); + cx.write_to_clipboard(ClipboardItem::new_string(" world".to_string())); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.paste(&Paste, window, cx); + assert_eq!(input.as_str(), "hello world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_enter_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + input.layout_data.supports_multiline = true; + + input.insert_enter(&Enter, window, cx); + assert_eq!(input.as_str(), "hello\n world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_left(cx: &mut TestAppContext) { + // Cursor at end of "hello" in "hello world" + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_left(&DeleteWordLeft, window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_left_with_selection(cx: &mut TestAppContext) { + // Selection from 0 to 5 ("hello") + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_left(&DeleteWordLeft, window, cx); + assert_eq!(input.as_str(), " world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_left_at_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_left(&DeleteWordLeft, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_right(cx: &mut TestAppContext) { + // Cursor at start + let view = create_test_input(cx, "hello world", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_right(&DeleteWordRight, window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_right_with_selection(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", (0, 5)); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_right(&DeleteWordRight, window, cx); + assert_eq!(input.as_str(), " world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_right_at_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 11); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_word_right(&DeleteWordRight, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_start(&DeleteToLineStart, window, cx); + assert_eq!(input.as_str(), " world"); + assert_eq!(input.selected_range, 0.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_beginning_of_line_multiline(cx: &mut TestAppContext) { + // Cursor at position 8 (middle of "line2") + let view = create_test_input(cx, "line1\nline2\nline3", 8); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_start(&DeleteToLineStart, window, cx); + assert_eq!(input.as_str(), "line1\nne2\nline3"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_beginning_of_line_at_start(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 0); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_start(&DeleteToLineStart, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_end_of_line(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_end(&DeleteToLineEnd, window, cx); + assert_eq!(input.as_str(), "hello"); + assert_eq!(input.selected_range, 5.into()); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_end_of_line_multiline(cx: &mut TestAppContext) { + // Cursor at position 8 (middle of "line2") + let view = create_test_input(cx, "line1\nline2\nline3", 8); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_end(&DeleteToLineEnd, window, cx); + assert_eq!(input.as_str(), "line1\nli\nline3"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_end_of_line_at_end(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 11); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + input.delete_to_line_end(&DeleteToLineEnd, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_left_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_word_left(&DeleteWordLeft, window, cx); + assert_eq!(input.as_str(), " world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_word_right_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 6); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_word_right(&DeleteWordRight, window, cx); + assert_eq!(input.as_str(), "hello "); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_beginning_of_line_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_to_line_start(&DeleteToLineStart, window, cx); + assert_eq!(input.as_str(), " world"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } + + #[gpui::test] + fn test_delete_to_end_of_line_is_undoable(cx: &mut TestAppContext) { + let view = create_test_input(cx, "hello world", 5); + view.update(cx, |view, window, cx| { + view.input.update(cx, |input, cx| { + without_history_grouping(input); + + input.delete_to_line_end(&DeleteToLineEnd, window, cx); + assert_eq!(input.as_str(), "hello"); + + input.undo(&Undo, window, cx); + assert_eq!(input.as_str(), "hello world"); + }); + }) + .unwrap(); + } } diff --git a/crates/gpui_elements/src/editable_text/storage.rs b/crates/gpui_elements/src/editable_text/storage.rs index 24b4d0307c..9cf58465ec 100644 --- a/crates/gpui_elements/src/editable_text/storage.rs +++ b/crates/gpui_elements/src/editable_text/storage.rs @@ -4,202 +4,202 @@ use unicode_segmentation::UnicodeSegmentation; /// Describes a boundary within a chunk of text. pub enum TextBoundary { - /// The utf-8 character - Graphmeme, - /// The current word (using whitespace as delimiters) - Word, - /// The current line - Line, - /// The entire document - Document, + /// The utf-8 character + Graphmeme, + /// The current word (using whitespace as delimiters) + Word, + /// The current line + Line, + /// The entire document + Document, } /// Implement this trait to create a storage medium that can be used as the content of EditableText elements. /// Default implementation is [`StringStorage`]. pub trait UnicodeTextStorage { - /// Returns the version/generation of the content, which should be incremented ever time the - /// content is changed so that rendering elements can reprocess the contents via the text layout engine. - fn version(&self) -> u16; + /// Returns the version/generation of the content, which should be incremented ever time the + /// content is changed so that rendering elements can reprocess the contents via the text layout engine. + fn version(&self) -> u16; - /// Returns a reference to the utf8 string. - fn content_utf8(&self) -> &str; + /// Returns a reference to the utf8 string. + fn content_utf8(&self) -> &str; - /// Returns the UTF-16 length of the content. - fn len_utf16(&self) -> usize; + /// Returns the UTF-16 length of the content. + fn len_utf16(&self) -> usize; - /// Replace contents within the provided range with the given str slice. - fn replace_range(&mut self, range: Range, text: &str); + /// Replace contents within the provided range with the given str slice. + fn replace_range(&mut self, range: Range, text: &str); - /// Returns the utf16 position equivalent of the provided utf8 character position. - fn utf_offset_8to16(&self, pos_uft8: usize) -> usize { - // Fast path: if offset is 0, return 0 - if pos_uft8 == 0 { - return 0; - } + /// Returns the utf16 position equivalent of the provided utf8 character position. + fn utf_offset_8to16(&self, pos_uft8: usize) -> usize { + // Fast path: if offset is 0, return 0 + if pos_uft8 == 0 { + return 0; + } - // Fast path: if offset is at or past end, return cached length - if pos_uft8 >= self.content_utf8().len() { - return self.len_utf16(); - } + // Fast path: if offset is at or past end, return cached length + if pos_uft8 >= self.content_utf8().len() { + return self.len_utf16(); + } - let mut count_utf16 = 0; - for (idx, character) in self.content_utf8().char_indices() { - if idx >= pos_uft8 { - break; - } - count_utf16 += character.len_utf16(); - } - count_utf16 - } + let mut count_utf16 = 0; + for (idx, character) in self.content_utf8().char_indices() { + if idx >= pos_uft8 { + break; + } + count_utf16 += character.len_utf16(); + } + count_utf16 + } - /// Returns the utf8 position equivalent of the provided utf16 character position. - fn utf_offset_16to8(&self, pos_utf16: usize) -> usize { - // Fast path: if offset is 0, return 0 - if pos_utf16 == 0 { - return 0; - } + /// Returns the utf8 position equivalent of the provided utf16 character position. + fn utf_offset_16to8(&self, pos_utf16: usize) -> usize { + // Fast path: if offset is 0, return 0 + if pos_utf16 == 0 { + return 0; + } - let mut count_utf16 = 0; - for (idx, character) in self.content_utf8().char_indices() { - if count_utf16 >= pos_utf16 { - return idx; - } - count_utf16 += character.len_utf16(); - } - self.content_utf8().len() - } + let mut count_utf16 = 0; + for (idx, character) in self.content_utf8().char_indices() { + if count_utf16 >= pos_utf16 { + return idx; + } + count_utf16 += character.len_utf16(); + } + self.content_utf8().len() + } - /// Converts a utf8 character range into a utf16 character range. - fn utf_range_8to16(&self, range_utf8: &Range) -> Range { - self.utf_offset_8to16(range_utf8.start)..self.utf_offset_8to16(range_utf8.end) - } + /// Converts a utf8 character range into a utf16 character range. + fn utf_range_8to16(&self, range_utf8: &Range) -> Range { + self.utf_offset_8to16(range_utf8.start)..self.utf_offset_8to16(range_utf8.end) + } - /// Converts a utf16 character range into a utf8 character range. - fn utf_range_16to8(&self, range_utf16: &Range) -> Range { - self.utf_offset_16to8(range_utf16.start)..self.utf_offset_16to8(range_utf16.end) - } + /// Converts a utf16 character range into a utf8 character range. + fn utf_range_16to8(&self, range_utf16: &Range) -> Range { + self.utf_offset_16to8(range_utf16.start)..self.utf_offset_16to8(range_utf16.end) + } - /// Builds a utf8 character range based on a caret position within the storage, - /// the direction to traverse, and the boundary to stop at. - /// The start of the range will be the earlier position (destination if Back, caret if Forward), - /// and the end will be the later position (caret if Back, destination if Forward). - fn range_from_caret( - &self, - caret: usize, - direction: NavigationDirection, - magnitude: TextBoundary, - ) -> Range { - let offset = self.offset_from_caret(caret, direction, magnitude); - match direction { - NavigationDirection::Back => offset..caret, - NavigationDirection::Forward => caret..offset, - } - } + /// Builds a utf8 character range based on a caret position within the storage, + /// the direction to traverse, and the boundary to stop at. + /// The start of the range will be the earlier position (destination if Back, caret if Forward), + /// and the end will be the later position (caret if Back, destination if Forward). + fn range_from_caret( + &self, + caret: usize, + direction: NavigationDirection, + magnitude: TextBoundary, + ) -> Range { + let offset = self.offset_from_caret(caret, direction, magnitude); + match direction { + NavigationDirection::Back => offset..caret, + NavigationDirection::Forward => caret..offset, + } + } - /// Finds the next location from the caret based on the direction to traverse and the boundary to stop at. - fn offset_from_caret( - &self, - caret: usize, - direction: NavigationDirection, - boundary: TextBoundary, - ) -> usize { - use NavigationDirection::*; - use TextBoundary::*; - match (direction, boundary) { - (Back, Graphmeme) => { - if caret == 0 { - return 0; - } + /// Finds the next location from the caret based on the direction to traverse and the boundary to stop at. + fn offset_from_caret( + &self, + caret: usize, + direction: NavigationDirection, + boundary: TextBoundary, + ) -> usize { + use NavigationDirection::*; + use TextBoundary::*; + match (direction, boundary) { + (Back, Graphmeme) => { + if caret == 0 { + return 0; + } - let str = self.content_utf8(); - let iter = str[..caret.min(str.len())].grapheme_indices(true); - iter.map(|(i, _)| i).next_back().unwrap_or(0) - } - (Forward, Graphmeme) => { - let str = self.content_utf8(); - let len_utf8 = str.len(); - if caret >= len_utf8 { - return len_utf8; - } + let str = self.content_utf8(); + let iter = str[..caret.min(str.len())].grapheme_indices(true); + iter.map(|(i, _)| i).next_back().unwrap_or(0) + } + (Forward, Graphmeme) => { + let str = self.content_utf8(); + let len_utf8 = str.len(); + if caret >= len_utf8 { + return len_utf8; + } - let mut iter = str[caret..].grapheme_indices(true); - iter.nth(1).map(|(i, _)| caret + i).unwrap_or(len_utf8) - } - (Back, Word) => { - if caret == 0 { - return 0; - } + let mut iter = str[caret..].grapheme_indices(true); + iter.nth(1).map(|(i, _)| caret + i).unwrap_or(len_utf8) + } + (Back, Word) => { + if caret == 0 { + return 0; + } - let str = self.content_utf8(); - let str = &str[..caret.min(str.len())]; + let str = self.content_utf8(); + let str = &str[..caret.min(str.len())]; - let mut last_word_start = 0; - for (idx, _) in str.unicode_word_indices() { - if idx < caret { - last_word_start = idx; - } - } + let mut last_word_start = 0; + for (idx, _) in str.unicode_word_indices() { + if idx < caret { + last_word_start = idx; + } + } - if last_word_start == 0 && caret > 0 { - let trimmed = str.trim_end(); - if trimmed.is_empty() { - return 0; - } - for (idx, _) in trimmed.unicode_word_indices() { - last_word_start = idx; - } - } + if last_word_start == 0 && caret > 0 { + let trimmed = str.trim_end(); + if trimmed.is_empty() { + return 0; + } + for (idx, _) in trimmed.unicode_word_indices() { + last_word_start = idx; + } + } - last_word_start - } - (Forward, Word) => { - let str = self.content_utf8(); - let len_utf8 = str.len(); - if caret >= len_utf8 { - return len_utf8; - } + last_word_start + } + (Forward, Word) => { + let str = self.content_utf8(); + let len_utf8 = str.len(); + if caret >= len_utf8 { + return len_utf8; + } - let str = &str[caret..]; - for (idx, word) in str.unicode_word_indices() { - let word_end = caret + idx + word.len(); - if word_end > caret { - return word_end; - } - } - len_utf8 - } - // Returns the utf-8 character position of first character after the first new-line - // preceding the character at the provided utf-8 character position. - (Back, Line) => { - let str = self.content_utf8(); - let iter = str[..caret.min(str.len())].rfind('\n'); - iter.map(|pos| pos + 1).unwrap_or(0) - } - // Returns the utf-8 character position of the character immediately before the first - // new-line character after the character at the provided utf-8 character position. - (Forward, Line) => { - let str = self.content_utf8(); - let iter = str[caret.min(str.len())..].find('\n'); - iter.map(|pos| caret + pos).unwrap_or(str.len()) - } - (Back, Document) => 0, - (Forward, Document) => self.content_utf8().len(), - } - } + let str = &str[caret..]; + for (idx, word) in str.unicode_word_indices() { + let word_end = caret + idx + word.len(); + if word_end > caret { + return word_end; + } + } + len_utf8 + } + // Returns the utf-8 character position of first character after the first new-line + // preceding the character at the provided utf-8 character position. + (Back, Line) => { + let str = self.content_utf8(); + let iter = str[..caret.min(str.len())].rfind('\n'); + iter.map(|pos| pos + 1).unwrap_or(0) + } + // Returns the utf-8 character position of the character immediately before the first + // new-line character after the character at the provided utf-8 character position. + (Forward, Line) => { + let str = self.content_utf8(); + let iter = str[caret.min(str.len())..].find('\n'); + iter.map(|pos| caret + pos).unwrap_or(str.len()) + } + (Back, Document) => 0, + (Forward, Document) => self.content_utf8().len(), + } + } - /// Returns the start and end of the word the position resides within. - fn word_range_at(&self, position: usize) -> Range { - let offset = position.min(self.content_utf8().len()); + /// Returns the start and end of the word the position resides within. + fn word_range_at(&self, position: usize) -> Range { + let offset = position.min(self.content_utf8().len()); - for (idx, word) in self.content_utf8().unicode_word_indices() { - let word_end = idx + word.len(); - if offset >= idx && offset <= word_end { - return idx..word_end; - } - } + for (idx, word) in self.content_utf8().unicode_word_indices() { + let word_end = idx + word.len(); + if offset >= idx && offset <= word_end { + return idx..word_end; + } + } - offset..offset - } + offset..offset + } } /// [`UnicodeTextStorage`] implementation for [`String`]. @@ -207,35 +207,35 @@ pub trait UnicodeTextStorage { /// Its a decent default for editable text fields though. #[derive(Clone, Default)] pub struct StringStorage { - value: String, - version: u16, + value: String, + version: u16, } impl From for StringStorage where - S: Into, + S: Into, { - fn from(value: S) -> Self { - Self { - value: value.into(), - version: u16::default(), - } - } + fn from(value: S) -> Self { + Self { + value: value.into(), + version: u16::default(), + } + } } impl UnicodeTextStorage for StringStorage { - fn version(&self) -> u16 { - self.version - } + fn version(&self) -> u16 { + self.version + } - fn content_utf8(&self) -> &str { - self.value.as_str() - } + fn content_utf8(&self) -> &str { + self.value.as_str() + } - fn len_utf16(&self) -> usize { - self.value.chars().map(|c| c.len_utf16()).sum() - } + fn len_utf16(&self) -> usize { + self.value.chars().map(|c| c.len_utf16()).sum() + } - fn replace_range(&mut self, range: Range, text: &str) { - self.value.replace_range(range, &text); - self.version = self.version.wrapping_add(1); - } + fn replace_range(&mut self, range: Range, text: &str) { + self.value.replace_range(range, &text); + self.version = self.version.wrapping_add(1); + } } diff --git a/crates/gpui_linux/src/linux.rs b/crates/gpui_linux/src/linux.rs index bafdc2e524..4c0580ed23 100644 --- a/crates/gpui_linux/src/linux.rs +++ b/crates/gpui_linux/src/linux.rs @@ -27,31 +27,31 @@ use std::rc::Rc; /// Returns the default platform implementation for the current OS. pub fn current_platform(headless: bool) -> Rc { - #[cfg(feature = "x11")] - use anyhow::Context as _; + #[cfg(feature = "x11")] + use anyhow::Context as _; - if headless { - return Rc::new(LinuxPlatform { - inner: HeadlessClient::new(), - }); - } + if headless { + return Rc::new(LinuxPlatform { + inner: HeadlessClient::new(), + }); + } - match gpui::guess_compositor() { - #[cfg(feature = "wayland")] - "Wayland" => Rc::new(LinuxPlatform { - inner: WaylandClient::new(), - }), + match gpui::guess_compositor() { + #[cfg(feature = "wayland")] + "Wayland" => Rc::new(LinuxPlatform { + inner: WaylandClient::new(), + }), - #[cfg(feature = "x11")] - "X11" => Rc::new(LinuxPlatform { - inner: X11Client::new() - .context("Failed to initialize X11 client.") - .unwrap(), - }), + #[cfg(feature = "x11")] + "X11" => Rc::new(LinuxPlatform { + inner: X11Client::new() + .context("Failed to initialize X11 client.") + .unwrap(), + }), - "Headless" => Rc::new(LinuxPlatform { - inner: HeadlessClient::new(), - }), - _ => unreachable!(), - } + "Headless" => Rc::new(LinuxPlatform { + inner: HeadlessClient::new(), + }), + _ => unreachable!(), + } } diff --git a/crates/gpui_linux/src/linux/dispatcher.rs b/crates/gpui_linux/src/linux/dispatcher.rs index 22df5799dd..f92533c37e 100644 --- a/crates/gpui_linux/src/linux/dispatcher.rs +++ b/crates/gpui_linux/src/linux/dispatcher.rs @@ -1,244 +1,244 @@ use calloop::{ - EventLoop, PostAction, - channel::{self, Sender}, - timer::TimeoutAction, + EventLoop, PostAction, + channel::{self, Sender}, + timer::TimeoutAction, }; use util::ResultExt; use std::{ - mem::MaybeUninit, - thread, - time::{Duration, Instant}, + mem::MaybeUninit, + thread, + time::{Duration, Instant}, }; use gpui::{ - GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver, - PriorityQueueSender, RunnableVariant, TaskTiming, ThreadTaskTimings, profiler, + GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver, + PriorityQueueSender, RunnableVariant, TaskTiming, ThreadTaskTimings, profiler, }; struct TimerAfter { - duration: Duration, - runnable: RunnableVariant, + duration: Duration, + runnable: RunnableVariant, } pub(crate) struct LinuxDispatcher { - main_sender: PriorityQueueCalloopSender, - timer_sender: Sender, - background_sender: PriorityQueueSender, - _background_threads: Vec>, - main_thread_id: thread::ThreadId, + main_sender: PriorityQueueCalloopSender, + timer_sender: Sender, + background_sender: PriorityQueueSender, + _background_threads: Vec>, + main_thread_id: thread::ThreadId, } const MIN_THREADS: usize = 2; impl LinuxDispatcher { - pub fn new(main_sender: PriorityQueueCalloopSender) -> Self { - let (background_sender, background_receiver) = PriorityQueueReceiver::new(); - let thread_count = - std::thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS)); + pub fn new(main_sender: PriorityQueueCalloopSender) -> Self { + let (background_sender, background_receiver) = PriorityQueueReceiver::new(); + let thread_count = + std::thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS)); - let mut background_threads = (0..thread_count) - .map(|i| { - let receiver: PriorityQueueReceiver = background_receiver.clone(); - std::thread::Builder::new() - .name(format!("Worker-{i}")) - .spawn(move || { - for runnable in receiver.iter() { - let start = Instant::now(); + let mut background_threads = (0..thread_count) + .map(|i| { + let receiver: PriorityQueueReceiver = background_receiver.clone(); + std::thread::Builder::new() + .name(format!("Worker-{i}")) + .spawn(move || { + for runnable in receiver.iter() { + let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - profiler::add_task_timing(timing); + let location = runnable.metadata().location; + let mut timing = TaskTiming { + location, + start, + end: None, + }; + profiler::add_task_timing(timing); - runnable.run(); + runnable.run(); - let end = Instant::now(); - timing.end = Some(end); - profiler::add_task_timing(timing); + let end = Instant::now(); + timing.end = Some(end); + profiler::add_task_timing(timing); - log::trace!( - "background thread {}: ran runnable. took: {:?}", - i, - start.elapsed() - ); - } - }) - .unwrap() - }) - .collect::>(); + log::trace!( + "background thread {}: ran runnable. took: {:?}", + i, + start.elapsed() + ); + } + }) + .unwrap() + }) + .collect::>(); - let (timer_sender, timer_channel) = calloop::channel::channel::(); - let timer_thread = std::thread::Builder::new() - .name("Timer".to_owned()) - .spawn(move || { - let mut event_loop: EventLoop<()> = - EventLoop::try_new().expect("Failed to initialize timer loop!"); + let (timer_sender, timer_channel) = calloop::channel::channel::(); + let timer_thread = std::thread::Builder::new() + .name("Timer".to_owned()) + .spawn(move || { + let mut event_loop: EventLoop<()> = + EventLoop::try_new().expect("Failed to initialize timer loop!"); - let handle = event_loop.handle(); - let timer_handle = event_loop.handle(); - handle - .insert_source(timer_channel, move |e, _, _| { - if let channel::Event::Msg(timer) = e { - let mut runnable = Some(timer.runnable); - timer_handle - .insert_source( - calloop::timer::Timer::from_duration(timer.duration), - move |_, _, _| { - if let Some(runnable) = runnable.take() { - let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - profiler::add_task_timing(timing); + let handle = event_loop.handle(); + let timer_handle = event_loop.handle(); + handle + .insert_source(timer_channel, move |e, _, _| { + if let channel::Event::Msg(timer) = e { + let mut runnable = Some(timer.runnable); + timer_handle + .insert_source( + calloop::timer::Timer::from_duration(timer.duration), + move |_, _, _| { + if let Some(runnable) = runnable.take() { + let start = Instant::now(); + let location = runnable.metadata().location; + let mut timing = TaskTiming { + location, + start, + end: None, + }; + profiler::add_task_timing(timing); - runnable.run(); - let end = Instant::now(); + runnable.run(); + let end = Instant::now(); - timing.end = Some(end); - profiler::add_task_timing(timing); - } - TimeoutAction::Drop - }, - ) - .expect("Failed to start timer"); - } - }) - .expect("Failed to start timer thread"); + timing.end = Some(end); + profiler::add_task_timing(timing); + } + TimeoutAction::Drop + }, + ) + .expect("Failed to start timer"); + } + }) + .expect("Failed to start timer thread"); - event_loop.run(None, &mut (), |_| {}).log_err(); - }) - .unwrap(); + event_loop.run(None, &mut (), |_| {}).log_err(); + }) + .unwrap(); - background_threads.push(timer_thread); + background_threads.push(timer_thread); - Self { - main_sender, - timer_sender, - background_sender, - _background_threads: background_threads, - main_thread_id: thread::current().id(), - } - } + Self { + main_sender, + timer_sender, + background_sender, + _background_threads: background_threads, + main_thread_id: thread::current().id(), + } + } } impl PlatformDispatcher for LinuxDispatcher { - fn get_all_timings(&self) -> Vec { - let global_timings = GLOBAL_THREAD_TIMINGS.lock(); - ThreadTaskTimings::convert(&global_timings) - } + fn get_all_timings(&self) -> Vec { + let global_timings = GLOBAL_THREAD_TIMINGS.lock(); + ThreadTaskTimings::convert(&global_timings) + } - fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings { - gpui::profiler::get_current_thread_task_timings() - } + fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings { + gpui::profiler::get_current_thread_task_timings() + } - fn is_main_thread(&self) -> bool { - thread::current().id() == self.main_thread_id - } + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - self.background_sender - .send(priority, runnable) - .unwrap_or_else(|_| panic!("blocking sender returned without value")); - } + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + self.background_sender + .send(priority, runnable) + .unwrap_or_else(|_| panic!("blocking sender returned without value")); + } - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { - self.main_sender - .send(priority, runnable) - .unwrap_or_else(|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_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { + self.main_sender + .send(priority, runnable) + .unwrap_or_else(|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.timer_sender - .send(TimerAfter { duration, runnable }) - .ok(); - } + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { + self.timer_sender + .send(TimerAfter { duration, runnable }) + .ok(); + } - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - // SAFETY: always safe to call - let thread_id = unsafe { libc::pthread_self() }; + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + // SAFETY: always safe to call + let thread_id = unsafe { libc::pthread_self() }; - let policy = libc::SCHED_FIFO; - let sched_priority = 65; + let policy = libc::SCHED_FIFO; + let sched_priority = 65; - // SAFETY: all sched_param members are valid when initialized to zero. - let mut sched_param = - unsafe { MaybeUninit::::zeroed().assume_init() }; - sched_param.sched_priority = sched_priority; - // SAFETY: sched_param is a valid initialized structure - let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) }; - if result != 0 { - log::warn!("failed to set realtime thread priority"); - } + // SAFETY: all sched_param members are valid when initialized to zero. + let mut sched_param = + unsafe { MaybeUninit::::zeroed().assume_init() }; + sched_param.sched_priority = sched_priority; + // SAFETY: sched_param is a valid initialized structure + let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) }; + if result != 0 { + log::warn!("failed to set realtime thread priority"); + } - f(); - }); - } + f(); + }); + } } pub struct PriorityQueueCalloopSender { - sender: PriorityQueueSender, - ping: calloop::ping::Ping, + sender: PriorityQueueSender, + ping: calloop::ping::Ping, } impl PriorityQueueCalloopSender { - fn new(tx: PriorityQueueSender, ping: calloop::ping::Ping) -> Self { - Self { sender: tx, ping } - } + fn new(tx: PriorityQueueSender, ping: calloop::ping::Ping) -> Self { + Self { sender: tx, ping } + } - fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError> { - let res = self.sender.send(priority, item); - if res.is_ok() { - self.ping.ping(); - } - res - } + fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError> { + let res = self.sender.send(priority, item); + if res.is_ok() { + self.ping.ping(); + } + res + } } impl Drop for PriorityQueueCalloopSender { - fn drop(&mut self) { - self.ping.ping(); - } + fn drop(&mut self) { + self.ping.ping(); + } } pub struct PriorityQueueCalloopReceiver { - receiver: PriorityQueueReceiver, - source: calloop::ping::PingSource, - ping: calloop::ping::Ping, + receiver: PriorityQueueReceiver, + source: calloop::ping::PingSource, + ping: calloop::ping::Ping, } impl PriorityQueueCalloopReceiver { - pub fn new() -> (PriorityQueueCalloopSender, Self) { - let (ping, source) = calloop::ping::make_ping().expect("Failed to create a Ping."); + pub fn new() -> (PriorityQueueCalloopSender, Self) { + let (ping, source) = calloop::ping::make_ping().expect("Failed to create a Ping."); - let (tx, rx) = PriorityQueueReceiver::new(); + let (tx, rx) = PriorityQueueReceiver::new(); - ( - PriorityQueueCalloopSender::new(tx, ping.clone()), - Self { - receiver: rx, - source, - ping, - }, - ) - } + ( + PriorityQueueCalloopSender::new(tx, ping.clone()), + Self { + receiver: rx, + source, + ping, + }, + ) + } } use calloop::channel::Event; @@ -247,156 +247,156 @@ use calloop::channel::Event; pub struct ChannelError(calloop::ping::PingError); impl std::fmt::Display for ChannelError { - #[cfg_attr(feature = "nightly_coverage", coverage(off))] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } + #[cfg_attr(feature = "nightly_coverage", coverage(off))] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } } impl std::error::Error for ChannelError { - #[cfg_attr(feature = "nightly_coverage", coverage(off))] - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.0) - } + #[cfg_attr(feature = "nightly_coverage", coverage(off))] + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } } impl calloop::EventSource for PriorityQueueCalloopReceiver { - type Event = Event; - type Metadata = (); - type Ret = (); - type Error = ChannelError; + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = ChannelError; - fn process_events( - &mut self, - readiness: calloop::Readiness, - token: calloop::Token, - mut callback: F, - ) -> Result - where - F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, - { - let mut clear_readiness = false; - let mut disconnected = false; + fn process_events( + &mut self, + readiness: calloop::Readiness, + token: calloop::Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let mut clear_readiness = false; + let mut disconnected = false; - let action = self - .source - .process_events(readiness, token, |(), &mut ()| { - let mut is_empty = true; + let action = self + .source + .process_events(readiness, token, |(), &mut ()| { + let mut is_empty = true; - let receiver = self.receiver.clone(); - for runnable in receiver.try_iter() { - match runnable { - Ok(r) => { - callback(Event::Msg(r), &mut ()); - is_empty = false; - } - Err(_) => { - disconnected = true; - } - } - } + let receiver = self.receiver.clone(); + for runnable in receiver.try_iter() { + match runnable { + Ok(r) => { + callback(Event::Msg(r), &mut ()); + is_empty = false; + } + Err(_) => { + disconnected = true; + } + } + } - if disconnected { - callback(Event::Closed, &mut ()); - } + if disconnected { + callback(Event::Closed, &mut ()); + } - if is_empty { - clear_readiness = true; - } - }) - .map_err(ChannelError)?; + if is_empty { + clear_readiness = true; + } + }) + .map_err(ChannelError)?; - if disconnected { - Ok(PostAction::Remove) - } else if clear_readiness { - Ok(action) - } else { - // Re-notify the ping source so we can try again. - self.ping.ping(); - Ok(PostAction::Continue) - } - } + if disconnected { + Ok(PostAction::Remove) + } else if clear_readiness { + Ok(action) + } else { + // Re-notify the ping source so we can try again. + self.ping.ping(); + Ok(PostAction::Continue) + } + } - fn register( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.source.register(poll, token_factory) - } + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.register(poll, token_factory) + } - fn reregister( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.source.reregister(poll, token_factory) - } + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.reregister(poll, token_factory) + } - fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { - self.source.unregister(poll) - } + fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { + self.source.unregister(poll) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn calloop_works() { - let mut event_loop = calloop::EventLoop::try_new().unwrap(); - let handle = event_loop.handle(); + #[test] + fn calloop_works() { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + let handle = event_loop.handle(); - let (tx, rx) = PriorityQueueCalloopReceiver::new(); + let (tx, rx) = PriorityQueueCalloopReceiver::new(); - struct Data { - got_msg: bool, - got_closed: bool, - } + struct Data { + got_msg: bool, + got_closed: bool, + } - let mut data = Data { - got_msg: false, - got_closed: false, - }; + let mut data = Data { + got_msg: false, + got_closed: false, + }; - let _channel_token = handle - .insert_source(rx, move |evt, &mut (), data: &mut Data| match evt { - Event::Msg(()) => { - data.got_msg = true; - } + let _channel_token = handle + .insert_source(rx, move |evt, &mut (), data: &mut Data| match evt { + Event::Msg(()) => { + data.got_msg = true; + } - Event::Closed => { - data.got_closed = true; - } - }) - .unwrap(); + Event::Closed => { + data.got_closed = true; + } + }) + .unwrap(); - // nothing is sent, nothing is received - event_loop - .dispatch(Some(::std::time::Duration::ZERO), &mut data) - .unwrap(); + // nothing is sent, nothing is received + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); - assert!(!data.got_msg); - assert!(!data.got_closed); - // a message is send + assert!(!data.got_msg); + assert!(!data.got_closed); + // a message is send - tx.send(Priority::Medium, ()).unwrap(); - event_loop - .dispatch(Some(::std::time::Duration::ZERO), &mut data) - .unwrap(); + tx.send(Priority::Medium, ()).unwrap(); + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); - assert!(data.got_msg); - assert!(!data.got_closed); + assert!(data.got_msg); + assert!(!data.got_closed); - // the sender is dropped - drop(tx); - event_loop - .dispatch(Some(::std::time::Duration::ZERO), &mut data) - .unwrap(); + // the sender is dropped + drop(tx); + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); - assert!(data.got_msg); - assert!(data.got_closed); - } + assert!(data.got_msg); + assert!(data.got_closed); + } } // running 1 test diff --git a/crates/gpui_linux/src/linux/headless/client.rs b/crates/gpui_linux/src/linux/headless/client.rs index 56cc9e8df0..25b4a99c30 100644 --- a/crates/gpui_linux/src/linux/headless/client.rs +++ b/crates/gpui_linux/src/linux/headless/client.rs @@ -6,123 +6,123 @@ use util::ResultExt; use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout}; use gpui::{ - AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout, - PlatformWindow, WindowParams, + AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout, + PlatformWindow, WindowParams, }; pub struct HeadlessClientState { - pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>, - pub(crate) event_loop: Option>, - pub(crate) common: LinuxCommon, + pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>, + pub(crate) event_loop: Option>, + pub(crate) common: LinuxCommon, } #[derive(Clone)] pub(crate) struct HeadlessClient(Rc>); impl HeadlessClient { - pub(crate) fn new() -> Self { - let event_loop = EventLoop::try_new().unwrap(); + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); - let handle = event_loop.handle(); + let handle = event_loop.handle(); - handle - .insert_source(main_receiver, |event, _, _: &mut HeadlessClient| { - if let calloop::channel::Event::Msg(runnable) = event { - runnable.run(); - } - }) - .ok(); + handle + .insert_source(main_receiver, |event, _, _: &mut HeadlessClient| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }) + .ok(); - HeadlessClient(Rc::new(RefCell::new(HeadlessClientState { - event_loop: Some(event_loop), - _loop_handle: handle, - common, - }))) - } + HeadlessClient(Rc::new(RefCell::new(HeadlessClientState { + event_loop: Some(event_loop), + _loop_handle: handle, + common, + }))) + } } impl LinuxClient for HeadlessClient { - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { - f(&mut self.0.borrow_mut().common) - } + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } - fn keyboard_layout(&self) -> Box { - Box::new(LinuxKeyboardLayout::new("unknown".into())) - } + fn keyboard_layout(&self) -> Box { + Box::new(LinuxKeyboardLayout::new("unknown".into())) + } - fn displays(&self) -> Vec> { - vec![] - } + fn displays(&self) -> Vec> { + vec![] + } - fn primary_display(&self) -> Option> { - None - } + fn primary_display(&self) -> Option> { + None + } - fn display(&self, _id: DisplayId) -> Option> { - None - } + fn display(&self, _id: DisplayId) -> Option> { + None + } - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> futures::channel::oneshot::Receiver>>> - { - let (tx, rx) = futures::channel::oneshot::channel(); - tx.send(Err(anyhow::anyhow!( - "Headless mode does not support screen capture." - ))) - .ok(); - rx - } + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + let (tx, rx) = futures::channel::oneshot::channel(); + tx.send(Err(anyhow::anyhow!( + "Headless mode does not support screen capture." + ))) + .ok(); + rx + } - fn active_window(&self) -> Option { - None - } + fn active_window(&self) -> Option { + None + } - fn window_stack(&self) -> Option> { - None - } + fn window_stack(&self) -> Option> { + None + } - fn open_window( - &self, - _handle: AnyWindowHandle, - _params: WindowParams, - ) -> anyhow::Result> { - anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode"); - } + fn open_window( + &self, + _handle: AnyWindowHandle, + _params: WindowParams, + ) -> anyhow::Result> { + anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode"); + } - fn compositor_name(&self) -> &'static str { - "headless" - } + fn compositor_name(&self) -> &'static str { + "headless" + } - fn set_cursor_style(&self, _style: CursorStyle) {} + fn set_cursor_style(&self, _style: CursorStyle) {} - fn open_uri(&self, _uri: &str) {} + fn open_uri(&self, _uri: &str) {} - fn reveal_path(&self, _path: std::path::PathBuf) {} + fn reveal_path(&self, _path: std::path::PathBuf) {} - fn write_to_primary(&self, _item: gpui::ClipboardItem) {} + fn write_to_primary(&self, _item: gpui::ClipboardItem) {} - fn write_to_clipboard(&self, _item: gpui::ClipboardItem) {} + fn write_to_clipboard(&self, _item: gpui::ClipboardItem) {} - fn read_from_primary(&self) -> Option { - None - } + fn read_from_primary(&self) -> Option { + None + } - fn read_from_clipboard(&self) -> Option { - None - } + fn read_from_clipboard(&self) -> Option { + None + } - fn run(&self) { - let mut event_loop = self - .0 - .borrow_mut() - .event_loop - .take() - .expect("App is already running"); + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); - event_loop.run(None, &mut self.clone(), |_| {}).log_err(); - } + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } } diff --git a/crates/gpui_linux/src/linux/keyboard.rs b/crates/gpui_linux/src/linux/keyboard.rs index d810a2f4f7..7d07f9533b 100644 --- a/crates/gpui_linux/src/linux/keyboard.rs +++ b/crates/gpui_linux/src/linux/keyboard.rs @@ -2,21 +2,21 @@ use gpui::{PlatformKeyboardLayout, SharedString}; #[derive(Clone)] pub(crate) struct LinuxKeyboardLayout { - name: SharedString, + name: SharedString, } impl PlatformKeyboardLayout for LinuxKeyboardLayout { - fn id(&self) -> &str { - &self.name - } + fn id(&self) -> &str { + &self.name + } - fn name(&self) -> &str { - &self.name - } + fn name(&self) -> &str { + &self.name + } } impl LinuxKeyboardLayout { - pub(crate) fn new(name: SharedString) -> Self { - Self { name } - } + pub(crate) fn new(name: SharedString) -> Self { + Self { name } + } } diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index 5c8f21c7ac..7c86cee59b 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -1,16 +1,16 @@ use std::{ - env, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, + env, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, }; #[cfg(any(feature = "wayland", feature = "x11"))] use std::{ - ffi::OsString, - fs::File, - io::Read as _, - os::fd::{AsFd, FromRawFd, IntoRawFd}, - time::Duration, + ffi::OsString, + fs::File, + io::Read as _, + os::fd::{AsFd, FromRawFd, IntoRawFd}, + time::Duration, }; use anyhow::{Context as _, anyhow}; @@ -23,11 +23,11 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver}; use gpui::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, RunnableVariant, SharedString, Task, ThermalState, WindowAppearance, - WindowButtonLayout, WindowParams, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Result, RunnableVariant, SharedString, Task, ThermalState, WindowAppearance, + WindowButtonLayout, WindowParams, }; #[cfg(any(feature = "wayland", feature = "x11"))] use gpui::{Pixels, Point, px}; @@ -45,202 +45,202 @@ pub(crate) const KEYRING_LABEL: &str = "gpui-ce"; #[cfg(any(feature = "wayland", feature = "x11"))] const FILE_PICKER_PORTAL_MISSING: &str = - "Couldn't open file picker due to missing xdg-desktop-portal implementation."; + "Couldn't open file picker due to missing xdg-desktop-portal implementation."; pub(crate) trait LinuxClient { - fn compositor_name(&self) -> &'static str; - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; - fn keyboard_layout(&self) -> Box; - fn displays(&self) -> Vec>; - #[allow(unused)] - fn display(&self, id: DisplayId) -> Option>; - fn primary_display(&self) -> Option>; + fn compositor_name(&self) -> &'static str; + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; + fn keyboard_layout(&self) -> Box; + fn displays(&self) -> Vec>; + #[allow(unused)] + fn display(&self, id: DisplayId) -> Option>; + fn primary_display(&self) -> Option>; - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - true - } + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - let (sources_tx, sources_rx) = oneshot::channel(); - sources_tx - .send(Err(anyhow::anyhow!( - "gpui_linux was compiled without the screen-capture feature" - ))) - .ok(); - sources_rx - } + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "gpui_linux was compiled without the screen-capture feature" + ))) + .ok(); + sources_rx + } - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> anyhow::Result>; - fn set_cursor_style(&self, style: CursorStyle); - fn hide_cursor_until_mouse_moves(&self) {} - fn is_cursor_visible(&self) -> bool { - true - } - fn open_uri(&self, uri: &str); - fn reveal_path(&self, path: PathBuf); - fn write_to_primary(&self, item: ClipboardItem); - fn write_to_clipboard(&self, item: ClipboardItem); - fn read_from_primary(&self) -> Option; - fn read_from_clipboard(&self) -> Option; - fn active_window(&self) -> Option; - fn window_stack(&self) -> Option>; - fn set_gpu_requirements(&self, _requirements: Box) {} - fn run(&self); + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; + fn set_cursor_style(&self, style: CursorStyle); + fn hide_cursor_until_mouse_moves(&self) {} + fn is_cursor_visible(&self) -> bool { + true + } + fn open_uri(&self, uri: &str); + fn reveal_path(&self, path: PathBuf); + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + fn active_window(&self) -> Option; + fn window_stack(&self) -> Option>; + fn set_gpu_requirements(&self, _requirements: Box) {} + fn run(&self); - #[cfg(any(feature = "wayland", feature = "x11"))] - fn window_identifier( - &self, - ) -> impl Future> + Send + 'static { - std::future::ready::>(None) - } + #[cfg(any(feature = "wayland", feature = "x11"))] + fn window_identifier( + &self, + ) -> impl Future> + Send + 'static { + std::future::ready::>(None) + } } #[derive(Default)] pub(crate) struct PlatformHandlers { - pub(crate) open_urls: Option)>>, - pub(crate) quit: Option>, - pub(crate) reopen: Option>, - pub(crate) app_menu_action: Option>, - pub(crate) will_open_app_menu: Option>, - pub(crate) validate_app_menu_command: Option bool>>, - pub(crate) keyboard_layout_change: Option>, + pub(crate) open_urls: Option)>>, + pub(crate) quit: Option>, + pub(crate) reopen: Option>, + pub(crate) app_menu_action: Option>, + pub(crate) will_open_app_menu: Option>, + pub(crate) validate_app_menu_command: Option bool>>, + pub(crate) keyboard_layout_change: Option>, } pub(crate) struct LinuxCommon { - pub(crate) background_executor: BackgroundExecutor, - pub(crate) foreground_executor: ForegroundExecutor, - pub(crate) text_system: Arc, - pub(crate) appearance: WindowAppearance, - pub(crate) auto_hide_scrollbars: bool, - pub(crate) button_layout: WindowButtonLayout, - pub(crate) callbacks: PlatformHandlers, - pub(crate) signal: LoopSignal, - pub(crate) menus: Vec, - pub(crate) keyring_label: SharedString, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) text_system: Arc, + pub(crate) appearance: WindowAppearance, + pub(crate) auto_hide_scrollbars: bool, + pub(crate) button_layout: WindowButtonLayout, + pub(crate) callbacks: PlatformHandlers, + pub(crate) signal: LoopSignal, + pub(crate) menus: Vec, + pub(crate) keyring_label: SharedString, } impl LinuxCommon { - pub fn new(signal: LoopSignal) -> (Self, PriorityQueueCalloopReceiver) { - let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new(); + pub fn new(signal: LoopSignal) -> (Self, PriorityQueueCalloopReceiver) { + let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new(); - #[cfg(any(feature = "wayland", feature = "x11"))] - let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans")); - #[cfg(not(any(feature = "wayland", feature = "x11")))] - let text_system = Arc::new(gpui::NoopTextSystem::new()); + #[cfg(any(feature = "wayland", feature = "x11"))] + let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans")); + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let text_system = Arc::new(gpui::NoopTextSystem::new()); - let callbacks = PlatformHandlers::default(); + let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let common = LinuxCommon { - background_executor, - foreground_executor: ForegroundExecutor::new(dispatcher), - text_system, - appearance: WindowAppearance::Light, - auto_hide_scrollbars: false, - button_layout: WindowButtonLayout::linux_default(), - callbacks, - signal, - menus: Vec::new(), - keyring_label: KEYRING_LABEL.into(), - }; + let common = LinuxCommon { + background_executor, + foreground_executor: ForegroundExecutor::new(dispatcher), + text_system, + appearance: WindowAppearance::Light, + auto_hide_scrollbars: false, + button_layout: WindowButtonLayout::linux_default(), + callbacks, + signal, + menus: Vec::new(), + keyring_label: KEYRING_LABEL.into(), + }; - (common, main_receiver) - } + (common, main_receiver) + } } pub(crate) struct LinuxPlatform

{ - pub(crate) inner: P, + pub(crate) inner: P, } impl Platform for LinuxPlatform

{ - fn background_executor(&self) -> BackgroundExecutor { - self.inner - .with_common(|common| common.background_executor.clone()) - } + fn background_executor(&self) -> BackgroundExecutor { + self.inner + .with_common(|common| common.background_executor.clone()) + } - fn foreground_executor(&self) -> ForegroundExecutor { - self.inner - .with_common(|common| common.foreground_executor.clone()) - } + fn foreground_executor(&self) -> ForegroundExecutor { + self.inner + .with_common(|common| common.foreground_executor.clone()) + } - fn text_system(&self) -> Arc { - self.inner.with_common(|common| common.text_system.clone()) - } + fn text_system(&self) -> Arc { + self.inner.with_common(|common| common.text_system.clone()) + } - fn keyboard_layout(&self) -> Box { - self.inner.keyboard_layout() - } + fn keyboard_layout(&self) -> Box { + self.inner.keyboard_layout() + } - fn keyboard_mapper(&self) -> Rc { - Rc::new(gpui::DummyKeyboardMapper) - } + fn keyboard_mapper(&self) -> Rc { + Rc::new(gpui::DummyKeyboardMapper) + } - fn on_keyboard_layout_change(&self, callback: Box) { - self.inner - .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); - } + fn on_keyboard_layout_change(&self, callback: Box) { + self.inner + .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); + } - fn on_thermal_state_change(&self, _callback: Box) {} + fn on_thermal_state_change(&self, _callback: Box) {} - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } - fn run(&self, on_finish_launching: Box) { - on_finish_launching(); + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); - LinuxClient::run(&self.inner); + LinuxClient::run(&self.inner); - let quit = self - .inner - .with_common(|common| common.callbacks.quit.take()); - if let Some(mut fun) = quit { - fun(); - } - } + let quit = self + .inner + .with_common(|common| common.callbacks.quit.take()); + if let Some(mut fun) = quit { + fun(); + } + } - fn quit(&self) { - self.inner.with_common(|common| common.signal.stop()); - } + fn quit(&self) { + self.inner.with_common(|common| common.signal.stop()); + } - fn compositor_name(&self) -> &'static str { - self.inner.compositor_name() - } + fn compositor_name(&self) -> &'static str { + self.inner.compositor_name() + } - fn restart(&self, binary_path: Option) { - use std::os::unix::process::CommandExt as _; + fn restart(&self, binary_path: Option) { + use std::os::unix::process::CommandExt as _; - // get the process id of the current process - let app_pid = std::process::id().to_string(); - // get the path to the executable - let app_path = if let Some(path) = binary_path { - path - } else { - match self.app_path() { - Ok(path) => path, - Err(err) => { - log::error!("Failed to get app path: {:?}", err); - return; - } - } - }; + // get the process id of the current process + let app_pid = std::process::id().to_string(); + // get the path to the executable + let app_path = if let Some(path) = binary_path { + path + } else { + match self.app_path() { + Ok(path) => path, + Err(err) => { + log::error!("Failed to get app path: {:?}", err); + return; + } + } + }; - log::info!("Restarting process, using app path: {:?}", app_path); + log::info!("Restarting process, using app path: {:?}", app_path); - // Script to wait for the current process to exit and then restart the app. - // Pass dynamic values as positional parameters to avoid shell interpolation issues. - let script = r#" + // Script to wait for the current process to exit and then restart the app. + // Pass dynamic values as positional parameters to avoid shell interpolation issues. + let script = r#" while kill -0 "$0" 2>/dev/null; do sleep 0.1 done @@ -248,536 +248,536 @@ impl Platform for LinuxPlatform

{ "$1" "#; - #[allow( - clippy::disallowed_methods, - reason = "We are restarting ourselves, using std command thus is fine" - )] - let restart_process = new_std_command("/usr/bin/env") - .arg("bash") - .arg("-c") - .arg(script) - .arg(&app_pid) - .arg(&app_path) - .process_group(0) - .spawn(); - - match restart_process { - Ok(_) => self.quit(), - Err(e) => log::error!("failed to spawn restart script: {:?}", e), - } - } - - fn activate(&self, _ignoring_other_apps: bool) { - log::info!("activate is not implemented on Linux, ignoring the call") - } - - fn hide(&self) { - log::info!("hide is not implemented on Linux, ignoring the call") - } - - fn hide_other_apps(&self) { - log::info!("hide_other_apps is not implemented on Linux, ignoring the call") - } - - fn unhide_other_apps(&self) { - log::info!("unhide_other_apps is not implemented on Linux, ignoring the call") - } - - fn primary_display(&self) -> Option> { - self.inner.primary_display() - } - - fn displays(&self) -> Vec> { - self.inner.displays() - } - - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - self.inner.is_screen_capture_supported() - } - - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - self.inner.screen_capture_sources() - } - - fn active_window(&self) -> Option { - self.inner.active_window() - } - - fn window_stack(&self) -> Option> { - self.inner.window_stack() - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> anyhow::Result> { - self.inner.open_window(handle, options) - } - - fn set_gpu_requirements(&self, requirements: Box) { - self.inner.set_gpu_requirements(requirements); - } - - fn set_keyring_label(&self, label: SharedString) { - self.inner - .with_common(|common| common.keyring_label = label); - } - - fn open_url(&self, url: &str) { - self.inner.open_uri(url); - } - - fn on_open_urls(&self, callback: Box)>) { - self.inner - .with_common(|common| common.callbacks.open_urls = Some(callback)); - } - - fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> oneshot::Receiver>>> { - let (done_tx, done_rx) = oneshot::channel(); - - #[cfg(not(any(feature = "wayland", feature = "x11")))] - let _ = (done_tx.send(Ok(None)), options); - - #[cfg(any(feature = "wayland", feature = "x11"))] - let identifier = self.inner.window_identifier(); - - #[cfg(any(feature = "wayland", feature = "x11"))] - self.foreground_executor() - .spawn(async move { - let title = if options.directories { - "Open Folder" - } else { - "Open File" - }; - - let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() - .identifier(identifier.await) - .modal(true) - .title(title) - .accept_label(options.prompt.as_ref().map(gpui::SharedString::as_str)) - .multiple(options.multiple) - .directory(options.directories) - .send() - .await - { - Ok(request) => request, - Err(err) => { - let result = match err { - ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING), - err => err.into(), - }; - let _ = done_tx.send(Err(result)); - return; - } - }; - - let result = match request.response() { - Ok(response) => Ok(Some( - response - .uris() - .iter() - .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) - .filter_map(|uri: url::Url| uri.to_file_path().ok()) - .collect::>(), - )), - Err(ashpd::Error::Response(_)) => Ok(None), - Err(e) => Err(e.into()), - }; - let _ = done_tx.send(result); - }) - .detach(); - done_rx - } - - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let (done_tx, done_rx) = oneshot::channel(); - - #[cfg(not(any(feature = "wayland", feature = "x11")))] - let _ = (done_tx.send(Ok(None)), directory, suggested_name); - - #[cfg(any(feature = "wayland", feature = "x11"))] - let identifier = self.inner.window_identifier(); - - #[cfg(any(feature = "wayland", feature = "x11"))] - self.foreground_executor() - .spawn({ - let directory = directory.to_owned(); - let suggested_name = suggested_name.map(|s| s.to_owned()); - - async move { - let mut request_builder = - ashpd::desktop::file_chooser::SaveFileRequest::default() - .identifier(identifier.await) - .modal(true) - .title("Save File") - .current_folder(directory) - .expect("pathbuf should not be nul terminated"); - - if let Some(suggested_name) = suggested_name { - request_builder = request_builder.current_name(suggested_name.as_str()); - } - - let request = match request_builder.send().await { - Ok(request) => request, - Err(err) => { - let result = match err { - ashpd::Error::PortalNotFound(_) => { - anyhow!(FILE_PICKER_PORTAL_MISSING) - } - err => err.into(), - }; - let _ = done_tx.send(Err(result)); - return; - } - }; - - let result = match request.response() { - Ok(response) => Ok(response - .uris() - .first() - .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) - .and_then(|uri: url::Url| uri.to_file_path().ok())), - Err(ashpd::Error::Response(_)) => Ok(None), - Err(e) => Err(e.into()), - }; - let _ = done_tx.send(result); - } - }) - .detach(); - - done_rx - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories". - false - } - - fn reveal_path(&self, path: &Path) { - self.inner.reveal_path(path.to_owned()); - } - - fn open_with_system(&self, path: &Path) { - let path = path.to_owned(); - self.background_executor() - .spawn(async move { - let _ = new_command("xdg-open") - .arg(path) - .spawn() - .context("invoking xdg-open") - .log_err()? - .status() - .await - .log_err()?; - Some(()) - }) - .detach(); - } - - fn on_quit(&self, callback: Box) { - self.inner.with_common(|common| { - common.callbacks.quit = Some(callback); - }); - } - - fn on_reopen(&self, callback: Box) { - self.inner.with_common(|common| { - common.callbacks.reopen = Some(callback); - }); - } - - fn on_app_menu_action(&self, callback: Box) { - self.inner.with_common(|common| { - common.callbacks.app_menu_action = Some(callback); - }); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.inner.with_common(|common| { - common.callbacks.will_open_app_menu = Some(callback); - }); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.inner.with_common(|common| { - common.callbacks.validate_app_menu_command = Some(callback); - }); - } - - fn app_path(&self) -> Result { - // get the path of the executable of the current process - let app_path = env::current_exe()?; - Ok(app_path) - } - - fn set_menus(&self, menus: Vec

, _keymap: &Keymap) { - self.inner.with_common(|common| { - common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); - }) - } - - fn get_menus(&self) -> Option> { - self.inner.with_common(|common| Some(common.menus.clone())) - } - - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) { - // todo(linux) - } - - fn path_for_auxiliary_executable(&self, _name: &str) -> Result { - Err(anyhow::Error::msg( - "Platform::path_for_auxiliary_executable is not implemented yet", - )) - } - - fn set_cursor_style(&self, style: CursorStyle) { - self.inner.set_cursor_style(style) - } - - fn hide_cursor_until_mouse_moves(&self) { - self.inner.hide_cursor_until_mouse_moves() - } - - fn is_cursor_visible(&self) -> bool { - self.inner.is_cursor_visible() - } - - fn should_auto_hide_scrollbars(&self) -> bool { - self.inner.with_common(|common| common.auto_hide_scrollbars) - } - - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { - let url = url.to_string(); - let username = username.to_string(); - let password = password.to_vec(); - let label = self - .inner - .with_common(|common| common.keyring_label.clone()); - self.background_executor().spawn(async move { - let keyring = oo7::Keyring::new().await?; - keyring.unlock().await?; - keyring - .create_item( - &label, - &vec![("url", &url), ("username", &username)], - password, - true, - ) - .await?; - Ok(()) - }) - } - - fn read_credentials(&self, url: &str) -> Task)>>> { - let url = url.to_string(); - let label = self - .inner - .with_common(|common| common.keyring_label.clone()); - self.background_executor().spawn(async move { - let keyring = oo7::Keyring::new().await?; - keyring.unlock().await?; - - let items = keyring.search_items(&vec![("url", &url)]).await?; - - for item in items.into_iter() { - if item.label().await.is_ok_and(|l| l == label.as_ref()) { - let attributes = item.attributes().await?; - let username = attributes - .get("username") - .context("Cannot find username in stored credentials")?; - item.unlock().await?; - let secret = item.secret().await?; - - // we lose the zeroizing capabilities at this boundary, - // a current limitation GPUI's credentials api - return Ok(Some((username.to_string(), secret.to_vec()))); - } else { - continue; - } - } - Ok(None) - }) - } - - fn delete_credentials(&self, url: &str) -> Task> { - let url = url.to_string(); - let label = self - .inner - .with_common(|common| common.keyring_label.clone()); - self.background_executor().spawn(async move { - let keyring = oo7::Keyring::new().await?; - keyring.unlock().await?; - - let items = keyring.search_items(&vec![("url", &url)]).await?; - - for item in items.into_iter() { - if item.label().await.is_ok_and(|l| l == label.as_ref()) { - item.delete().await?; - return Ok(()); - } - } - - Ok(()) - }) - } - - fn window_appearance(&self) -> WindowAppearance { - self.inner.with_common(|common| common.appearance) - } - - fn button_layout(&self) -> Option { - Some(self.inner.with_common(|common| common.button_layout)) - } - - fn register_url_scheme(&self, _: &str) -> Task> { - Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) - } - - fn write_to_primary(&self, item: ClipboardItem) { - self.inner.write_to_primary(item) - } - - fn write_to_clipboard(&self, item: ClipboardItem) { - self.inner.write_to_clipboard(item) - } - - fn read_from_primary(&self) -> Option { - self.inner.read_from_primary() - } - - fn read_from_clipboard(&self) -> Option { - self.inner.read_from_clipboard() - } - - fn add_recent_document(&self, _path: &Path) {} + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = new_std_command("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg(script) + .arg(&app_pid) + .arg(&app_path) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, _ignoring_other_apps: bool) { + log::info!("activate is not implemented on Linux, ignoring the call") + } + + fn hide(&self) { + log::info!("hide is not implemented on Linux, ignoring the call") + } + + fn hide_other_apps(&self) { + log::info!("hide_other_apps is not implemented on Linux, ignoring the call") + } + + fn unhide_other_apps(&self) { + log::info!("unhide_other_apps is not implemented on Linux, ignoring the call") + } + + fn primary_display(&self) -> Option> { + self.inner.primary_display() + } + + fn displays(&self) -> Vec> { + self.inner.displays() + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + self.inner.is_screen_capture_supported() + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.inner.screen_capture_sources() + } + + fn active_window(&self) -> Option { + self.inner.active_window() + } + + fn window_stack(&self) -> Option> { + self.inner.window_stack() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result> { + self.inner.open_window(handle, options) + } + + fn set_gpu_requirements(&self, requirements: Box) { + self.inner.set_gpu_requirements(requirements); + } + + fn set_keyring_label(&self, label: SharedString) { + self.inner + .with_common(|common| common.keyring_label = label); + } + + fn open_url(&self, url: &str) { + self.inner.open_uri(url); + } + + fn on_open_urls(&self, callback: Box)>) { + self.inner + .with_common(|common| common.callbacks.open_urls = Some(callback)); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), options); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.inner.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn(async move { + let title = if options.directories { + "Open Folder" + } else { + "Open File" + }; + + let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title(title) + .accept_label(options.prompt.as_ref().map(gpui::SharedString::as_str)) + .multiple(options.multiple) + .directory(options.directories) + .send() + .await + { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING), + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(Some( + response + .uris() + .iter() + .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) + .filter_map(|uri: url::Url| uri.to_file_path().ok()) + .collect::>(), + )), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + }) + .detach(); + done_rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), directory, suggested_name); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.inner.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn({ + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + + async move { + let mut request_builder = + ashpd::desktop::file_chooser::SaveFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title("Save File") + .current_folder(directory) + .expect("pathbuf should not be nul terminated"); + + if let Some(suggested_name) = suggested_name { + request_builder = request_builder.current_name(suggested_name.as_str()); + } + + let request = match request_builder.send().await { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => { + anyhow!(FILE_PICKER_PORTAL_MISSING) + } + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(response + .uris() + .first() + .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) + .and_then(|uri: url::Url| uri.to_file_path().ok())), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + } + }) + .detach(); + + done_rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories". + false + } + + fn reveal_path(&self, path: &Path) { + self.inner.reveal_path(path.to_owned()); + } + + fn open_with_system(&self, path: &Path) { + let path = path.to_owned(); + self.background_executor() + .spawn(async move { + let _ = new_command("xdg-open") + .arg(path) + .spawn() + .context("invoking xdg-open") + .log_err()? + .status() + .await + .log_err()?; + Some(()) + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.quit = Some(callback); + }); + } + + fn on_reopen(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.reopen = Some(callback); + }); + } + + fn on_app_menu_action(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.app_menu_action = Some(callback); + }); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.will_open_app_menu = Some(callback); + }); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.inner.with_common(|common| { + common.callbacks.validate_app_menu_command = Some(callback); + }); + } + + fn app_path(&self) -> Result { + // get the path of the executable of the current process + let app_path = env::current_exe()?; + Ok(app_path) + } + + fn set_menus(&self, menus: Vec, _keymap: &Keymap) { + self.inner.with_common(|common| { + common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); + }) + } + + fn get_menus(&self) -> Option> { + self.inner.with_common(|common| Some(common.menus.clone())) + } + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) { + // todo(linux) + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + Err(anyhow::Error::msg( + "Platform::path_for_auxiliary_executable is not implemented yet", + )) + } + + fn set_cursor_style(&self, style: CursorStyle) { + self.inner.set_cursor_style(style) + } + + fn hide_cursor_until_mouse_moves(&self) { + self.inner.hide_cursor_until_mouse_moves() + } + + fn is_cursor_visible(&self) -> bool { + self.inner.is_cursor_visible() + } + + fn should_auto_hide_scrollbars(&self) -> bool { + self.inner.with_common(|common| common.auto_hide_scrollbars) + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + keyring + .create_item( + &label, + &vec![("url", &url), ("username", &username)], + password, + true, + ) + .await?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|l| l == label.as_ref()) { + let attributes = item.attributes().await?; + let username = attributes + .get("username") + .context("Cannot find username in stored credentials")?; + item.unlock().await?; + let secret = item.secret().await?; + + // we lose the zeroizing capabilities at this boundary, + // a current limitation GPUI's credentials api + return Ok(Some((username.to_string(), secret.to_vec()))); + } else { + continue; + } + } + Ok(None) + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|l| l == label.as_ref()) { + item.delete().await?; + return Ok(()); + } + } + + Ok(()) + }) + } + + fn window_appearance(&self) -> WindowAppearance { + self.inner.with_common(|common| common.appearance) + } + + fn button_layout(&self) -> Option { + Some(self.inner.with_common(|common| common.button_layout)) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } + + fn write_to_primary(&self, item: ClipboardItem) { + self.inner.write_to_primary(item) + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + self.inner.write_to_clipboard(item) + } + + fn read_from_primary(&self) -> Option { + self.inner.read_from_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.inner.read_from_clipboard() + } + + fn add_recent_document(&self, _path: &Path) {} } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn open_uri_internal( - executor: BackgroundExecutor, - uri: &str, - activation_token: Option, + executor: BackgroundExecutor, + uri: &str, + activation_token: Option, ) { - if let Some(uri) = ashpd::Uri::parse(uri).log_err() { - executor - .spawn(async move { - let mut xdg_open_failed = false; - for mut command in open::commands(uri.to_string()) { - if let Some(token) = activation_token.as_ref() { - command.env("XDG_ACTIVATION_TOKEN", token); - } - let program = format!("{:?}", command.get_program()); - match smol::process::Command::from(command).spawn() { - Ok(mut cmd) => match cmd.status().await { - Ok(status) if status.success() => return, - Ok(status) => { - log::error!("Command {} exited with status: {}", program, status); - xdg_open_failed = true; - } - Err(e) => { - log::error!("Failed to get status from {}: {}", program, e); - xdg_open_failed = true; - } - }, - Err(e) => { - log::error!("Failed to open with {}: {}", program, e); - xdg_open_failed = true; - } - } - } + if let Some(uri) = ashpd::Uri::parse(uri).log_err() { + executor + .spawn(async move { + let mut xdg_open_failed = false; + for mut command in open::commands(uri.to_string()) { + if let Some(token) = activation_token.as_ref() { + command.env("XDG_ACTIVATION_TOKEN", token); + } + let program = format!("{:?}", command.get_program()); + match smol::process::Command::from(command).spawn() { + Ok(mut cmd) => match cmd.status().await { + Ok(status) if status.success() => return, + Ok(status) => { + log::error!("Command {} exited with status: {}", program, status); + xdg_open_failed = true; + } + Err(e) => { + log::error!("Failed to get status from {}: {}", program, e); + xdg_open_failed = true; + } + }, + Err(e) => { + log::error!("Failed to open with {}: {}", program, e); + xdg_open_failed = true; + } + } + } - if xdg_open_failed { - match ashpd::desktop::open_uri::OpenFileRequest::default() - .activation_token(activation_token.map(ashpd::ActivationToken::from)) - .send_uri(&uri) - .await - .and_then(|e| e.response()) - { - Ok(()) => {} - Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {} - Err(e) => { - log::error!("Failed to open with dbus: {}", e); - } - } - } - }) - .detach(); - } + if xdg_open_failed { + match ashpd::desktop::open_uri::OpenFileRequest::default() + .activation_token(activation_token.map(ashpd::ActivationToken::from)) + .send_uri(&uri) + .await + .and_then(|e| e.response()) + { + Ok(()) => {} + Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {} + Err(e) => { + log::error!("Failed to open with dbus: {}", e); + } + } + } + }) + .detach(); + } } #[cfg(any(feature = "x11", feature = "wayland"))] pub(super) fn reveal_path_internal( - executor: BackgroundExecutor, - path: PathBuf, - activation_token: Option, + executor: BackgroundExecutor, + path: PathBuf, + activation_token: Option, ) { - executor - .spawn(async move { - if let Some(dir) = File::open(path.clone()).log_err() { - match ashpd::desktop::open_uri::OpenDirectoryRequest::default() - .activation_token(activation_token.map(ashpd::ActivationToken::from)) - .send(&dir.as_fd()) - .await - { - Ok(_) => return, - Err(e) => log::error!("Failed to open with dbus: {}", e), - } - if path.is_dir() { - open::that_detached(path).log_err(); - } else { - open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err(); - } - } - }) - .detach(); + executor + .spawn(async move { + if let Some(dir) = File::open(path.clone()).log_err() { + match ashpd::desktop::open_uri::OpenDirectoryRequest::default() + .activation_token(activation_token.map(ashpd::ActivationToken::from)) + .send(&dir.as_fd()) + .await + { + Ok(_) => return, + Err(e) => log::error!("Failed to open with dbus: {}", e), + } + if path.is_dir() { + open::that_detached(path).log_err(); + } else { + open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err(); + } + } + }) + .detach(); } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { - let diff = a - b; - diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE + let diff = a - b; + diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option { - let mut locales = Vec::default(); - if let Some(locale) = env::var_os("LC_CTYPE") { - locales.push(locale); - } - locales.push(OsString::from("C")); - let mut state: Option = None; - for locale in locales { - if let Ok(table) = - xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) - { - state = Some(xkb::compose::State::new( - &table, - xkb::compose::STATE_NO_FLAGS, - )); - break; - } - } - state + let mut locales = Vec::default(); + if let Some(locale) = env::var_os("LC_CTYPE") { + locales.push(locale); + } + locales.push(OsString::from("C")); + let mut state: Option = None; + for locale in locales { + if let Ok(table) = + xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) + { + state = Some(xkb::compose::State::new( + &table, + xkb::compose::STATE_NO_FLAGS, + )); + break; + } + } + state } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) unsafe fn read_fd(fd: filedescriptor::FileDescriptor) -> Result> { - let mut file = unsafe { File::from_raw_fd(fd.into_raw_fd()) }; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - Ok(buffer) + let mut file = unsafe { File::from_raw_fd(fd.into_raw_fd()) }; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) } #[cfg(any(feature = "wayland", feature = "x11"))] @@ -785,224 +785,224 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] { - // Based on cursor names from chromium: - // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 - match style { - CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME], - CursorStyle::IBeam => &["text", "xterm"], - CursorStyle::Crosshair => &["crosshair", "cross"], - CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"], - CursorStyle::OpenHand => &["openhand", "grab", "hand1"], - CursorStyle::PointingHand => &["pointer", "hand", "hand2"], - CursorStyle::ResizeLeft => &["w-resize", "left_side"], - CursorStyle::ResizeRight => &["e-resize", "right_side"], - CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"], - CursorStyle::ResizeUp => &["n-resize", "top_side"], - CursorStyle::ResizeDown => &["s-resize", "bottom_side"], - CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"], - CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"], - CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"], - CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"], - CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"], - CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"], - CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"], - CursorStyle::DragLink => &["alias"], - CursorStyle::DragCopy => &["copy"], - CursorStyle::ContextualMenu => &["context-menu"], - } + // Based on cursor names from chromium: + // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 + match style { + CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME], + CursorStyle::IBeam => &["text", "xterm"], + CursorStyle::Crosshair => &["crosshair", "cross"], + CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"], + CursorStyle::OpenHand => &["openhand", "grab", "hand1"], + CursorStyle::PointingHand => &["pointer", "hand", "hand2"], + CursorStyle::ResizeLeft => &["w-resize", "left_side"], + CursorStyle::ResizeRight => &["e-resize", "right_side"], + CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"], + CursorStyle::ResizeUp => &["n-resize", "top_side"], + CursorStyle::ResizeDown => &["s-resize", "bottom_side"], + CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"], + CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"], + CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"], + CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"], + CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"], + CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"], + CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"], + CursorStyle::DragLink => &["alias"], + CursorStyle::DragCopy => &["copy"], + CursorStyle::ContextualMenu => &["context-menu"], + } } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { - if let Ok(xcursor_path) = env::var("XCURSOR_PATH") { - log::warn!( - "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \ + if let Ok(xcursor_path) = env::var("XCURSOR_PATH") { + log::warn!( + "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \ XCURSOR_PATH overrides the default icon search. Its current value is '{}'", - message, - xcursor_path - ); - } else { - log::warn!("{:#}", message); - } + message, + xcursor_path + ); + } else { + log::warn!("{:#}", message); + } } #[cfg(any(feature = "wayland", feature = "x11"))] fn guess_ascii(keycode: Keycode, shift: bool) -> Option { - let c = match (keycode.raw(), shift) { - (24, _) => 'q', - (25, _) => 'w', - (26, _) => 'e', - (27, _) => 'r', - (28, _) => 't', - (29, _) => 'y', - (30, _) => 'u', - (31, _) => 'i', - (32, _) => 'o', - (33, _) => 'p', - (34, false) => '[', - (34, true) => '{', - (35, false) => ']', - (35, true) => '}', - (38, _) => 'a', - (39, _) => 's', - (40, _) => 'd', - (41, _) => 'f', - (42, _) => 'g', - (43, _) => 'h', - (44, _) => 'j', - (45, _) => 'k', - (46, _) => 'l', - (47, false) => ';', - (47, true) => ':', - (48, false) => '\'', - (48, true) => '"', - (49, false) => '`', - (49, true) => '~', - (51, false) => '\\', - (51, true) => '|', - (52, _) => 'z', - (53, _) => 'x', - (54, _) => 'c', - (55, _) => 'v', - (56, _) => 'b', - (57, _) => 'n', - (58, _) => 'm', - (59, false) => ',', - (59, true) => '>', - (60, false) => '.', - (60, true) => '<', - (61, false) => '/', - (61, true) => '?', + let c = match (keycode.raw(), shift) { + (24, _) => 'q', + (25, _) => 'w', + (26, _) => 'e', + (27, _) => 'r', + (28, _) => 't', + (29, _) => 'y', + (30, _) => 'u', + (31, _) => 'i', + (32, _) => 'o', + (33, _) => 'p', + (34, false) => '[', + (34, true) => '{', + (35, false) => ']', + (35, true) => '}', + (38, _) => 'a', + (39, _) => 's', + (40, _) => 'd', + (41, _) => 'f', + (42, _) => 'g', + (43, _) => 'h', + (44, _) => 'j', + (45, _) => 'k', + (46, _) => 'l', + (47, false) => ';', + (47, true) => ':', + (48, false) => '\'', + (48, true) => '"', + (49, false) => '`', + (49, true) => '~', + (51, false) => '\\', + (51, true) => '|', + (52, _) => 'z', + (53, _) => 'x', + (54, _) => 'c', + (55, _) => 'v', + (56, _) => 'b', + (57, _) => 'n', + (58, _) => 'm', + (59, false) => ',', + (59, true) => '>', + (60, false) => '.', + (60, true) => '<', + (61, false) => '/', + (61, true) => '?', - _ => return None, - }; + _ => return None, + }; - Some(c) + Some(c) } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn keystroke_from_xkb( - state: &State, - mut modifiers: gpui::Modifiers, - keycode: Keycode, + state: &State, + mut modifiers: gpui::Modifiers, + keycode: Keycode, ) -> gpui::Keystroke { - let key_utf32 = state.key_get_utf32(keycode); - let key_utf8 = state.key_get_utf8(keycode); - let key_sym = state.key_get_one_sym(keycode); + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); - let key = match key_sym { - Keysym::Return => "enter".to_owned(), - Keysym::Prior => "pageup".to_owned(), - Keysym::Next => "pagedown".to_owned(), - Keysym::ISO_Left_Tab => "tab".to_owned(), - Keysym::KP_Prior => "pageup".to_owned(), - Keysym::KP_Next => "pagedown".to_owned(), - Keysym::XF86_Back => "back".to_owned(), - Keysym::XF86_Forward => "forward".to_owned(), - Keysym::XF86_Cut => "cut".to_owned(), - Keysym::XF86_Copy => "copy".to_owned(), - Keysym::XF86_Paste => "paste".to_owned(), - Keysym::XF86_New => "new".to_owned(), - Keysym::XF86_Open => "open".to_owned(), - Keysym::XF86_Save => "save".to_owned(), + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + Keysym::ISO_Left_Tab => "tab".to_owned(), + Keysym::KP_Prior => "pageup".to_owned(), + Keysym::KP_Next => "pagedown".to_owned(), + Keysym::XF86_Back => "back".to_owned(), + Keysym::XF86_Forward => "forward".to_owned(), + Keysym::XF86_Cut => "cut".to_owned(), + Keysym::XF86_Copy => "copy".to_owned(), + Keysym::XF86_Paste => "paste".to_owned(), + Keysym::XF86_New => "new".to_owned(), + Keysym::XF86_Open => "open".to_owned(), + Keysym::XF86_Save => "save".to_owned(), - Keysym::comma => ",".to_owned(), - Keysym::period => ".".to_owned(), - Keysym::less => "<".to_owned(), - Keysym::greater => ">".to_owned(), - Keysym::slash => "/".to_owned(), - Keysym::question => "?".to_owned(), + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), - Keysym::semicolon => ";".to_owned(), - Keysym::colon => ":".to_owned(), - Keysym::apostrophe => "'".to_owned(), - Keysym::quotedbl => "\"".to_owned(), + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), - Keysym::bracketleft => "[".to_owned(), - Keysym::braceleft => "{".to_owned(), - Keysym::bracketright => "]".to_owned(), - Keysym::braceright => "}".to_owned(), - Keysym::backslash => "\\".to_owned(), - Keysym::bar => "|".to_owned(), + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), - Keysym::grave => "`".to_owned(), - Keysym::asciitilde => "~".to_owned(), - Keysym::exclam => "!".to_owned(), - Keysym::at => "@".to_owned(), - Keysym::numbersign => "#".to_owned(), - Keysym::dollar => "$".to_owned(), - Keysym::percent => "%".to_owned(), - Keysym::asciicircum => "^".to_owned(), - Keysym::ampersand => "&".to_owned(), - Keysym::asterisk => "*".to_owned(), - Keysym::parenleft => "(".to_owned(), - Keysym::parenright => ")".to_owned(), - Keysym::minus => "-".to_owned(), - Keysym::underscore => "_".to_owned(), - Keysym::equal => "=".to_owned(), - Keysym::plus => "+".to_owned(), - Keysym::space => "space".to_owned(), - Keysym::BackSpace => "backspace".to_owned(), - Keysym::Tab => "tab".to_owned(), - Keysym::Delete => "delete".to_owned(), - Keysym::Escape => "escape".to_owned(), + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + Keysym::space => "space".to_owned(), + Keysym::BackSpace => "backspace".to_owned(), + Keysym::Tab => "tab".to_owned(), + Keysym::Delete => "delete".to_owned(), + Keysym::Escape => "escape".to_owned(), - Keysym::Left => "left".to_owned(), - Keysym::Right => "right".to_owned(), - Keysym::Up => "up".to_owned(), - Keysym::Down => "down".to_owned(), - Keysym::Home => "home".to_owned(), - Keysym::End => "end".to_owned(), - Keysym::Insert => "insert".to_owned(), + Keysym::Left => "left".to_owned(), + Keysym::Right => "right".to_owned(), + Keysym::Up => "up".to_owned(), + Keysym::Down => "down".to_owned(), + Keysym::Home => "home".to_owned(), + Keysym::End => "end".to_owned(), + Keysym::Insert => "insert".to_owned(), - _ => { - let name = xkb::keysym_get_name(key_sym).to_lowercase(); - if key_sym.is_keypad_key() { - name.replace("kp_", "") - } else if let Some(key) = key_utf8.chars().next() - && key_utf8.len() == 1 - && key.is_ascii() - { - if key.is_ascii_graphic() { - key_utf8.to_lowercase() - // map ctrl-a to `a` - // ctrl-0..9 may emit control codes like ctrl-[, but - // we don't want to map them to `[` - } else if key_utf32 <= 0x1f - && !name.chars().next().is_some_and(|c| c.is_ascii_digit()) - { - ((key_utf32 as u8 + 0x40) as char) - .to_ascii_lowercase() - .to_string() - } else { - name - } - } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { - String::from(key_en) - } else { - name - } - } - }; + _ => { + let name = xkb::keysym_get_name(key_sym).to_lowercase(); + if key_sym.is_keypad_key() { + name.replace("kp_", "") + } else if let Some(key) = key_utf8.chars().next() + && key_utf8.len() == 1 + && key.is_ascii() + { + if key.is_ascii_graphic() { + key_utf8.to_lowercase() + // map ctrl-a to `a` + // ctrl-0..9 may emit control codes like ctrl-[, but + // we don't want to map them to `[` + } else if key_utf32 <= 0x1f + && !name.chars().next().is_some_and(|c| c.is_ascii_digit()) + { + ((key_utf32 as u8 + 0x40) as char) + .to_ascii_lowercase() + .to_string() + } else { + name + } + } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { + String::from(key_en) + } else { + name + } + } + }; - if modifiers.shift { - // we only include the shift for upper-case letters by convention, - // so don't include for numbers and symbols, but do include for - // tab/enter, etc. - if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() { - modifiers.shift = false; - } - } + if modifiers.shift { + // we only include the shift for upper-case letters by convention, + // so don't include for numbers and symbols, but do include for + // tab/enter, etc. + if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() { + modifiers.shift = false; + } + } - // Ignore control characters (and DEL) for the purposes of key_char - let key_char = - (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); + // Ignore control characters (and DEL) for the purposes of key_char + let key_char = + (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); - gpui::Keystroke { - modifiers, - key, - key_char, - } + gpui::Keystroke { + modifiers, + key, + key_char, + } } /** @@ -1011,78 +1011,78 @@ pub(super) fn keystroke_from_xkb( */ #[cfg(any(feature = "wayland", feature = "x11"))] pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option { - match keysym { - Keysym::dead_grave => Some("`".to_owned()), - Keysym::dead_acute => Some("´".to_owned()), - Keysym::dead_circumflex => Some("^".to_owned()), - Keysym::dead_tilde => Some("~".to_owned()), - Keysym::dead_macron => Some("¯".to_owned()), - Keysym::dead_breve => Some("˘".to_owned()), - Keysym::dead_abovedot => Some("˙".to_owned()), - Keysym::dead_diaeresis => Some("¨".to_owned()), - Keysym::dead_abovering => Some("˚".to_owned()), - Keysym::dead_doubleacute => Some("˝".to_owned()), - Keysym::dead_caron => Some("ˇ".to_owned()), - Keysym::dead_cedilla => Some("¸".to_owned()), - Keysym::dead_ogonek => Some("˛".to_owned()), - Keysym::dead_iota => Some("ͅ".to_owned()), - Keysym::dead_voiced_sound => Some("゙".to_owned()), - Keysym::dead_semivoiced_sound => Some("゚".to_owned()), - Keysym::dead_belowdot => Some("̣̣".to_owned()), - Keysym::dead_hook => Some("̡".to_owned()), - Keysym::dead_horn => Some("̛".to_owned()), - Keysym::dead_stroke => Some("̶̶".to_owned()), - Keysym::dead_abovecomma => Some("̓̓".to_owned()), - Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()), - Keysym::dead_doublegrave => Some("̏".to_owned()), - Keysym::dead_belowring => Some("˳".to_owned()), - Keysym::dead_belowmacron => Some("̱".to_owned()), - Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()), - Keysym::dead_belowtilde => Some("̰".to_owned()), - Keysym::dead_belowbreve => Some("̮".to_owned()), - Keysym::dead_belowdiaeresis => Some("̤".to_owned()), - Keysym::dead_invertedbreve => Some("̯".to_owned()), - Keysym::dead_belowcomma => Some("̦".to_owned()), - Keysym::dead_currency => None, - Keysym::dead_lowline => None, - Keysym::dead_aboveverticalline => None, - Keysym::dead_belowverticalline => None, - Keysym::dead_longsolidusoverlay => None, - Keysym::dead_a => None, - Keysym::dead_A => None, - Keysym::dead_e => None, - Keysym::dead_E => None, - Keysym::dead_i => None, - Keysym::dead_I => None, - Keysym::dead_o => None, - Keysym::dead_O => None, - Keysym::dead_u => None, - Keysym::dead_U => None, - Keysym::dead_small_schwa => Some("ə".to_owned()), - Keysym::dead_capital_schwa => Some("Ə".to_owned()), - Keysym::dead_greek => None, - _ => None, - } + match keysym { + Keysym::dead_grave => Some("`".to_owned()), + Keysym::dead_acute => Some("´".to_owned()), + Keysym::dead_circumflex => Some("^".to_owned()), + Keysym::dead_tilde => Some("~".to_owned()), + Keysym::dead_macron => Some("¯".to_owned()), + Keysym::dead_breve => Some("˘".to_owned()), + Keysym::dead_abovedot => Some("˙".to_owned()), + Keysym::dead_diaeresis => Some("¨".to_owned()), + Keysym::dead_abovering => Some("˚".to_owned()), + Keysym::dead_doubleacute => Some("˝".to_owned()), + Keysym::dead_caron => Some("ˇ".to_owned()), + Keysym::dead_cedilla => Some("¸".to_owned()), + Keysym::dead_ogonek => Some("˛".to_owned()), + Keysym::dead_iota => Some("ͅ".to_owned()), + Keysym::dead_voiced_sound => Some("゙".to_owned()), + Keysym::dead_semivoiced_sound => Some("゚".to_owned()), + Keysym::dead_belowdot => Some("̣̣".to_owned()), + Keysym::dead_hook => Some("̡".to_owned()), + Keysym::dead_horn => Some("̛".to_owned()), + Keysym::dead_stroke => Some("̶̶".to_owned()), + Keysym::dead_abovecomma => Some("̓̓".to_owned()), + Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()), + Keysym::dead_doublegrave => Some("̏".to_owned()), + Keysym::dead_belowring => Some("˳".to_owned()), + Keysym::dead_belowmacron => Some("̱".to_owned()), + Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()), + Keysym::dead_belowtilde => Some("̰".to_owned()), + Keysym::dead_belowbreve => Some("̮".to_owned()), + Keysym::dead_belowdiaeresis => Some("̤".to_owned()), + Keysym::dead_invertedbreve => Some("̯".to_owned()), + Keysym::dead_belowcomma => Some("̦".to_owned()), + Keysym::dead_currency => None, + Keysym::dead_lowline => None, + Keysym::dead_aboveverticalline => None, + Keysym::dead_belowverticalline => None, + Keysym::dead_longsolidusoverlay => None, + Keysym::dead_a => None, + Keysym::dead_A => None, + Keysym::dead_e => None, + Keysym::dead_E => None, + Keysym::dead_i => None, + Keysym::dead_I => None, + Keysym::dead_o => None, + Keysym::dead_O => None, + Keysym::dead_u => None, + Keysym::dead_U => None, + Keysym::dead_small_schwa => Some("ə".to_owned()), + Keysym::dead_capital_schwa => Some("Ə".to_owned()), + Keysym::dead_greek => None, + _ => None, + } } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui::Modifiers { - let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); - let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); - let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); - let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); - gpui::Modifiers { - shift, - alt, - control, - platform, - function: false, - } + let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + gpui::Modifiers { + shift, + alt, + control, + platform, + function: false, + } } #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui::Capslock { - let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); - gpui::Capslock { on } + let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); + gpui::Capslock { on } } /// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a @@ -1090,61 +1090,61 @@ pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui::Capslock { /// prioritize the compositor's rendering device. #[cfg(any(feature = "wayland", feature = "x11"))] pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option { - fn dev_major(dev: u64) -> u32 { - ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32) - } + fn dev_major(dev: u64) -> u32 { + ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32) + } - fn dev_minor(dev: u64) -> u32 { - (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32) - } + fn dev_minor(dev: u64) -> u32 { + (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32) + } - fn read_sysfs_hex_id(path: &str) -> Option { - let content = std::fs::read_to_string(path).ok()?; - let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim()); - u32::from_str_radix(trimmed, 16).ok() - } + fn read_sysfs_hex_id(path: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim()); + u32::from_str_radix(trimmed, 16).ok() + } - let major = dev_major(dev); - let minor = dev_minor(dev); + let major = dev_major(dev); + let minor = dev_minor(dev); - let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor"); - let device_path = format!("/sys/dev/char/{major}:{minor}/device/device"); + let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor"); + let device_path = format!("/sys/dev/char/{major}:{minor}/device/device"); - let vendor_id = read_sysfs_hex_id(&vendor_path)?; - let device_id = read_sysfs_hex_id(&device_path)?; + let vendor_id = read_sysfs_hex_id(&vendor_path)?; + let device_id = read_sysfs_hex_id(&device_path)?; - log::info!( - "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})", - vendor_id, - device_id, - ); + log::info!( + "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})", + vendor_id, + device_id, + ); - Some(gpui_wgpu::CompositorGpuHint { - vendor_id, - device_id, - }) + Some(gpui_wgpu::CompositorGpuHint { + vendor_id, + device_id, + }) } #[cfg(test)] mod tests { - use super::*; - use gpui::{Point, px}; + use super::*; + use gpui::{Point, px}; - #[test] - fn test_is_within_click_distance() { - let zero = Point::new(px(0.0), px(0.0)); - assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); - assert!(is_within_click_distance( - zero, - Point::new(px(-4.9), px(5.0)) - )); - assert!(is_within_click_distance( - Point::new(px(3.0), px(2.0)), - Point::new(px(-2.0), px(-2.0)) - )); - assert!(!is_within_click_distance( - zero, - Point::new(px(5.0), px(5.1)) - ),); - } + #[test] + fn test_is_within_click_distance() { + let zero = Point::new(px(0.0), px(0.0)); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); + } } diff --git a/crates/gpui_linux/src/linux/wayland.rs b/crates/gpui_linux/src/linux/wayland.rs index a69efb5c35..9b8ea168d6 100644 --- a/crates/gpui_linux/src/linux/wayland.rs +++ b/crates/gpui_linux/src/linux/wayland.rs @@ -16,27 +16,27 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1:: use gpui::CursorStyle; pub(super) fn to_shape(style: CursorStyle) -> Shape { - match style { - CursorStyle::Arrow => Shape::Default, - CursorStyle::IBeam => Shape::Text, - CursorStyle::Crosshair => Shape::Crosshair, - CursorStyle::ClosedHand => Shape::Grabbing, - CursorStyle::OpenHand => Shape::Grab, - CursorStyle::PointingHand => Shape::Pointer, - CursorStyle::ResizeLeft => Shape::WResize, - CursorStyle::ResizeRight => Shape::EResize, - CursorStyle::ResizeLeftRight => Shape::EwResize, - CursorStyle::ResizeUp => Shape::NResize, - CursorStyle::ResizeDown => Shape::SResize, - CursorStyle::ResizeUpDown => Shape::NsResize, - CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize, - CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize, - CursorStyle::ResizeColumn => Shape::ColResize, - CursorStyle::ResizeRow => Shape::RowResize, - CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, - CursorStyle::OperationNotAllowed => Shape::NotAllowed, - CursorStyle::DragLink => Shape::Alias, - CursorStyle::DragCopy => Shape::Copy, - CursorStyle::ContextualMenu => Shape::ContextMenu, - } + match style { + CursorStyle::Arrow => Shape::Default, + CursorStyle::IBeam => Shape::Text, + CursorStyle::Crosshair => Shape::Crosshair, + CursorStyle::ClosedHand => Shape::Grabbing, + CursorStyle::OpenHand => Shape::Grab, + CursorStyle::PointingHand => Shape::Pointer, + CursorStyle::ResizeLeft => Shape::WResize, + CursorStyle::ResizeRight => Shape::EResize, + CursorStyle::ResizeLeftRight => Shape::EwResize, + CursorStyle::ResizeUp => Shape::NResize, + CursorStyle::ResizeDown => Shape::SResize, + CursorStyle::ResizeUpDown => Shape::NsResize, + CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize, + CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize, + CursorStyle::ResizeColumn => Shape::ColResize, + CursorStyle::ResizeRow => Shape::RowResize, + CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, + CursorStyle::OperationNotAllowed => Shape::NotAllowed, + CursorStyle::DragLink => Shape::Alias, + CursorStyle::DragCopy => Shape::Copy, + CursorStyle::ContextualMenu => Shape::ContextMenu, + } } diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 98ec884464..785903cd5d 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -1,16 +1,16 @@ use std::{ - cell::{RefCell, RefMut}, - hash::Hash, - os::fd::{AsRawFd, BorrowedFd}, - path::PathBuf, - rc::{Rc, Weak}, - time::{Duration, Instant}, + cell::{RefCell, RefMut}, + hash::Hash, + os::fd::{AsRawFd, BorrowedFd}, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, }; use ashpd::WindowIdentifier; use calloop::{ - EventLoop, LoopHandle, - timer::{TimeoutAction, Timer}, + EventLoop, LoopHandle, + timer::{TimeoutAction, Timer}, }; use calloop_wayland_source::WaylandSource; use collections::HashMap; @@ -27,45 +27,45 @@ use wayland_client::protocol::wl_data_device_manager::DndAction; use wayland_client::protocol::wl_data_offer::WlDataOffer; use wayland_client::protocol::wl_pointer::AxisSource; use wayland_client::protocol::{ - wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, }; use wayland_client::{ - Connection, Dispatch, Proxy, QueueHandle, delegate_noop, - protocol::{ - wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, - wl_shm_pool, wl_surface, - }, + Connection, Dispatch, Proxy, QueueHandle, delegate_noop, + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, + wl_shm_pool, wl_surface, + }, }; use wayland_protocols::wp::pointer_gestures::zv1::client::{ - zwp_pointer_gesture_hold_v1, zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1, + zwp_pointer_gesture_hold_v1, zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1, }; use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ - self, ZwpPrimarySelectionOfferV1, + self, ZwpPrimarySelectionOfferV1, }; use wayland_protocols::wp::primary_selection::zv1::client::{ - zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, - zwp_primary_selection_source_v1, + zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, }; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ - ContentHint, ContentPurpose, + ContentHint, ContentPurpose, }; use wayland_protocols::wp::text_input::zv3::client::{ - zwp_text_input_manager_v3, zwp_text_input_v3, + zwp_text_input_manager_v3, zwp_text_input_v3, }; use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; use wayland_protocols::xdg::decoration::zv1::client::{ - zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, + zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, }; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1; use wayland_protocols::{ - wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, - xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, + wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, + xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, }; use wayland_protocols::{ - wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, - xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, + wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, + xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, }; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; @@ -73,36 +73,36 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; use super::{ - display::WaylandDisplay, - scroll::KineticScrollController, - window::{ImeInput, WaylandWindowStatePtr}, + display::WaylandDisplay, + scroll::KineticScrollController, + window::{ImeInput, WaylandWindowStatePtr}, }; use crate::linux::{ - DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, SCROLL_LINES, - capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance, - keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal, - read_fd, reveal_path_internal, - wayland::{ - clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, - cursor::Cursor, - serial::{SerialKind, SerialTracker}, - to_shape, - window::WaylandWindow, - }, - xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, + DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, SCROLL_LINES, + capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance, + keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal, + read_fd, reveal_path_internal, + wayland::{ + clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, + cursor::Cursor, + serial::{SerialKind, SerialTracker}, + to_shape, + window::WaylandWindow, + }, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }; use gpui::{ - AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, - ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, - Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, - ScrollDelta, ScrollWheelEvent, SharedString, Size, TaskTiming, TouchPhase, WindowButtonLayout, - WindowParams, point, profiler, px, size, + AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, + ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, + MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, + Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, + ScrollDelta, ScrollWheelEvent, SharedString, Size, TaskTiming, TouchPhase, WindowButtonLayout, + WindowParams, point, profiler, px, size, }; use gpui_wgpu::{CompositorGpuHint, GpuContext}; use wayland_protocols::wp::linux_dmabuf::zv1::client::{ - zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, + zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, }; /// Used to convert evdev scancode to xkb scancode @@ -112,191 +112,191 @@ const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unk #[derive(Clone)] pub struct Globals { - pub qh: QueueHandle, - pub activation: Option, - pub compositor: wl_compositor::WlCompositor, - pub cursor_shape_manager: Option, - pub data_device_manager: Option, - pub primary_selection_manager: - Option, - pub wm_base: xdg_wm_base::XdgWmBase, - pub shm: wl_shm::WlShm, - pub seat: wl_seat::WlSeat, - pub viewporter: Option, - pub fractional_scale_manager: - Option, - pub decoration_manager: Option, - pub layer_shell: Option, - pub blur_manager: Option, - pub text_input_manager: Option, - pub gesture_manager: Option, - pub dialog: Option, - pub system_bell: Option, - pub executor: ForegroundExecutor, + pub qh: QueueHandle, + pub activation: Option, + pub compositor: wl_compositor::WlCompositor, + pub cursor_shape_manager: Option, + pub data_device_manager: Option, + pub primary_selection_manager: + Option, + pub wm_base: xdg_wm_base::XdgWmBase, + pub shm: wl_shm::WlShm, + pub seat: wl_seat::WlSeat, + pub viewporter: Option, + pub fractional_scale_manager: + Option, + pub decoration_manager: Option, + pub layer_shell: Option, + pub blur_manager: Option, + pub text_input_manager: Option, + pub gesture_manager: Option, + pub dialog: Option, + pub system_bell: Option, + pub executor: ForegroundExecutor, } impl Globals { - fn new( - globals: GlobalList, - executor: ForegroundExecutor, - qh: QueueHandle, - seat: wl_seat::WlSeat, - ) -> Self { - let dialog_v = XdgWmDialogV1::interface().version; - Globals { - activation: globals.bind(&qh, 1..=1, ()).ok(), - compositor: globals - .bind( - &qh, - wl_surface::REQ_SET_BUFFER_SCALE_SINCE - ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, - (), - ) - .unwrap(), - cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), - data_device_manager: globals - .bind( - &qh, - WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, - (), - ) - .ok(), - primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), - shm: globals.bind(&qh, 1..=1, ()).unwrap(), - seat, - wm_base: globals.bind(&qh, 1..=5, ()).unwrap(), - viewporter: globals.bind(&qh, 1..=1, ()).ok(), - fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), - decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), - layer_shell: globals.bind(&qh, 1..=5, ()).ok(), - blur_manager: globals.bind(&qh, 1..=1, ()).ok(), - text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), - gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), - dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), - system_bell: globals.bind(&qh, 1..=1, ()).ok(), - executor, - qh, - } - } + fn new( + globals: GlobalList, + executor: ForegroundExecutor, + qh: QueueHandle, + seat: wl_seat::WlSeat, + ) -> Self { + let dialog_v = XdgWmDialogV1::interface().version; + Globals { + activation: globals.bind(&qh, 1..=1, ()).ok(), + compositor: globals + .bind( + &qh, + wl_surface::REQ_SET_BUFFER_SCALE_SINCE + ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, + (), + ) + .unwrap(), + cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), + data_device_manager: globals + .bind( + &qh, + WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, + (), + ) + .ok(), + primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + seat, + wm_base: globals.bind(&qh, 1..=5, ()).unwrap(), + viewporter: globals.bind(&qh, 1..=1, ()).ok(), + fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), + decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), + layer_shell: globals.bind(&qh, 1..=5, ()).ok(), + blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), + gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), + dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), + system_bell: globals.bind(&qh, 1..=1, ()).ok(), + executor, + qh, + } + } } #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct InProgressOutput { - name: Option, - scale: Option, - position: Option>, - size: Option>, - subpixel: Option, + name: Option, + scale: Option, + position: Option>, + size: Option>, + subpixel: Option, } impl InProgressOutput { - fn complete(&self) -> Option { - if let Some((position, size)) = self.position.zip(self.size) { - let scale = self.scale.unwrap_or(1); - Some(Output { - name: self.name.clone(), - scale, - bounds: Bounds::new(position, size), - subpixel: self.subpixel, - }) - } else { - None - } - } + fn complete(&self) -> Option { + if let Some((position, size)) = self.position.zip(self.size) { + let scale = self.scale.unwrap_or(1); + Some(Output { + name: self.name.clone(), + scale, + bounds: Bounds::new(position, size), + subpixel: self.subpixel, + }) + } else { + None + } + } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Output { - pub name: Option, - pub scale: i32, - pub bounds: Bounds, - pub subpixel: Option, + pub name: Option, + pub scale: i32, + pub bounds: Bounds, + pub subpixel: Option, } pub(crate) struct WaylandClientState { - serial_tracker: SerialTracker, - globals: Globals, - pub gpu_context: GpuContext, - pub compositor_gpu: Option, - pub gpu_requirements: Option, - wl_seat: wl_seat::WlSeat, // TODO: Multi seat support - wl_pointer: Option, - pinch_gesture: Option, - hold_gesture: Option, - pinch_scale: f32, - wl_keyboard: Option, - cursor_shape_device: Option, - data_device: Option, - primary_selection: Option, - text_input: Option, - pre_edit_text: Option, - ime_pre_edit: Option, - composing: bool, - // Surface to Window mapping - windows: HashMap, - // Output to scale mapping - outputs: HashMap, - in_progress_outputs: HashMap, - wl_outputs: HashMap, - keyboard_layout: LinuxKeyboardLayout, - keymap_state: Option, - compose_state: Option, - drag: DragState, - click: ClickState, - repeat: KeyRepeat, - pub modifiers: Modifiers, - pub capslock: Capslock, - axis_source: AxisSource, - pub mouse_location: Option>, - continuous_scroll_delta: Option>, - discrete_scroll_delta: Option>, - kinetic_scroll: KineticScrollController, - vertical_modifier: f32, - horizontal_modifier: f32, - scroll_event_received: bool, - enter_token: Option<()>, - button_pressed: Option, - mouse_focused_window: Option, - keyboard_focused_window: Option, - loop_handle: LoopHandle<'static, WaylandClientStatePtr>, - cursor_style: Option, - cursor_hidden_window: Option, - clipboard: Clipboard, - data_offers: Vec>, - primary_data_offer: Option>, - cursor: Cursor, - pending_activation: Option, - event_loop: Option>, - pub common: LinuxCommon, + serial_tracker: SerialTracker, + globals: Globals, + pub gpu_context: GpuContext, + pub compositor_gpu: Option, + pub gpu_requirements: Option, + wl_seat: wl_seat::WlSeat, // TODO: Multi seat support + wl_pointer: Option, + pinch_gesture: Option, + hold_gesture: Option, + pinch_scale: f32, + wl_keyboard: Option, + cursor_shape_device: Option, + data_device: Option, + primary_selection: Option, + text_input: Option, + pre_edit_text: Option, + ime_pre_edit: Option, + composing: bool, + // Surface to Window mapping + windows: HashMap, + // Output to scale mapping + outputs: HashMap, + in_progress_outputs: HashMap, + wl_outputs: HashMap, + keyboard_layout: LinuxKeyboardLayout, + keymap_state: Option, + compose_state: Option, + drag: DragState, + click: ClickState, + repeat: KeyRepeat, + pub modifiers: Modifiers, + pub capslock: Capslock, + axis_source: AxisSource, + pub mouse_location: Option>, + continuous_scroll_delta: Option>, + discrete_scroll_delta: Option>, + kinetic_scroll: KineticScrollController, + vertical_modifier: f32, + horizontal_modifier: f32, + scroll_event_received: bool, + enter_token: Option<()>, + button_pressed: Option, + mouse_focused_window: Option, + keyboard_focused_window: Option, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + cursor_style: Option, + cursor_hidden_window: Option, + clipboard: Clipboard, + data_offers: Vec>, + primary_data_offer: Option>, + cursor: Cursor, + pending_activation: Option, + event_loop: Option>, + pub common: LinuxCommon, } pub struct DragState { - data_offer: Option, - window: Option, - position: Point, + data_offer: Option, + window: Option, + position: Point, } pub struct ClickState { - last_mouse_button: Option, - last_click: Instant, - last_location: Point, - current_count: usize, + last_mouse_button: Option, + last_click: Instant, + last_location: Point, + current_count: usize, } pub(crate) struct KeyRepeat { - characters_per_second: u32, - delay: Duration, - current_id: u64, - current_keycode: Option, + characters_per_second: u32, + delay: Duration, + current_id: u64, + current_keycode: Option, } pub(crate) enum PendingActivation { - /// URI to open in the web browser. - Uri(String), - /// Path to open in the file explorer. - Path(PathBuf), - /// A window from ourselves to raise. - Window(ObjectId), + /// URI to open in the web browser. + Uri(String), + /// Path to open in the file explorer. + Path(PathBuf), + /// A window from ourselves to raise. + Window(ObjectId), } /// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the @@ -305,856 +305,856 @@ pub(crate) enum PendingActivation { pub struct WaylandClientStatePtr(Weak>); impl WaylandClientStatePtr { - pub fn get_client(&self) -> Rc> { - self.0 - .upgrade() - .expect("The pointer should always be valid when dispatching in wayland") - } + pub fn get_client(&self) -> Rc> { + self.0 + .upgrade() + .expect("The pointer should always be valid when dispatching in wayland") + } - pub fn get_serial(&self, kind: SerialKind) -> u32 { - self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) - } + pub fn get_serial(&self, kind: SerialKind) -> u32 { + self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) + } - pub fn set_pending_activation(&self, window: ObjectId) { - self.0.upgrade().unwrap().borrow_mut().pending_activation = - Some(PendingActivation::Window(window)); - } + pub fn set_pending_activation(&self, window: ObjectId) { + self.0.upgrade().unwrap().borrow_mut().pending_activation = + Some(PendingActivation::Window(window)); + } - pub fn enable_ime(&self) { - let client = self.get_client(); - let mut state = client.borrow_mut(); - let Some(text_input) = state.text_input.take() else { - return; - }; + pub fn enable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let Some(text_input) = state.text_input.take() else { + return; + }; - text_input.enable(); - text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); - if let Some(window) = state.keyboard_focused_window.clone() { - drop(state); - if let Some(area) = window.get_ime_area() { - text_input.set_cursor_rectangle( - f32::from(area.origin.x) as i32, - f32::from(area.origin.y) as i32, - f32::from(area.size.width) as i32, - f32::from(area.size.height) as i32, - ); - } - state = client.borrow_mut(); - } - text_input.commit(); - state.text_input = Some(text_input); - } + text_input.enable(); + text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + if let Some(area) = window.get_ime_area() { + text_input.set_cursor_rectangle( + f32::from(area.origin.x) as i32, + f32::from(area.origin.y) as i32, + f32::from(area.size.width) as i32, + f32::from(area.size.height) as i32, + ); + } + state = client.borrow_mut(); + } + text_input.commit(); + state.text_input = Some(text_input); + } - pub fn disable_ime(&self) { - let client = self.get_client(); - let mut state = client.borrow_mut(); - state.composing = false; - if let Some(text_input) = &state.text_input { - text_input.disable(); - text_input.commit(); - } - } + pub fn disable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + state.composing = false; + if let Some(text_input) = &state.text_input { + text_input.disable(); + text_input.commit(); + } + } - pub fn update_ime_position(&self, bounds: Bounds) { - let client = self.get_client(); - let state = client.borrow_mut(); - if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() { - return; - } + pub fn update_ime_position(&self, bounds: Bounds) { + let client = self.get_client(); + let state = client.borrow_mut(); + if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() { + return; + } - let text_input = state.text_input.as_ref().unwrap(); - text_input.set_cursor_rectangle( - bounds.origin.x.as_f32() as i32, - bounds.origin.y.as_f32() as i32, - bounds.size.width.as_f32() as i32, - bounds.size.height.as_f32() as i32, - ); - text_input.commit(); - } + let text_input = state.text_input.as_ref().unwrap(); + text_input.set_cursor_rectangle( + bounds.origin.x.as_f32() as i32, + bounds.origin.y.as_f32() as i32, + bounds.size.width.as_f32() as i32, + bounds.size.height.as_f32() as i32, + ); + text_input.commit(); + } - pub fn handle_keyboard_layout_change(&self) { - let client = self.get_client(); - let mut state = client.borrow_mut(); - let changed = if let Some(keymap_state) = &state.keymap_state { - let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); - let keymap = keymap_state.get_keymap(); - let layout_name = keymap.layout_get_name(layout_idx); - let changed = layout_name != state.keyboard_layout.name(); - if changed { - state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); - } - changed - } else { - let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); - if changed { - state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); - } - changed - }; + pub fn handle_keyboard_layout_change(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let changed = if let Some(keymap_state) = &state.keymap_state { + let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + let keymap = keymap_state.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + let changed = layout_name != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + } + changed + } else { + let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); + } + changed + }; - if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() - { - drop(state); - callback(); - state = client.borrow_mut(); - state.common.callbacks.keyboard_layout_change = Some(callback); - } - } + if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() + { + drop(state); + callback(); + state = client.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } - pub fn drop_window(&self, surface_id: &ObjectId) { - let client = self.get_client(); - let mut state = client.borrow_mut(); - let closed_window = state.windows.remove(surface_id).unwrap(); - if let Some(window) = state.mouse_focused_window.take() - && !window.ptr_eq(&closed_window) - { - state.mouse_focused_window = Some(window); - } - if let Some(window) = state.keyboard_focused_window.take() - && !window.ptr_eq(&closed_window) - { - state.keyboard_focused_window = Some(window); - } - if let Some(window) = state.cursor_hidden_window.take() - && !window.ptr_eq(&closed_window) - { - state.cursor_hidden_window = Some(window); - } - } + pub fn drop_window(&self, surface_id: &ObjectId) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let closed_window = state.windows.remove(surface_id).unwrap(); + if let Some(window) = state.mouse_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.mouse_focused_window = Some(window); + } + if let Some(window) = state.keyboard_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.keyboard_focused_window = Some(window); + } + if let Some(window) = state.cursor_hidden_window.take() + && !window.ptr_eq(&closed_window) + { + state.cursor_hidden_window = Some(window); + } + } } impl WaylandClientState { - fn hide_cursor_until_mouse_moves(&mut self) { - if self.cursor_hidden_window.is_some() { - return; - } - let Some(focused_window) = self.mouse_focused_window.clone() else { - // No surface to apply the hidden cursor to. - return; - }; - let Some(wl_pointer) = self.wl_pointer.clone() else { - // Seat lost its pointer capability; nothing to hide. - return; - }; - let serial = self.serial_tracker.get(SerialKind::MouseEnter); - wl_pointer.set_cursor(serial, None, 0, 0); - self.cursor_hidden_window = Some(focused_window); - } + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + // No surface to apply the hidden cursor to. + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + // Seat lost its pointer capability; nothing to hide. + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + wl_pointer.set_cursor(serial, None, 0, 0); + self.cursor_hidden_window = Some(focused_window); + } - fn restore_cursor_after_hide(&mut self) { - if self.cursor_hidden_window.take().is_none() { - return; - } - let Some(style) = self.cursor_style else { - return; - }; - let serial = self.serial_tracker.get(SerialKind::MouseEnter); - if let Some(cursor_shape_device) = &self.cursor_shape_device { - cursor_shape_device.set_shape(serial, to_shape(style)); - return; - } - let Some(focused_window) = self.mouse_focused_window.clone() else { - log::warn!( - "wayland: no focused surface to restore cursor style {:?} after hide; cursor may stay invisible", - style - ); - return; - }; - let Some(wl_pointer) = self.wl_pointer.clone() else { - log::warn!( - "wayland: no wl_pointer to restore cursor style {:?} after hide; cursor may stay invisible", - style - ); - return; - }; - let scale = focused_window.primary_output_scale(); - self.cursor.set_icon( - &wl_pointer, - serial, - cursor_style_to_icon_names(style), - scale, - ); - } + fn restore_cursor_after_hide(&mut self) { + if self.cursor_hidden_window.take().is_none() { + return; + } + let Some(style) = self.cursor_style else { + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &self.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + log::warn!( + "wayland: no focused surface to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + log::warn!( + "wayland: no wl_pointer to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let scale = focused_window.primary_output_scale(); + self.cursor.set_icon( + &wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); + } } #[derive(Clone)] pub struct WaylandClient(Rc>); impl Drop for WaylandClient { - fn drop(&mut self) { - let mut state = self.0.borrow_mut(); - state.windows.clear(); + fn drop(&mut self) { + let mut state = self.0.borrow_mut(); + state.windows.clear(); - if let Some(wl_pointer) = &state.wl_pointer { - wl_pointer.release(); - } - if let Some(cursor_shape_device) = &state.cursor_shape_device { - cursor_shape_device.destroy(); - } - if let Some(data_device) = &state.data_device { - data_device.release(); - } - if let Some(text_input) = &state.text_input { - text_input.destroy(); - } - } + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.destroy(); + } + if let Some(data_device) = &state.data_device { + data_device.release(); + } + if let Some(text_input) = &state.text_input { + text_input.destroy(); + } + } } const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; fn wl_seat_version(version: u32) -> u32 { - // We rely on the wl_pointer.frame event - const WL_SEAT_MIN_VERSION: u32 = 5; - const WL_SEAT_MAX_VERSION: u32 = 9; + // We rely on the wl_pointer.frame event + const WL_SEAT_MIN_VERSION: u32 = 5; + const WL_SEAT_MAX_VERSION: u32 = 9; - if version < WL_SEAT_MIN_VERSION { - panic!( - "wl_seat below required version: {} < {}", - version, WL_SEAT_MIN_VERSION - ); - } + if version < WL_SEAT_MIN_VERSION { + panic!( + "wl_seat below required version: {} < {}", + version, WL_SEAT_MIN_VERSION + ); + } - version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION) + version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION) } fn wl_output_version(version: u32) -> u32 { - const WL_OUTPUT_MIN_VERSION: u32 = 2; - const WL_OUTPUT_MAX_VERSION: u32 = 4; + const WL_OUTPUT_MIN_VERSION: u32 = 2; + const WL_OUTPUT_MAX_VERSION: u32 = 4; - if version < WL_OUTPUT_MIN_VERSION { - panic!( - "wl_output below required version: {} < {}", - version, WL_OUTPUT_MIN_VERSION - ); - } + if version < WL_OUTPUT_MIN_VERSION { + panic!( + "wl_output below required version: {} < {}", + version, WL_OUTPUT_MIN_VERSION + ); + } - version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION) + version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION) } impl WaylandClient { - pub(crate) fn new() -> Self { - let conn = Connection::connect_to_env().unwrap(); + pub(crate) fn new() -> Self { + let conn = Connection::connect_to_env().unwrap(); - let (globals, event_queue) = registry_queue_init::(&conn).unwrap(); - let qh = event_queue.handle(); + let (globals, event_queue) = registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); - let mut seat: Option = None; - #[allow(clippy::mutable_key_type)] - let mut in_progress_outputs = HashMap::default(); - #[allow(clippy::mutable_key_type)] - let mut wl_outputs: HashMap = HashMap::default(); - globals.contents().with_list(|list| { - for global in list { - match &global.interface[..] { - "wl_seat" => { - seat = Some(globals.registry().bind::( - global.name, - wl_seat_version(global.version), - &qh, - (), - )); - } - "wl_output" => { - let output = globals.registry().bind::( - global.name, - wl_output_version(global.version), - &qh, - (), - ); - in_progress_outputs.insert(output.id(), InProgressOutput::default()); - wl_outputs.insert(output.id(), output); - } - _ => {} - } - } - }); + let mut seat: Option = None; + #[allow(clippy::mutable_key_type)] + let mut in_progress_outputs = HashMap::default(); + #[allow(clippy::mutable_key_type)] + let mut wl_outputs: HashMap = HashMap::default(); + globals.contents().with_list(|list| { + for global in list { + match &global.interface[..] { + "wl_seat" => { + seat = Some(globals.registry().bind::( + global.name, + wl_seat_version(global.version), + &qh, + (), + )); + } + "wl_output" => { + let output = globals.registry().bind::( + global.name, + wl_output_version(global.version), + &qh, + (), + ); + in_progress_outputs.insert(output.id(), InProgressOutput::default()); + wl_outputs.insert(output.id(), output); + } + _ => {} + } + } + }); - let event_loop = EventLoop::::try_new().unwrap(); + let event_loop = EventLoop::::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); - let handle = event_loop.handle(); - handle - .insert_source(main_receiver, { - let handle = handle.clone(); - move |event, _, _: &mut WaylandClientStatePtr| { - if let calloop::channel::Event::Msg(runnable) = event { - handle.insert_idle(|_| { - let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - profiler::add_task_timing(timing); + let handle = event_loop.handle(); + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut WaylandClientStatePtr| { + if let calloop::channel::Event::Msg(runnable) = event { + handle.insert_idle(|_| { + let start = Instant::now(); + let location = runnable.metadata().location; + let mut timing = TaskTiming { + location, + start, + end: None, + }; + profiler::add_task_timing(timing); - runnable.run(); + runnable.run(); - let end = Instant::now(); - timing.end = Some(end); - profiler::add_task_timing(timing); - }); - } - } - }) - .unwrap(); + let end = Instant::now(); + timing.end = Some(end); + profiler::add_task_timing(timing); + }); + } + } + }) + .unwrap(); - let compositor_gpu = detect_compositor_gpu(); - let gpu_context = Rc::new(RefCell::new(None)); + let compositor_gpu = detect_compositor_gpu(); + let gpu_context = Rc::new(RefCell::new(None)); - let seat = seat.unwrap(); - let globals = Globals::new( - globals, - common.foreground_executor.clone(), - qh.clone(), - seat.clone(), - ); + let seat = seat.unwrap(); + let globals = Globals::new( + globals, + common.foreground_executor.clone(), + qh.clone(), + seat.clone(), + ); - let data_device = globals - .data_device_manager - .as_ref() - .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); + let data_device = globals + .data_device_manager + .as_ref() + .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); - let primary_selection = globals - .primary_selection_manager - .as_ref() - .map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ())); + let primary_selection = globals + .primary_selection_manager + .as_ref() + .map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ())); - let cursor = Cursor::new(&conn, &globals, 24); + let cursor = Cursor::new(&conn, &globals, 24); - handle - .insert_source(XDPEventSource::new(&common.background_executor), { - move |event, _, client| match event { - XDPEvent::WindowAppearance(appearance) => { - if let Some(client) = client.0.upgrade() { - let mut client = client.borrow_mut(); + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); - client.common.appearance = appearance; + client.common.appearance = appearance; - for window in client.windows.values_mut() { - window.set_appearance(appearance); - } - } - } - XDPEvent::ButtonLayout(layout_str) => { - if let Some(client) = client.0.upgrade() { - let layout = WindowButtonLayout::parse(&layout_str) - .log_err() - .unwrap_or_else(WindowButtonLayout::linux_default); - let mut client = client.borrow_mut(); - client.common.button_layout = layout; + for window in client.windows.values_mut() { + window.set_appearance(appearance); + } + } + } + XDPEvent::ButtonLayout(layout_str) => { + if let Some(client) = client.0.upgrade() { + let layout = WindowButtonLayout::parse(&layout_str) + .log_err() + .unwrap_or_else(WindowButtonLayout::linux_default); + let mut client = client.borrow_mut(); + client.common.button_layout = layout; - for window in client.windows.values_mut() { - window.set_button_layout(); - } - } - } - XDPEvent::CursorTheme(theme) => { - if let Some(client) = client.0.upgrade() { - let mut client = client.borrow_mut(); - client.cursor.set_theme(theme); - } - } - XDPEvent::CursorSize(size) => { - if let Some(client) = client.0.upgrade() { - let mut client = client.borrow_mut(); - client.cursor.set_size(size); - } - } - } - }) - .unwrap(); + for window in client.windows.values_mut() { + window.set_button_layout(); + } + } + } + XDPEvent::CursorTheme(theme) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_theme(theme); + } + } + XDPEvent::CursorSize(size) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_size(size); + } + } + } + }) + .unwrap(); - let state = Rc::new(RefCell::new(WaylandClientState { - serial_tracker: SerialTracker::new(), - globals, - gpu_context, - compositor_gpu, - gpu_requirements: None, - wl_seat: seat, - wl_pointer: None, - wl_keyboard: None, - pinch_gesture: None, - hold_gesture: None, - pinch_scale: 1.0, - cursor_shape_device: None, - data_device, - primary_selection, - text_input: None, - pre_edit_text: None, - ime_pre_edit: None, - composing: false, - outputs: HashMap::default(), - in_progress_outputs, - wl_outputs, - windows: HashMap::default(), - common, - keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), - keymap_state: None, - compose_state: None, - drag: DragState { - data_offer: None, - window: None, - position: Point::default(), - }, - click: ClickState { - last_click: Instant::now(), - last_mouse_button: None, - last_location: Point::default(), - current_count: 0, - }, - repeat: KeyRepeat { - characters_per_second: 16, - delay: Duration::from_millis(500), - current_id: 0, - current_keycode: None, - }, - modifiers: Modifiers { - shift: false, - control: false, - alt: false, - function: false, - platform: false, - }, - capslock: Capslock { on: false }, - scroll_event_received: false, - axis_source: AxisSource::Wheel, - mouse_location: None, - continuous_scroll_delta: None, - discrete_scroll_delta: None, - kinetic_scroll: KineticScrollController::new(), - vertical_modifier: -1.0, - horizontal_modifier: -1.0, - button_pressed: None, - mouse_focused_window: None, - keyboard_focused_window: None, - loop_handle: handle.clone(), - enter_token: None, - cursor_style: None, - cursor_hidden_window: None, - clipboard: Clipboard::new(conn.clone(), handle.clone()), - data_offers: Vec::new(), - primary_data_offer: None, - cursor, - pending_activation: None, - event_loop: Some(event_loop), - })); + let state = Rc::new(RefCell::new(WaylandClientState { + serial_tracker: SerialTracker::new(), + globals, + gpu_context, + compositor_gpu, + gpu_requirements: None, + wl_seat: seat, + wl_pointer: None, + wl_keyboard: None, + pinch_gesture: None, + hold_gesture: None, + pinch_scale: 1.0, + cursor_shape_device: None, + data_device, + primary_selection, + text_input: None, + pre_edit_text: None, + ime_pre_edit: None, + composing: false, + outputs: HashMap::default(), + in_progress_outputs, + wl_outputs, + windows: HashMap::default(), + common, + keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), + keymap_state: None, + compose_state: None, + drag: DragState { + data_offer: None, + window: None, + position: Point::default(), + }, + click: ClickState { + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::default(), + current_count: 0, + }, + repeat: KeyRepeat { + characters_per_second: 16, + delay: Duration::from_millis(500), + current_id: 0, + current_keycode: None, + }, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + platform: false, + }, + capslock: Capslock { on: false }, + scroll_event_received: false, + axis_source: AxisSource::Wheel, + mouse_location: None, + continuous_scroll_delta: None, + discrete_scroll_delta: None, + kinetic_scroll: KineticScrollController::new(), + vertical_modifier: -1.0, + horizontal_modifier: -1.0, + button_pressed: None, + mouse_focused_window: None, + keyboard_focused_window: None, + loop_handle: handle.clone(), + enter_token: None, + cursor_style: None, + cursor_hidden_window: None, + clipboard: Clipboard::new(conn.clone(), handle.clone()), + data_offers: Vec::new(), + primary_data_offer: None, + cursor, + pending_activation: None, + event_loop: Some(event_loop), + })); - WaylandSource::new(conn, event_queue) - .insert(handle) - .unwrap(); + WaylandSource::new(conn, event_queue) + .insert(handle) + .unwrap(); - Self(state) - } + Self(state) + } } impl LinuxClient for WaylandClient { - fn keyboard_layout(&self) -> Box { - Box::new(self.0.borrow().keyboard_layout.clone()) - } + fn keyboard_layout(&self) -> Box { + Box::new(self.0.borrow().keyboard_layout.clone()) + } - fn displays(&self) -> Vec> { - self.0 - .borrow() - .outputs - .iter() - .map(|(id, output)| { - Rc::new(WaylandDisplay { - id: id.clone(), - name: output.name.clone(), - bounds: output.bounds.to_pixels(output.scale as f32), - }) as Rc - }) - .collect() - } + fn displays(&self) -> Vec> { + self.0 + .borrow() + .outputs + .iter() + .map(|(id, output)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + .collect() + } - fn display(&self, id: DisplayId) -> Option> { - self.0 - .borrow() - .outputs - .iter() - .find_map(|(object_id, output)| { - (object_id.protocol_id() as u64 == u64::from(id)).then(|| { - Rc::new(WaylandDisplay { - id: object_id.clone(), - name: output.name.clone(), - bounds: output.bounds.to_pixels(output.scale as f32), - }) as Rc - }) - }) - } + fn display(&self, id: DisplayId) -> Option> { + self.0 + .borrow() + .outputs + .iter() + .find_map(|(object_id, output)| { + (object_id.protocol_id() as u64 == u64::from(id)).then(|| { + Rc::new(WaylandDisplay { + id: object_id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + }) + } - fn primary_display(&self) -> Option> { - None - } + fn primary_display(&self) -> Option> { + None + } - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> futures::channel::oneshot::Receiver>>> - { - // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may - // be tricky. - // - // start_scap_default_target_source() - let (sources_tx, sources_rx) = futures::channel::oneshot::channel(); - sources_tx - .send(Err(anyhow::anyhow!( - "Wayland screen capture not yet implemented." - ))) - .ok(); - sources_rx - } + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may + // be tricky. + // + // start_scap_default_target_source() + let (sources_tx, sources_rx) = futures::channel::oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "Wayland screen capture not yet implemented." + ))) + .ok(); + sources_rx + } - fn set_gpu_requirements(&self, requirements: Box) { - if let Ok(reqs) = requirements.downcast::() { - self.0.borrow_mut().gpu_requirements = Some(*reqs); - } else { - log::warn!("set_gpu_requirements: unexpected type, expected WgpuDeviceRequirements"); - } - } + fn set_gpu_requirements(&self, requirements: Box) { + if let Ok(reqs) = requirements.downcast::() { + self.0.borrow_mut().gpu_requirements = Some(*reqs); + } else { + log::warn!("set_gpu_requirements: unexpected type, expected WgpuDeviceRequirements"); + } + } - fn open_window( - &self, - handle: AnyWindowHandle, - params: WindowParams, - ) -> anyhow::Result> { - let mut state = self.0.borrow_mut(); + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); - let parent = state.keyboard_focused_window.clone(); + let parent = state.keyboard_focused_window.clone(); - let target_output = params.display_id.and_then(|display_id| { - let target_protocol_id: u64 = display_id.into(); - state - .wl_outputs - .iter() - .find(|(id, _)| id.protocol_id() as u64 == target_protocol_id) - .map(|(_, output)| output.clone()) - }); + let target_output = params.display_id.and_then(|display_id| { + let target_protocol_id: u64 = display_id.into(); + state + .wl_outputs + .iter() + .find(|(id, _)| id.protocol_id() as u64 == target_protocol_id) + .map(|(_, output)| output.clone()) + }); - let appearance = state.common.appearance; - let compositor_gpu = state.compositor_gpu.take(); - let gpu_requirements = state.gpu_requirements.clone(); - let (window, surface_id) = WaylandWindow::new( - handle, - state.globals.clone(), - state.gpu_context.clone(), - compositor_gpu, - gpu_requirements, - WaylandClientStatePtr(Rc::downgrade(&self.0)), - params, - appearance, - parent, - target_output, - )?; - state.windows.insert(surface_id, window.0.clone()); + let appearance = state.common.appearance; + let compositor_gpu = state.compositor_gpu.take(); + let gpu_requirements = state.gpu_requirements.clone(); + let (window, surface_id) = WaylandWindow::new( + handle, + state.globals.clone(), + state.gpu_context.clone(), + compositor_gpu, + gpu_requirements, + WaylandClientStatePtr(Rc::downgrade(&self.0)), + params, + appearance, + parent, + target_output, + )?; + state.windows.insert(surface_id, window.0.clone()); - Ok(Box::new(window)) - } + Ok(Box::new(window)) + } - fn set_cursor_style(&self, style: CursorStyle) { - let mut state = self.0.borrow_mut(); + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); - let need_update = state.cursor_style != Some(style) - && (state.mouse_focused_window.is_none() - || state - .mouse_focused_window - .as_ref() - .is_some_and(|w| !w.is_blocked())); + let need_update = state.cursor_style != Some(style) + && (state.mouse_focused_window.is_none() + || state + .mouse_focused_window + .as_ref() + .is_some_and(|w| !w.is_blocked())); - if !need_update { - return; - } + if !need_update { + return; + } - state.cursor_style = Some(style); + state.cursor_style = Some(style); - // Don't clobber the invisible cursor; restore reads back from `cursor_style`. - if state.cursor_hidden_window.is_some() { - return; - } + // Don't clobber the invisible cursor; restore reads back from `cursor_style`. + if state.cursor_hidden_window.is_some() { + return; + } - let serial = state.serial_tracker.get(SerialKind::MouseEnter); - if let Some(cursor_shape_device) = &state.cursor_shape_device { - cursor_shape_device.set_shape(serial, to_shape(style)); - } else if let Some(focused_window) = &state.mouse_focused_window { - // cursor-shape-v1 isn't supported, set the cursor using a surface. - let wl_pointer = state - .wl_pointer - .clone() - .expect("window is focused by pointer"); - let scale = focused_window.primary_output_scale(); - state.cursor.set_icon( - &wl_pointer, - serial, - cursor_style_to_icon_names(style), - scale, - ); - } - } + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + } else if let Some(focused_window) = &state.mouse_focused_window { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = focused_window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); + } + } - fn hide_cursor_until_mouse_moves(&self) { - self.0.borrow_mut().hide_cursor_until_mouse_moves(); - } + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } - fn is_cursor_visible(&self) -> bool { - self.0.borrow().cursor_hidden_window.is_none() - } + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() + } - fn open_uri(&self, uri: &str) { - let mut state = self.0.borrow_mut(); - if let (Some(activation), Some(window)) = ( - state.globals.activation.clone(), - state.mouse_focused_window.clone(), - ) { - state.pending_activation = Some(PendingActivation::Uri(uri.to_string())); - let token = activation.get_activation_token(&state.globals.qh, ()); - let serial = state.serial_tracker.get(SerialKind::MousePress); - token.set_serial(serial, &state.wl_seat); - token.set_surface(&window.surface()); - token.commit(); - } else { - let executor = state.common.background_executor.clone(); - open_uri_internal(executor, uri, None); - } - } + fn open_uri(&self, uri: &str) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Uri(uri.to_string())); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + open_uri_internal(executor, uri, None); + } + } - fn reveal_path(&self, path: PathBuf) { - let mut state = self.0.borrow_mut(); - if let (Some(activation), Some(window)) = ( - state.globals.activation.clone(), - state.mouse_focused_window.clone(), - ) { - state.pending_activation = Some(PendingActivation::Path(path)); - let token = activation.get_activation_token(&state.globals.qh, ()); - let serial = state.serial_tracker.get(SerialKind::MousePress); - token.set_serial(serial, &state.wl_seat); - token.set_surface(&window.surface()); - token.commit(); - } else { - let executor = state.common.background_executor.clone(); - reveal_path_internal(executor, path, None); - } - } + fn reveal_path(&self, path: PathBuf) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Path(path)); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + reveal_path_internal(executor, path, None); + } + } - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { - f(&mut self.0.borrow_mut().common) - } + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } - fn run(&self) { - let mut event_loop = self - .0 - .borrow_mut() - .event_loop - .take() - .expect("App is already running"); + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); - event_loop - .run( - None, - &mut WaylandClientStatePtr(Rc::downgrade(&self.0)), - |_| {}, - ) - .log_err(); - } + event_loop + .run( + None, + &mut WaylandClientStatePtr(Rc::downgrade(&self.0)), + |_| {}, + ) + .log_err(); + } - fn write_to_primary(&self, item: gpui::ClipboardItem) { - let mut state = self.0.borrow_mut(); - let (Some(primary_selection_manager), Some(primary_selection)) = ( - state.globals.primary_selection_manager.clone(), - state.primary_selection.clone(), - ) else { - return; - }; - if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { - state.clipboard.set_primary(item); - let serial = state.serial_tracker.get_latest(); - let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); - for mime_type in TEXT_MIME_TYPES { - data_source.offer(mime_type.to_string()); - } - data_source.offer(state.clipboard.self_mime()); - primary_selection.set_selection(Some(&data_source), serial); - } - } + fn write_to_primary(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(primary_selection_manager), Some(primary_selection)) = ( + state.globals.primary_selection_manager.clone(), + state.primary_selection.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set_primary(item); + let serial = state.serial_tracker.get_latest(); + let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + primary_selection.set_selection(Some(&data_source), serial); + } + } - fn write_to_clipboard(&self, item: gpui::ClipboardItem) { - let mut state = self.0.borrow_mut(); - let (Some(data_device_manager), Some(data_device)) = ( - state.globals.data_device_manager.clone(), - state.data_device.clone(), - ) else { - return; - }; - if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { - state.clipboard.set(item); - let serial = state.serial_tracker.get_latest(); - let data_source = data_device_manager.create_data_source(&state.globals.qh, ()); - for mime_type in TEXT_MIME_TYPES { - data_source.offer(mime_type.to_string()); - } - data_source.offer(state.clipboard.self_mime()); - data_device.set_selection(Some(&data_source), serial); - } - } + fn write_to_clipboard(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(data_device_manager), Some(data_device)) = ( + state.globals.data_device_manager.clone(), + state.data_device.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set(item); + let serial = state.serial_tracker.get_latest(); + let data_source = data_device_manager.create_data_source(&state.globals.qh, ()); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + data_device.set_selection(Some(&data_source), serial); + } + } - fn read_from_primary(&self) -> Option { - self.0.borrow_mut().clipboard.read_primary() - } + fn read_from_primary(&self) -> Option { + self.0.borrow_mut().clipboard.read_primary() + } - fn read_from_clipboard(&self) -> Option { - self.0.borrow_mut().clipboard.read() - } + fn read_from_clipboard(&self) -> Option { + self.0.borrow_mut().clipboard.read() + } - fn active_window(&self) -> Option { - self.0 - .borrow_mut() - .keyboard_focused_window - .as_ref() - .map(|window| window.handle()) - } + fn active_window(&self) -> Option { + self.0 + .borrow_mut() + .keyboard_focused_window + .as_ref() + .map(|window| window.handle()) + } - fn window_stack(&self) -> Option> { - None - } + fn window_stack(&self) -> Option> { + None + } - fn compositor_name(&self) -> &'static str { - "Wayland" - } + fn compositor_name(&self) -> &'static str { + "Wayland" + } - fn window_identifier(&self) -> impl Future> + Send + 'static { - async fn inner(surface: Option) -> Option { - if let Some(surface) = surface { - ashpd::WindowIdentifier::from_wayland(&surface).await - } else { - None - } - } + fn window_identifier(&self) -> impl Future> + Send + 'static { + async fn inner(surface: Option) -> Option { + if let Some(surface) = surface { + ashpd::WindowIdentifier::from_wayland(&surface).await + } else { + None + } + } - let client_state = self.0.borrow(); - let active_window = client_state.keyboard_focused_window.as_ref(); - inner(active_window.map(|aw| aw.surface())) - } + let client_state = self.0.borrow(); + let active_window = client_state.keyboard_focused_window.as_ref(); + inner(active_window.map(|aw| aw.surface())) + } } struct DmabufProbeState { - device: Option, + device: Option, } impl Dispatch for DmabufProbeState { - fn event( - _: &mut Self, - _: &wl_registry::WlRegistry, - _: wl_registry::Event, - _: &GlobalListContents, - _: &Connection, - _: &QueueHandle, - ) { - } + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } } impl Dispatch for DmabufProbeState { - fn event( - _: &mut Self, - _: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, - _: zwp_linux_dmabuf_v1::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } + fn event( + _: &mut Self, + _: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } } impl Dispatch for DmabufProbeState { - fn event( - state: &mut Self, - _: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, - event: zwp_linux_dmabuf_feedback_v1::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } = event { - if let Ok(bytes) = <[u8; 8]>::try_from(device.as_slice()) { - state.device = Some(u64::from_ne_bytes(bytes)); - } - } - } + fn event( + state: &mut Self, + _: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + event: zwp_linux_dmabuf_feedback_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } = event { + if let Ok(bytes) = <[u8; 8]>::try_from(device.as_slice()) { + state.device = Some(u64::from_ne_bytes(bytes)); + } + } + } } fn detect_compositor_gpu() -> Option { - let connection = Connection::connect_to_env().ok()?; - let (globals, mut event_queue) = registry_queue_init::(&connection).ok()?; - let queue_handle = event_queue.handle(); + let connection = Connection::connect_to_env().ok()?; + let (globals, mut event_queue) = registry_queue_init::(&connection).ok()?; + let queue_handle = event_queue.handle(); - let dmabuf: zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 = - globals.bind(&queue_handle, 4..=4, ()).ok()?; - let feedback = dmabuf.get_default_feedback(&queue_handle, ()); + let dmabuf: zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 = + globals.bind(&queue_handle, 4..=4, ()).ok()?; + let feedback = dmabuf.get_default_feedback(&queue_handle, ()); - let mut state = DmabufProbeState { device: None }; + let mut state = DmabufProbeState { device: None }; - event_queue.roundtrip(&mut state).ok()?; + event_queue.roundtrip(&mut state).ok()?; - feedback.destroy(); - dmabuf.destroy(); + feedback.destroy(); + dmabuf.destroy(); - crate::linux::compositor_gpu_hint_from_dev_t(state.device?) + crate::linux::compositor_gpu_hint_from_dev_t(state.device?) } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - registry: &wl_registry::WlRegistry, - event: wl_registry::Event, - _: &GlobalListContents, - _: &Connection, - qh: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + qh: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - match event { - wl_registry::Event::Global { - name, - interface, - version, - } => match &interface[..] { - "wl_seat" => { - if let Some(wl_pointer) = state.wl_pointer.take() { - wl_pointer.release(); - } - if let Some(wl_keyboard) = state.wl_keyboard.take() { - wl_keyboard.release(); - } - state.wl_seat.release(); - state.wl_seat = registry.bind::( - name, - wl_seat_version(version), - qh, - (), - ); - } - "wl_output" => { - let output = registry.bind::( - name, - wl_output_version(version), - qh, - (), - ); + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => match &interface[..] { + "wl_seat" => { + if let Some(wl_pointer) = state.wl_pointer.take() { + wl_pointer.release(); + } + if let Some(wl_keyboard) = state.wl_keyboard.take() { + wl_keyboard.release(); + } + state.wl_seat.release(); + state.wl_seat = registry.bind::( + name, + wl_seat_version(version), + qh, + (), + ); + } + "wl_output" => { + let output = registry.bind::( + name, + wl_output_version(version), + qh, + (), + ); - state - .in_progress_outputs - .insert(output.id(), InProgressOutput::default()); - state.wl_outputs.insert(output.id(), output); - } - _ => {} - }, - wl_registry::Event::GlobalRemove { name: _ } => { - // TODO: handle global removal - } - _ => {} - } - } + state + .in_progress_outputs + .insert(output.id(), InProgressOutput::default()); + state.wl_outputs.insert(output.id(), output); + } + _ => {} + }, + wl_registry::Event::GlobalRemove { name: _ } => { + // TODO: handle global removal + } + _ => {} + } + } } delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); @@ -1178,1484 +1178,1483 @@ delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport); impl Dispatch for WaylandClientStatePtr { - fn event( - state: &mut WaylandClientStatePtr, - _: &wl_callback::WlCallback, - event: wl_callback::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = state.get_client(); - let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; - let kinetic_input = if let wl_callback::Event::Done { .. } = event { - state.kinetic_scroll.tick(&window) - } else { - None - }; - drop(state); + fn event( + state: &mut WaylandClientStatePtr, + _: &wl_callback::WlCallback, + event: wl_callback::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + let kinetic_input = if let wl_callback::Event::Done { .. } = event { + state.kinetic_scroll.tick(&window) + } else { + None + }; + drop(state); - if let wl_callback::Event::Done { .. } = event { - window.frame(); - if let Some((window, input)) = kinetic_input { - window.handle_input(input); - } - } - } + if let wl_callback::Event::Done { .. } = event { + window.frame(); + if let Some((window, input)) = kinetic_input { + window.handle_input(input); + } + } + } } pub(crate) fn get_window( - state: &mut RefMut, - surface_id: &ObjectId, + state: &mut RefMut, + surface_id: &ObjectId, ) -> Option { - state.windows.get(surface_id).cloned() + state.windows.get(surface_id).cloned() } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - surface: &wl_surface::WlSurface, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + surface: &wl_surface::WlSurface, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, &surface.id()) else { - return; - }; - #[allow(clippy::mutable_key_type)] - let outputs = state.outputs.clone(); - drop(state); + let Some(window) = get_window(&mut state, &surface.id()) else { + return; + }; + #[allow(clippy::mutable_key_type)] + let outputs = state.outputs.clone(); + drop(state); - window.handle_surface_event(event, outputs); - } + window.handle_surface_event(event, outputs); + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - output: &wl_output::WlOutput, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + output: &wl_output::WlOutput, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - let Some(in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else { - return; - }; + let Some(in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else { + return; + }; - match event { - wl_output::Event::Name { name } => { - in_progress_output.name = Some(name); - } - wl_output::Event::Scale { factor } => { - in_progress_output.scale = Some(factor); - } - wl_output::Event::Geometry { x, y, subpixel, .. } => { - in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y))); - if let WEnum::Value(subpixel) = subpixel { - in_progress_output.subpixel = Some(subpixel); - } - } - wl_output::Event::Mode { width, height, .. } => { - in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height))) - } - wl_output::Event::Done => { - if let Some(complete) = in_progress_output.complete() { - state.outputs.insert(output.id(), complete); - } - state.in_progress_outputs.remove(&output.id()); - } - _ => {} - } - } + match event { + wl_output::Event::Name { name } => { + in_progress_output.name = Some(name); + } + wl_output::Event::Scale { factor } => { + in_progress_output.scale = Some(factor); + } + wl_output::Event::Geometry { x, y, subpixel, .. } => { + in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y))); + if let WEnum::Value(subpixel) = subpixel { + in_progress_output.subpixel = Some(subpixel); + } + } + wl_output::Event::Mode { width, height, .. } => { + in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height))) + } + wl_output::Event::Done => { + if let Some(complete) = in_progress_output.complete() { + state.outputs.insert(output.id(), complete); + } + state.in_progress_outputs.remove(&output.id()); + } + _ => {} + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - state: &mut Self, - _: &xdg_surface::XdgSurface, - event: xdg_surface::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = state.get_client(); - let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; - drop(state); - window.handle_xdg_surface_event(event); - } + fn event( + state: &mut Self, + _: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + window.handle_xdg_surface_event(event); + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &xdg_toplevel::XdgToplevel, - event: ::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; + fn event( + this: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; - drop(state); - let should_close = window.handle_toplevel_event(event); + drop(state); + let should_close = window.handle_toplevel_event(event); - if should_close { - // The close logic will be handled in drop_window() - window.close(); - } - } + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, - event: ::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; + fn event( + this: &mut Self, + _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; - drop(state); - let should_close = window.handle_layersurface_event(event); + drop(state); + let should_close = window.handle_layersurface_event(event); - if should_close { - // The close logic will be handled in drop_window() - window.close(); - } - } + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - _: &mut Self, - wm_base: &xdg_wm_base::XdgWmBase, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let xdg_wm_base::Event::Ping { serial } = event { - wm_base.pong(serial); - } - } + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - token: &xdg_activation_token_v1::XdgActivationTokenV1, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + token: &xdg_activation_token_v1::XdgActivationTokenV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - if let xdg_activation_token_v1::Event::Done { token } = event { - let executor = state.common.background_executor.clone(); - match state.pending_activation.take() { - Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)), - Some(PendingActivation::Path(path)) => { - reveal_path_internal(executor, path, Some(token)) - } - Some(PendingActivation::Window(window)) => { - let Some(window) = get_window(&mut state, &window) else { - return; - }; - let activation = state.globals.activation.as_ref().unwrap(); - activation.activate(token, &window.surface()); - } - None => log::error!("activation token received with no pending activation"), - } - } + if let xdg_activation_token_v1::Event::Done { token } = event { + let executor = state.common.background_executor.clone(); + match state.pending_activation.take() { + Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)), + Some(PendingActivation::Path(path)) => { + reveal_path_internal(executor, path, Some(token)) + } + Some(PendingActivation::Window(window)) => { + let Some(window) = get_window(&mut state, &window) else { + return; + }; + let activation = state.globals.activation.as_ref().unwrap(); + activation.activate(token, &window.surface()); + } + None => log::error!("activation token received with no pending activation"), + } + } - token.destroy(); - } + token.destroy(); + } } impl Dispatch for WaylandClientStatePtr { - fn event( - state: &mut Self, - seat: &wl_seat::WlSeat, - event: wl_seat::Event, - _: &(), - _: &Connection, - qh: &QueueHandle, - ) { - if let wl_seat::Event::Capabilities { - capabilities: WEnum::Value(capabilities), - } = event - { - let client = state.get_client(); - let mut state = client.borrow_mut(); - if capabilities.contains(wl_seat::Capability::Keyboard) { - let keyboard = seat.get_keyboard(qh, ()); + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + let client = state.get_client(); + let mut state = client.borrow_mut(); + if capabilities.contains(wl_seat::Capability::Keyboard) { + let keyboard = seat.get_keyboard(qh, ()); - if let Some(text_input) = state.text_input.take() { - text_input.destroy(); - state.ime_pre_edit = None; - state.composing = false; - } + if let Some(text_input) = state.text_input.take() { + text_input.destroy(); + state.ime_pre_edit = None; + state.composing = false; + } - state.text_input = state - .globals - .text_input_manager - .as_ref() - .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); + state.text_input = state + .globals + .text_input_manager + .as_ref() + .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); - if let Some(wl_keyboard) = &state.wl_keyboard { - wl_keyboard.release(); - } + if let Some(wl_keyboard) = &state.wl_keyboard { + wl_keyboard.release(); + } - state.wl_keyboard = Some(keyboard); - } - if capabilities.contains(wl_seat::Capability::Pointer) { - let pointer = seat.get_pointer(qh, ()); + state.wl_keyboard = Some(keyboard); + } + if capabilities.contains(wl_seat::Capability::Pointer) { + let pointer = seat.get_pointer(qh, ()); - if let Some(cursor_shape_device) = state.cursor_shape_device.take() { - cursor_shape_device.destroy(); - } + if let Some(cursor_shape_device) = state.cursor_shape_device.take() { + cursor_shape_device.destroy(); + } - state.cursor_shape_device = state - .globals - .cursor_shape_manager - .as_ref() - .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ())); + state.cursor_shape_device = state + .globals + .cursor_shape_manager + .as_ref() + .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ())); - state.pinch_gesture = state.globals.gesture_manager.as_ref().map( - |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| { - gesture_manager.get_pinch_gesture(&pointer, qh, ()) - }, - ); + state.pinch_gesture = state.globals.gesture_manager.as_ref().map( + |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| { + gesture_manager.get_pinch_gesture(&pointer, qh, ()) + }, + ); - state.hold_gesture = state.globals.gesture_manager.as_ref().and_then( - |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| { - (gesture_manager.version() >= 3) - .then(|| gesture_manager.get_hold_gesture(&pointer, qh, ())) - }, - ); + state.hold_gesture = state.globals.gesture_manager.as_ref().and_then( + |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| { + (gesture_manager.version() >= 3) + .then(|| gesture_manager.get_hold_gesture(&pointer, qh, ())) + }, + ); - if let Some(wl_pointer) = &state.wl_pointer { - wl_pointer.release(); - } + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } - state.wl_pointer = Some(pointer); - } - } - } + state.wl_pointer = Some(pointer); + } + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &wl_keyboard::WlKeyboard, - event: wl_keyboard::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); - match event { - wl_keyboard::Event::RepeatInfo { rate, delay } => { - state.repeat.characters_per_second = rate as u32; - state.repeat.delay = Duration::from_millis(delay as u64); - } - wl_keyboard::Event::Keymap { - format: WEnum::Value(format), - fd, - size, - .. - } => { - if format != wl_keyboard::KeymapFormat::XkbV1 { - log::error!("Received keymap format {:?}, expected XkbV1", format); - return; - } - let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); - let keymap = unsafe { - xkb::Keymap::new_from_fd( - &xkb_context, - fd, - size as usize, - XKB_KEYMAP_FORMAT_TEXT_V1, - KEYMAP_COMPILE_NO_FLAGS, - ) - .log_err() - .flatten() - .expect("Failed to create keymap") - }; - state.keymap_state = Some(xkb::State::new(&keymap)); - state.compose_state = get_xkb_compose_state(&xkb_context); - drop(state); + fn event( + this: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + wl_keyboard::Event::RepeatInfo { rate, delay } => { + state.repeat.characters_per_second = rate as u32; + state.repeat.delay = Duration::from_millis(delay as u64); + } + wl_keyboard::Event::Keymap { + format: WEnum::Value(format), + fd, + size, + .. + } => { + if format != wl_keyboard::KeymapFormat::XkbV1 { + log::error!("Received keymap format {:?}, expected XkbV1", format); + return; + } + let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb_context, + fd, + size as usize, + XKB_KEYMAP_FORMAT_TEXT_V1, + KEYMAP_COMPILE_NO_FLAGS, + ) + .log_err() + .flatten() + .expect("Failed to create keymap") + }; + state.keymap_state = Some(xkb::State::new(&keymap)); + state.compose_state = get_xkb_compose_state(&xkb_context); + drop(state); - this.handle_keyboard_layout_change(); - } - wl_keyboard::Event::Enter { surface, .. } => { - state.keyboard_focused_window = get_window(&mut state, &surface.id()); - state.enter_token = Some(()); + this.handle_keyboard_layout_change(); + } + wl_keyboard::Event::Enter { surface, .. } => { + state.keyboard_focused_window = get_window(&mut state, &surface.id()); + state.enter_token = Some(()); - if let Some(window) = state.keyboard_focused_window.clone() { - drop(state); - window.set_focused(true); - } - } - wl_keyboard::Event::Leave { surface, .. } => { - let keyboard_focused_window = get_window(&mut state, &surface.id()); - state.keyboard_focused_window = None; - state.enter_token.take(); - // Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly - state.repeat.current_id += 1; - state.restore_cursor_after_hide(); + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + window.set_focused(true); + } + } + wl_keyboard::Event::Leave { surface, .. } => { + let keyboard_focused_window = get_window(&mut state, &surface.id()); + state.keyboard_focused_window = None; + state.enter_token.take(); + // Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly + state.repeat.current_id += 1; + state.restore_cursor_after_hide(); - if let Some(window) = keyboard_focused_window { - if let Some(ref mut compose) = state.compose_state { - compose.reset(); - } - state.pre_edit_text.take(); - drop(state); - window.handle_ime(ImeInput::DeleteText); - window.set_focused(false); - } - } - wl_keyboard::Event::Modifiers { - mods_depressed, - mods_latched, - mods_locked, - group, - .. - } => { - let focused_window = state.keyboard_focused_window.clone(); + if let Some(window) = keyboard_focused_window { + if let Some(ref mut compose) = state.compose_state { + compose.reset(); + } + state.pre_edit_text.take(); + drop(state); + window.handle_ime(ImeInput::DeleteText); + window.set_focused(false); + } + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + let focused_window = state.keyboard_focused_window.clone(); - let keymap_state = state.keymap_state.as_mut().unwrap(); - let old_layout = - keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); - keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); - state.modifiers = modifiers_from_xkb(keymap_state); - let keymap_state = state.keymap_state.as_mut().unwrap(); - state.capslock = capslock_from_xkb(keymap_state); + let keymap_state = state.keymap_state.as_mut().unwrap(); + let old_layout = + keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + state.modifiers = modifiers_from_xkb(keymap_state); + let keymap_state = state.keymap_state.as_mut().unwrap(); + state.capslock = capslock_from_xkb(keymap_state); - let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers: state.modifiers, - capslock: state.capslock, - }); - drop(state); + let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: state.modifiers, + capslock: state.capslock, + }); + drop(state); - if let Some(focused_window) = focused_window { - focused_window.handle_input(input); - } + if let Some(focused_window) = focused_window { + focused_window.handle_input(input); + } - if group != old_layout { - this.handle_keyboard_layout_change(); - } - } - wl_keyboard::Event::Key { - serial, - key, - state: WEnum::Value(key_state), - .. - } => { - state.serial_tracker.update(SerialKind::KeyPress, serial); + if group != old_layout { + this.handle_keyboard_layout_change(); + } + } + wl_keyboard::Event::Key { + serial, + key, + state: WEnum::Value(key_state), + .. + } => { + state.serial_tracker.update(SerialKind::KeyPress, serial); - let focused_window = state.keyboard_focused_window.clone(); - let Some(focused_window) = focused_window else { - return; - }; + let focused_window = state.keyboard_focused_window.clone(); + let Some(focused_window) = focused_window else { + return; + }; - let keymap_state = state.keymap_state.as_ref().unwrap(); - let keycode = Keycode::from(key + MIN_KEYCODE); - let keysym = keymap_state.key_get_one_sym(keycode); + let keymap_state = state.keymap_state.as_ref().unwrap(); + let keycode = Keycode::from(key + MIN_KEYCODE); + let keysym = keymap_state.key_get_one_sym(keycode); - match key_state { - wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { - let mut keystroke = - keystroke_from_xkb(keymap_state, state.modifiers, keycode); - if let Some(mut compose) = state.compose_state.take() { - compose.feed(keysym); - match compose.status() { - xkb::Status::Composing => { - keystroke.key_char = None; - state.pre_edit_text = - compose.utf8().or(keystroke_underlying_dead_key(keysym)); - let pre_edit = - state.pre_edit_text.clone().unwrap_or(String::default()); - drop(state); - focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit)); - state = client.borrow_mut(); - } + match key_state { + wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { + let mut keystroke = + keystroke_from_xkb(keymap_state, state.modifiers, keycode); + if let Some(mut compose) = state.compose_state.take() { + compose.feed(keysym); + match compose.status() { + xkb::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = + compose.utf8().or(keystroke_underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit)); + state = client.borrow_mut(); + } - xkb::Status::Composed => { - state.pre_edit_text.take(); - keystroke.key_char = compose.utf8(); - if let Some(keysym) = compose.keysym() { - keystroke.key = xkb::keysym_get_name(keysym); - } - } - xkb::Status::Cancelled => { - let pre_edit = state.pre_edit_text.take(); - let new_pre_edit = keystroke_underlying_dead_key(keysym); - state.pre_edit_text = new_pre_edit.clone(); - drop(state); - if let Some(pre_edit) = pre_edit { - focused_window.handle_ime(ImeInput::InsertText(pre_edit)); - } - if let Some(current_key) = new_pre_edit { - focused_window - .handle_ime(ImeInput::SetMarkedText(current_key)); - } - compose.feed(keysym); - state = client.borrow_mut(); - } - _ => {} - } - state.compose_state = Some(compose); - } - let input = PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held: false, - prefer_character_input: false, - }); + xkb::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose.utf8(); + if let Some(keysym) = compose.keysym() { + keystroke.key = xkb::keysym_get_name(keysym); + } + } + xkb::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + let new_pre_edit = keystroke_underlying_dead_key(keysym); + state.pre_edit_text = new_pre_edit.clone(); + drop(state); + if let Some(pre_edit) = pre_edit { + focused_window.handle_ime(ImeInput::InsertText(pre_edit)); + } + if let Some(current_key) = new_pre_edit { + focused_window + .handle_ime(ImeInput::SetMarkedText(current_key)); + } + compose.feed(keysym); + state = client.borrow_mut(); + } + _ => {} + } + state.compose_state = Some(compose); + } + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + prefer_character_input: false, + }); - state.repeat.current_id += 1; - state.repeat.current_keycode = Some(keycode); + state.repeat.current_id += 1; + state.repeat.current_keycode = Some(keycode); - let rate = state.repeat.characters_per_second; - let repeat_interval = Duration::from_secs(1) / rate.max(1); - let id = state.repeat.current_id; - state - .loop_handle - .insert_source(Timer::from_duration(state.repeat.delay), { - let input = PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: true, - prefer_character_input: false, - }); - move |event_timestamp, _metadata, this| { - let client = this.get_client(); - let state = client.borrow(); - let is_repeating = id == state.repeat.current_id - && state.repeat.current_keycode.is_some() - && state.keyboard_focused_window.is_some(); + let rate = state.repeat.characters_per_second; + let repeat_interval = Duration::from_secs(1) / rate.max(1); + let id = state.repeat.current_id; + state + .loop_handle + .insert_source(Timer::from_duration(state.repeat.delay), { + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: true, + prefer_character_input: false, + }); + move |event_timestamp, _metadata, this| { + let client = this.get_client(); + let state = client.borrow(); + let is_repeating = id == state.repeat.current_id + && state.repeat.current_keycode.is_some() + && state.keyboard_focused_window.is_some(); - if !is_repeating || rate == 0 { - return TimeoutAction::Drop; - } + if !is_repeating || rate == 0 { + return TimeoutAction::Drop; + } - let focused_window = - state.keyboard_focused_window.as_ref().unwrap().clone(); + let focused_window = + state.keyboard_focused_window.as_ref().unwrap().clone(); - drop(state); - focused_window.handle_input(input.clone()); + drop(state); + focused_window.handle_input(input.clone()); - // If the new scheduled time is in the past the event will repeat as soon as possible - TimeoutAction::ToInstant(event_timestamp + repeat_interval) - } - }) - .unwrap(); + // If the new scheduled time is in the past the event will repeat as soon as possible + TimeoutAction::ToInstant(event_timestamp + repeat_interval) + } + }) + .unwrap(); - drop(state); - focused_window.handle_input(input); - } - wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { - let input = PlatformInput::KeyUp(KeyUpEvent { - keystroke: keystroke_from_xkb(keymap_state, state.modifiers, keycode), - }); + drop(state); + focused_window.handle_input(input); + } + wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { + let input = PlatformInput::KeyUp(KeyUpEvent { + keystroke: keystroke_from_xkb(keymap_state, state.modifiers, keycode), + }); - if state.repeat.current_keycode == Some(keycode) { - state.repeat.current_keycode = None; - } + if state.repeat.current_keycode == Some(keycode) { + state.repeat.current_keycode = None; + } - drop(state); - focused_window.handle_input(input); - } - _ => {} - } - } - _ => {} - } - } + drop(state); + focused_window.handle_input(input); + } + _ => {} + } + } + _ => {} + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - text_input: &zwp_text_input_v3::ZwpTextInputV3, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); - match event { - zwp_text_input_v3::Event::Enter { .. } => { - drop(state); - this.enable_ime(); - } - zwp_text_input_v3::Event::Leave { .. } => { - drop(state); - this.disable_ime(); - } - zwp_text_input_v3::Event::CommitString { text } => { - state.composing = false; - let Some(window) = state.keyboard_focused_window.clone() else { - return; - }; + fn event( + this: &mut Self, + text_input: &zwp_text_input_v3::ZwpTextInputV3, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + zwp_text_input_v3::Event::Enter { .. } => { + drop(state); + this.enable_ime(); + } + zwp_text_input_v3::Event::Leave { .. } => { + drop(state); + this.disable_ime(); + } + zwp_text_input_v3::Event::CommitString { text } => { + state.composing = false; + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; - if let Some(commit_text) = text { - drop(state); - // IBus Intercepts keys like `a`, `b`, but those keys are needed for vim mode. - // We should only send ASCII characters to Zed, otherwise a user could remap a letter like `か` or `相`. - if commit_text.len() == 1 { - window.handle_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke: Keystroke { - modifiers: Modifiers::default(), - key: commit_text.clone(), - key_char: Some(commit_text), - }, - is_held: false, - prefer_character_input: false, - })); - } else { - window.handle_ime(ImeInput::InsertText(commit_text)); - } - } - } - zwp_text_input_v3::Event::PreeditString { text, .. } => { - state.composing = true; - state.ime_pre_edit = text; - } - zwp_text_input_v3::Event::Done { serial } => { - let last_serial = state.serial_tracker.get(SerialKind::InputMethod); - state.serial_tracker.update(SerialKind::InputMethod, serial); - let Some(window) = state.keyboard_focused_window.clone() else { - return; - }; + if let Some(commit_text) = text { + drop(state); + // IBus Intercepts keys like `a`, `b`, but those keys are needed for vim mode. + // We should only send ASCII characters to Zed, otherwise a user could remap a letter like `か` or `相`. + if commit_text.len() == 1 { + window.handle_input(PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: commit_text.clone(), + key_char: Some(commit_text), + }, + is_held: false, + prefer_character_input: false, + })); + } else { + window.handle_ime(ImeInput::InsertText(commit_text)); + } + } + } + zwp_text_input_v3::Event::PreeditString { text, .. } => { + state.composing = true; + state.ime_pre_edit = text; + } + zwp_text_input_v3::Event::Done { serial } => { + let last_serial = state.serial_tracker.get(SerialKind::InputMethod); + state.serial_tracker.update(SerialKind::InputMethod, serial); + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; - if let Some(text) = state.ime_pre_edit.take() { - drop(state); - window.handle_ime(ImeInput::SetMarkedText(text)); - if let Some(area) = window.get_ime_area() { - text_input.set_cursor_rectangle( - f32::from(area.origin.x) as i32, - f32::from(area.origin.y) as i32, - f32::from(area.size.width) as i32, - f32::from(area.size.height) as i32, - ); - if last_serial == serial { - text_input.commit(); - } - } - } else { - state.composing = false; - drop(state); - window.handle_ime(ImeInput::DeleteText); - } - } - _ => {} - } - } + if let Some(text) = state.ime_pre_edit.take() { + drop(state); + window.handle_ime(ImeInput::SetMarkedText(text)); + if let Some(area) = window.get_ime_area() { + text_input.set_cursor_rectangle( + f32::from(area.origin.x) as i32, + f32::from(area.origin.y) as i32, + f32::from(area.size.width) as i32, + f32::from(area.size.height) as i32, + ); + if last_serial == serial { + text_input.commit(); + } + } + } else { + state.composing = false; + drop(state); + window.handle_ime(ImeInput::DeleteText); + } + } + _ => {} + } + } } fn linux_button_to_gpui(button: u32) -> Option { - // These values are coming from . - const BTN_LEFT: u32 = 0x110; - const BTN_RIGHT: u32 = 0x111; - const BTN_MIDDLE: u32 = 0x112; - const BTN_SIDE: u32 = 0x113; - const BTN_EXTRA: u32 = 0x114; - const BTN_FORWARD: u32 = 0x115; - const BTN_BACK: u32 = 0x116; + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; - Some(match button { - BTN_LEFT => MouseButton::Left, - BTN_RIGHT => MouseButton::Right, - BTN_MIDDLE => MouseButton::Middle, - BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), - BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), - _ => return None, - }) + Some(match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), + BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - wl_pointer: &wl_pointer::WlPointer, - event: wl_pointer::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + wl_pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - match event { - wl_pointer::Event::Enter { - serial, - surface, - surface_x, - surface_y, - .. - } => { - state.serial_tracker.update(SerialKind::MouseEnter, serial); - state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); - state.button_pressed = None; + match event { + wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + .. + } => { + state.serial_tracker.update(SerialKind::MouseEnter, serial); + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.button_pressed = None; - if let Some(window) = get_window(&mut state, &surface.id()) { - state.mouse_focused_window = Some(window.clone()); + if let Some(window) = get_window(&mut state, &surface.id()) { + state.mouse_focused_window = Some(window.clone()); - if state.enter_token.is_some() { - state.enter_token = None; - } - state.restore_cursor_after_hide(); - if let Some(style) = state.cursor_style { - if let Some(cursor_shape_device) = &state.cursor_shape_device { - cursor_shape_device.set_shape(serial, to_shape(style)); - } else { - let scale = window.primary_output_scale(); - state.cursor.set_icon( - wl_pointer, - serial, - cursor_style_to_icon_names(style), - scale, - ); - } - } - drop(state); - window.set_hovered(true); - } - } - wl_pointer::Event::Leave { .. } => { - if let Some(focused_window) = state.mouse_focused_window.clone() { - let input = PlatformInput::MouseExited(MouseExitEvent { - position: state.mouse_location.unwrap(), - pressed_button: state.button_pressed, - modifiers: state.modifiers, - }); - state.mouse_focused_window = None; - state.mouse_location = None; - state.button_pressed = None; - state.cursor_hidden_window = None; + if state.enter_token.is_some() { + state.enter_token = None; + } + state.restore_cursor_after_hide(); + if let Some(style) = state.cursor_style { + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + } else { + let scale = window.primary_output_scale(); + state.cursor.set_icon( + wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); + } + } + drop(state); + window.set_hovered(true); + } + } + wl_pointer::Event::Leave { .. } => { + if let Some(focused_window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseExited(MouseExitEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + state.mouse_focused_window = None; + state.mouse_location = None; + state.button_pressed = None; + state.cursor_hidden_window = None; - drop(state); - focused_window.handle_input(input); - focused_window.set_hovered(false); - } - } - wl_pointer::Event::Motion { - surface_x, - surface_y, - .. - } => { - if state.mouse_focused_window.is_none() { - return; - } - state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); - state.restore_cursor_after_hide(); - let kinetic_input = state.kinetic_scroll.cancel(); + drop(state); + focused_window.handle_input(input); + focused_window.set_hovered(false); + } + } + wl_pointer::Event::Motion { + surface_x, + surface_y, + .. + } => { + if state.mouse_focused_window.is_none() { + return; + } + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.restore_cursor_after_hide(); + let kinetic_input = state.kinetic_scroll.cancel(); - if let Some(window) = state.mouse_focused_window.clone() { - if window.is_blocked() { - let default_style = CursorStyle::Arrow; - if state.cursor_style != Some(default_style) { - let serial = state.serial_tracker.get(SerialKind::MouseEnter); - state.cursor_style = Some(default_style); + if let Some(window) = state.mouse_focused_window.clone() { + if window.is_blocked() { + let default_style = CursorStyle::Arrow; + if state.cursor_style != Some(default_style) { + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + state.cursor_style = Some(default_style); - if let Some(cursor_shape_device) = &state.cursor_shape_device { - cursor_shape_device.set_shape(serial, to_shape(default_style)); - } else { - // cursor-shape-v1 isn't supported, set the cursor using a surface. - let wl_pointer = state - .wl_pointer - .clone() - .expect("window is focused by pointer"); - let scale = window.primary_output_scale(); - state.cursor.set_icon( - &wl_pointer, - serial, - cursor_style_to_icon_names(default_style), - scale, - ); - } - } - } - if state - .keyboard_focused_window - .as_ref() - .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) - { - state.enter_token = None; - } - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: state.mouse_location.unwrap(), - pressed_button: state.button_pressed, - modifiers: state.modifiers, - }); - drop(state); - if let Some((window, input)) = kinetic_input { - window.handle_input(input); - } - window.handle_input(input); - } - } - wl_pointer::Event::Button { - serial, - button, - state: WEnum::Value(button_state), - .. - } => { - state.serial_tracker.update(SerialKind::MousePress, serial); - let button = linux_button_to_gpui(button); - let Some(button) = button else { return }; - if state.mouse_focused_window.is_none() { - return; - } - match button_state { - wl_pointer::ButtonState::Pressed => { - if let Some(window) = state.keyboard_focused_window.clone() { - if state.composing && state.text_input.is_some() { - drop(state); - // text_input_v3 don't have something like a reset function - this.disable_ime(); - this.enable_ime(); - window.handle_ime(ImeInput::UnmarkText); - state = client.borrow_mut(); - } else if let (Some(text), Some(compose)) = - (state.pre_edit_text.take(), state.compose_state.as_mut()) - { - compose.reset(); - drop(state); - window.handle_ime(ImeInput::InsertText(text)); - state = client.borrow_mut(); - } - } - let click_elapsed = state.click.last_click.elapsed(); + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(default_style)); + } else { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial, + cursor_style_to_icon_names(default_style), + scale, + ); + } + } + } + if state + .keyboard_focused_window + .as_ref() + .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) + { + state.enter_token = None; + } + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + drop(state); + if let Some((window, input)) = kinetic_input { + window.handle_input(input); + } + window.handle_input(input); + } + } + wl_pointer::Event::Button { + serial, + button, + state: WEnum::Value(button_state), + .. + } => { + state.serial_tracker.update(SerialKind::MousePress, serial); + let button = linux_button_to_gpui(button); + let Some(button) = button else { return }; + if state.mouse_focused_window.is_none() { + return; + } + match button_state { + wl_pointer::ButtonState::Pressed => { + if let Some(window) = state.keyboard_focused_window.clone() { + if state.composing && state.text_input.is_some() { + drop(state); + // text_input_v3 don't have something like a reset function + this.disable_ime(); + this.enable_ime(); + window.handle_ime(ImeInput::UnmarkText); + state = client.borrow_mut(); + } else if let (Some(text), Some(compose)) = + (state.pre_edit_text.take(), state.compose_state.as_mut()) + { + compose.reset(); + drop(state); + window.handle_ime(ImeInput::InsertText(text)); + state = client.borrow_mut(); + } + } + let click_elapsed = state.click.last_click.elapsed(); - if click_elapsed < DOUBLE_CLICK_INTERVAL - && state - .click - .last_mouse_button - .is_some_and(|prev_button| prev_button == button) - && is_within_click_distance( - state.click.last_location, - state.mouse_location.unwrap(), - ) - { - state.click.current_count += 1; - } else { - state.click.current_count = 1; - } + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .click + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance( + state.click.last_location, + state.mouse_location.unwrap(), + ) { + state.click.current_count += 1; + } else { + state.click.current_count = 1; + } - state.click.last_click = Instant::now(); - state.click.last_mouse_button = Some(button); - state.click.last_location = state.mouse_location.unwrap(); + state.click.last_click = Instant::now(); + state.click.last_mouse_button = Some(button); + state.click.last_location = state.mouse_location.unwrap(); - state.button_pressed = Some(button); + state.button_pressed = Some(button); - if let Some(window) = state.mouse_focused_window.clone() { - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: state.mouse_location.unwrap(), - modifiers: state.modifiers, - click_count: state.click.current_count, - first_mouse: state.enter_token.take().is_some(), - }); - drop(state); - window.handle_input(input); - } - } - wl_pointer::ButtonState::Released => { - state.button_pressed = None; + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + first_mouse: state.enter_token.take().is_some(), + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; - if let Some(window) = state.mouse_focused_window.clone() { - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: state.mouse_location.unwrap(), - modifiers: state.modifiers, - click_count: state.click.current_count, - }); - drop(state); - window.handle_input(input); - } - } - _ => {} - } - } + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + }); + drop(state); + window.handle_input(input); + } + } + _ => {} + } + } - // Axis Events - wl_pointer::Event::AxisSource { - axis_source: WEnum::Value(axis_source), - } => { - state.axis_source = axis_source; - } - wl_pointer::Event::Axis { - axis: WEnum::Value(axis), - value, - .. - } => { - if state.axis_source == AxisSource::Wheel { - return; - } - if state.axis_source == AxisSource::Finger { - state.kinetic_scroll.start_finger_scroll(); - } - let axis = if state.modifiers.shift { - wl_pointer::Axis::HorizontalScroll - } else { - axis - }; - let axis_modifier = match axis { - wl_pointer::Axis::VerticalScroll => state.vertical_modifier, - wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, - _ => 1.0, - }; - state.scroll_event_received = true; - let scroll_delta = state - .continuous_scroll_delta - .get_or_insert(point(px(0.0), px(0.0))); - let modifier = 3.0; - match axis { - wl_pointer::Axis::VerticalScroll => { - scroll_delta.y += px(value as f32 * modifier * axis_modifier); - } - wl_pointer::Axis::HorizontalScroll => { - scroll_delta.x += px(value as f32 * modifier * axis_modifier); - } - _ => unreachable!(), - } - } - wl_pointer::Event::AxisDiscrete { - axis: WEnum::Value(axis), - discrete, - } => { - state.scroll_event_received = true; - let axis = if state.modifiers.shift { - wl_pointer::Axis::HorizontalScroll - } else { - axis - }; - let axis_modifier = match axis { - wl_pointer::Axis::VerticalScroll => state.vertical_modifier, - wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, - _ => 1.0, - }; + // Axis Events + wl_pointer::Event::AxisSource { + axis_source: WEnum::Value(axis_source), + } => { + state.axis_source = axis_source; + } + wl_pointer::Event::Axis { + axis: WEnum::Value(axis), + value, + .. + } => { + if state.axis_source == AxisSource::Wheel { + return; + } + if state.axis_source == AxisSource::Finger { + state.kinetic_scroll.start_finger_scroll(); + } + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + state.scroll_event_received = true; + let scroll_delta = state + .continuous_scroll_delta + .get_or_insert(point(px(0.0), px(0.0))); + let modifier = 3.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += px(value as f32 * modifier * axis_modifier); + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += px(value as f32 * modifier * axis_modifier); + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisDiscrete { + axis: WEnum::Value(axis), + discrete, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; - let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); - match axis { - wl_pointer::Axis::VerticalScroll => { - scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES; - } - wl_pointer::Axis::HorizontalScroll => { - scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES; - } - _ => unreachable!(), - } - } - wl_pointer::Event::AxisValue120 { - axis: WEnum::Value(axis), - value120, - } => { - state.scroll_event_received = true; - let axis = if state.modifiers.shift { - wl_pointer::Axis::HorizontalScroll - } else { - axis - }; - let axis_modifier = match axis { - wl_pointer::Axis::VerticalScroll => state.vertical_modifier, - wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, - _ => unreachable!(), - }; + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisValue120 { + axis: WEnum::Value(axis), + value120, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => unreachable!(), + }; - let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); - let wheel_percent = value120 as f32 / 120.0; - match axis { - wl_pointer::Axis::VerticalScroll => { - scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES; - } - wl_pointer::Axis::HorizontalScroll => { - scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES; - } - _ => unreachable!(), - } - } - wl_pointer::Event::AxisStop { .. } => { - if state.axis_source == AxisSource::Finger - && state.kinetic_scroll.stop_finger_scroll() - { - state.scroll_event_received = true; - } - } - wl_pointer::Event::Frame => { - if state.scroll_event_received { - state.scroll_event_received = false; - let continuous = state.continuous_scroll_delta.take(); - let discrete = state.discrete_scroll_delta.take(); - if let Some(continuous) = continuous { - let touch_phase = state.kinetic_scroll.touch_phase(); - if state.axis_source == AxisSource::Finger { - state - .kinetic_scroll - .record_delta(Instant::now(), continuous); - } - let mut kinetic_input = None; - if state.kinetic_scroll.has_pending_stop() { - if let (Some(window), Some(position)) = - (state.mouse_focused_window.clone(), state.mouse_location) - { - let modifiers = state.modifiers; - kinetic_input = state - .kinetic_scroll - .finish_pending_stop(window, position, modifiers); - } - } - if let Some(window) = state.mouse_focused_window.clone() { - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location.unwrap(), - delta: ScrollDelta::Pixels(continuous), - modifiers: state.modifiers, - touch_phase, - }); - drop(state); - window.handle_input(input); - if let Some((window, input)) = kinetic_input { - window.handle_input(input); - } - } - } else if let Some(discrete) = discrete - && let Some(window) = state.mouse_focused_window.clone() - { - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location.unwrap(), - delta: ScrollDelta::Lines(discrete), - modifiers: state.modifiers, - touch_phase: TouchPhase::Moved, - }); - drop(state); - window.handle_input(input); - } else if state.kinetic_scroll.has_pending_stop() - && let (Some(window), Some(position)) = - (state.mouse_focused_window.clone(), state.mouse_location) - { - let modifiers = state.modifiers; - if let Some((window, input)) = state - .kinetic_scroll - .finish_pending_stop(window, position, modifiers) - { - drop(state); - window.handle_input(input); - } - } - } - } - _ => {} - } - } + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + let wheel_percent = value120 as f32 / 120.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisStop { .. } => { + if state.axis_source == AxisSource::Finger + && state.kinetic_scroll.stop_finger_scroll() + { + state.scroll_event_received = true; + } + } + wl_pointer::Event::Frame => { + if state.scroll_event_received { + state.scroll_event_received = false; + let continuous = state.continuous_scroll_delta.take(); + let discrete = state.discrete_scroll_delta.take(); + if let Some(continuous) = continuous { + let touch_phase = state.kinetic_scroll.touch_phase(); + if state.axis_source == AxisSource::Finger { + state + .kinetic_scroll + .record_delta(Instant::now(), continuous); + } + let mut kinetic_input = None; + if state.kinetic_scroll.has_pending_stop() { + if let (Some(window), Some(position)) = + (state.mouse_focused_window.clone(), state.mouse_location) + { + let modifiers = state.modifiers; + kinetic_input = state + .kinetic_scroll + .finish_pending_stop(window, position, modifiers); + } + } + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Pixels(continuous), + modifiers: state.modifiers, + touch_phase, + }); + drop(state); + window.handle_input(input); + if let Some((window, input)) = kinetic_input { + window.handle_input(input); + } + } + } else if let Some(discrete) = discrete + && let Some(window) = state.mouse_focused_window.clone() + { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } else if state.kinetic_scroll.has_pending_stop() + && let (Some(window), Some(position)) = + (state.mouse_focused_window.clone(), state.mouse_location) + { + let modifiers = state.modifiers; + if let Some((window, input)) = state + .kinetic_scroll + .finish_pending_stop(window, position, modifiers) + { + drop(state); + window.handle_input(input); + } + } + } + } + _ => {} + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - _this: &mut Self, - _: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1, - _: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - // The gesture manager doesn't generate events - } + fn event( + _this: &mut Self, + _: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + // The gesture manager doesn't generate events + } } impl Dispatch - for WaylandClientStatePtr + for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - use gpui::PinchEvent; + fn event( + this: &mut Self, + _: &zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + use gpui::PinchEvent; - let client = this.get_client(); - let mut state = client.borrow_mut(); + let client = this.get_client(); + let mut state = client.borrow_mut(); - let Some(window) = state.mouse_focused_window.clone() else { - return; - }; + let Some(window) = state.mouse_focused_window.clone() else { + return; + }; - match event { - zwp_pointer_gesture_pinch_v1::Event::Begin { - serial: _, - time: _, - surface: _, - fingers: _, - } => { - if let Some((window, input)) = state.kinetic_scroll.cancel() { - drop(state); - window.handle_input(input); - state = client.borrow_mut(); - } - state.pinch_scale = 1.0; - let input = PlatformInput::Pinch(PinchEvent { - position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), - delta: 0.0, - modifiers: state.modifiers, - phase: TouchPhase::Started, - }); - drop(state); - window.handle_input(input); - } - zwp_pointer_gesture_pinch_v1::Event::Update { time: _, scale, .. } => { - let new_absolute_scale = scale as f32; - let previous_scale = state.pinch_scale; - let zoom_delta = new_absolute_scale - previous_scale; - state.pinch_scale = new_absolute_scale; + match event { + zwp_pointer_gesture_pinch_v1::Event::Begin { + serial: _, + time: _, + surface: _, + fingers: _, + } => { + if let Some((window, input)) = state.kinetic_scroll.cancel() { + drop(state); + window.handle_input(input); + state = client.borrow_mut(); + } + state.pinch_scale = 1.0; + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: 0.0, + modifiers: state.modifiers, + phase: TouchPhase::Started, + }); + drop(state); + window.handle_input(input); + } + zwp_pointer_gesture_pinch_v1::Event::Update { time: _, scale, .. } => { + let new_absolute_scale = scale as f32; + let previous_scale = state.pinch_scale; + let zoom_delta = new_absolute_scale - previous_scale; + state.pinch_scale = new_absolute_scale; - let input = PlatformInput::Pinch(PinchEvent { - position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), - delta: zoom_delta, - modifiers: state.modifiers, - phase: TouchPhase::Moved, - }); - drop(state); - window.handle_input(input); - } - zwp_pointer_gesture_pinch_v1::Event::End { - serial: _, - time: _, - cancelled: _, - } => { - state.pinch_scale = 1.0; - let input = PlatformInput::Pinch(PinchEvent { - position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), - delta: 0.0, - modifiers: state.modifiers, - phase: TouchPhase::Ended, - }); - drop(state); - window.handle_input(input); - } - _ => {} - } - } + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: zoom_delta, + modifiers: state.modifiers, + phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + zwp_pointer_gesture_pinch_v1::Event::End { + serial: _, + time: _, + cancelled: _, + } => { + state.pinch_scale = 1.0; + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: 0.0, + modifiers: state.modifiers, + phase: TouchPhase::Ended, + }); + drop(state); + window.handle_input(input); + } + _ => {} + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &zwp_pointer_gesture_hold_v1::ZwpPointerGestureHoldV1, - event: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let zwp_pointer_gesture_hold_v1::Event::Begin { .. } = event { - let client = this.get_client(); - let mut state = client.borrow_mut(); - if let Some((window, input)) = state.kinetic_scroll.cancel() { - drop(state); - window.handle_input(input); - } - } - } + fn event( + this: &mut Self, + _: &zwp_pointer_gesture_hold_v1::ZwpPointerGestureHoldV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let zwp_pointer_gesture_hold_v1::Event::Begin { .. } = event { + let client = this.get_client(); + let mut state = client.borrow_mut(); + if let Some((window, input)) = state.kinetic_scroll.cancel() { + drop(state); + window.handle_input(input); + } + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &wp_fractional_scale_v1::WpFractionalScaleV1, - event: ::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + _: &wp_fractional_scale_v1::WpFractionalScaleV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; - drop(state); - window.handle_fractional_scale_event(event); - } + drop(state); + window.handle_fractional_scale_event(event); + } } impl Dispatch - for WaylandClientStatePtr + for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, - event: zxdg_toplevel_decoration_v1::Event, - surface_id: &ObjectId, - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); - let Some(window) = get_window(&mut state, surface_id) else { - return; - }; + fn event( + this: &mut Self, + _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; - drop(state); - window.handle_toplevel_decoration_event(event); - } + drop(state); + window.handle_toplevel_decoration_event(event); + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &wl_data_device::WlDataDevice, - event: wl_data_device::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - match event { - // Clipboard - wl_data_device::Event::DataOffer { id: data_offer } => { - state.data_offers.push(DataOffer::new(data_offer)); - if state.data_offers.len() > 2 { - // At most we store a clipboard offer and a drag and drop offer. - state.data_offers.remove(0).inner.destroy(); - } - } - wl_data_device::Event::Selection { id: data_offer } => { - if let Some(offer) = data_offer { - let offer = state - .data_offers - .iter() - .find(|wrapper| wrapper.inner.id() == offer.id()); - let offer = offer.cloned(); - state.clipboard.set_offer(offer); - } else { - state.clipboard.set_offer(None); - } - } + match event { + // Clipboard + wl_data_device::Event::DataOffer { id: data_offer } => { + state.data_offers.push(DataOffer::new(data_offer)); + if state.data_offers.len() > 2 { + // At most we store a clipboard offer and a drag and drop offer. + state.data_offers.remove(0).inner.destroy(); + } + } + wl_data_device::Event::Selection { id: data_offer } => { + if let Some(offer) = data_offer { + let offer = state + .data_offers + .iter() + .find(|wrapper| wrapper.inner.id() == offer.id()); + let offer = offer.cloned(); + state.clipboard.set_offer(offer); + } else { + state.clipboard.set_offer(None); + } + } - // Drag and drop - wl_data_device::Event::Enter { - serial, - surface, - x, - y, - id: data_offer, - } => { - state.serial_tracker.update(SerialKind::DataDevice, serial); - if let Some(data_offer) = data_offer { - let Some(drag_window) = get_window(&mut state, &surface.id()) else { - return; - }; + // Drag and drop + wl_data_device::Event::Enter { + serial, + surface, + x, + y, + id: data_offer, + } => { + state.serial_tracker.update(SerialKind::DataDevice, serial); + if let Some(data_offer) = data_offer { + let Some(drag_window) = get_window(&mut state, &surface.id()) else { + return; + }; - const ACTIONS: DndAction = DndAction::Copy; - data_offer.set_actions(ACTIONS, ACTIONS); + const ACTIONS: DndAction = DndAction::Copy; + data_offer.set_actions(ACTIONS, ACTIONS); - let pipe = Pipe::new().unwrap(); - data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { - BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) - }); - let fd = pipe.read; - drop(pipe.write); + let pipe = Pipe::new().unwrap(); + data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); - let read_task = state.common.background_executor.spawn(async { - let buffer = unsafe { read_fd(fd)? }; - let text = String::from_utf8(buffer)?; - anyhow::Ok(text) - }); + let read_task = state.common.background_executor.spawn(async { + let buffer = unsafe { read_fd(fd)? }; + let text = String::from_utf8(buffer)?; + anyhow::Ok(text) + }); - let this = this.clone(); - state - .common - .foreground_executor - .spawn(async move { - let file_list = match read_task.await { - Ok(list) => list, - Err(err) => { - log::error!("error reading drag and drop pipe: {err:?}"); - return; - } - }; + let this = this.clone(); + state + .common + .foreground_executor + .spawn(async move { + let file_list = match read_task.await { + Ok(list) => list, + Err(err) => { + log::error!("error reading drag and drop pipe: {err:?}"); + return; + } + }; - let paths: SmallVec<[_; 2]> = file_list - .lines() - .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| match url.to_file_path() { - Ok(url) => Some(url), - Err(()) => { - log::error!("Failed turn {url:?} into a file path"); - None - } - }) - .collect(); - let position = Point::new(x.into(), y.into()); + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| match url.to_file_path() { + Ok(url) => Some(url), + Err(()) => { + log::error!("Failed turn {url:?} into a file path"); + None + } + }) + .collect(); + let position = Point::new(x.into(), y.into()); - // Prevent dropping text from other programs. - if paths.is_empty() { - data_offer.destroy(); - return; - } + // Prevent dropping text from other programs. + if paths.is_empty() { + data_offer.destroy(); + return; + } - let input = PlatformInput::FileDrop(FileDropEvent::Entered { - position, - paths: gpui::ExternalPaths(paths), - }); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: gpui::ExternalPaths(paths), + }); - let client = this.get_client(); - let mut state = client.borrow_mut(); - state.drag.data_offer = Some(data_offer); - state.drag.window = Some(drag_window.clone()); - state.drag.position = position; + let client = this.get_client(); + let mut state = client.borrow_mut(); + state.drag.data_offer = Some(data_offer); + state.drag.window = Some(drag_window.clone()); + state.drag.position = position; - drop(state); - drag_window.handle_input(input); - }) - .detach(); - } - } - wl_data_device::Event::Motion { x, y, .. } => { - let Some(drag_window) = state.drag.window.clone() else { - return; - }; - let position = Point::new(x.into(), y.into()); - state.drag.position = position; + drop(state); + drag_window.handle_input(input); + }) + .detach(); + } + } + wl_data_device::Event::Motion { x, y, .. } => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let position = Point::new(x.into(), y.into()); + state.drag.position = position; - let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); - drop(state); - drag_window.handle_input(input); - } - wl_data_device::Event::Leave => { - let Some(drag_window) = state.drag.window.clone() else { - return; - }; - let data_offer = state.drag.data_offer.clone().unwrap(); - data_offer.destroy(); + let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Leave => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.destroy(); - state.drag.data_offer = None; - state.drag.window = None; + state.drag.data_offer = None; + state.drag.window = None; - let input = PlatformInput::FileDrop(FileDropEvent::Exited {}); - drop(state); - drag_window.handle_input(input); - } - wl_data_device::Event::Drop => { - let Some(drag_window) = state.drag.window.clone() else { - return; - }; - let data_offer = state.drag.data_offer.clone().unwrap(); - data_offer.finish(); - data_offer.destroy(); + let input = PlatformInput::FileDrop(FileDropEvent::Exited {}); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Drop => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.finish(); + data_offer.destroy(); - state.drag.data_offer = None; - state.drag.window = None; + state.drag.data_offer = None; + state.drag.window = None; - let input = PlatformInput::FileDrop(FileDropEvent::Submit { - position: state.drag.position, - }); - drop(state); - drag_window.handle_input(input); - } - _ => {} - } - } + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: state.drag.position, + }); + drop(state); + drag_window.handle_input(input); + } + _ => {} + } + } - event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ - wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), - ]); + event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ + wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), + ]); } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - data_offer: &wl_data_offer::WlDataOffer, - event: wl_data_offer::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - if let wl_data_offer::Event::Offer { mime_type } = event { - // Drag and drop - if mime_type == FILE_LIST_MIME_TYPE { - let serial = state.serial_tracker.get(SerialKind::DataDevice); - let mime_type = mime_type.clone(); - data_offer.accept(serial, Some(mime_type)); - } + if let wl_data_offer::Event::Offer { mime_type } = event { + // Drag and drop + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); + data_offer.accept(serial, Some(mime_type)); + } - // Clipboard - if let Some(offer) = state - .data_offers - .iter_mut() - .find(|wrapper| wrapper.inner.id() == data_offer.id()) - { - offer.add_mime_type(mime_type); - } - } - } + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); + } + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - this: &mut Self, - data_source: &wl_data_source::WlDataSource, - event: wl_data_source::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let state = client.borrow_mut(); + fn event( + this: &mut Self, + data_source: &wl_data_source::WlDataSource, + event: wl_data_source::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let state = client.borrow_mut(); - match event { - wl_data_source::Event::Send { mime_type, fd } => { - state.clipboard.send(mime_type, fd); - } - wl_data_source::Event::Cancelled => { - data_source.destroy(); - } - _ => {} - } - } + match event { + wl_data_source::Event::Send { mime_type, fd } => { + state.clipboard.send(mime_type, fd); + } + wl_data_source::Event::Cancelled => { + data_source.destroy(); + } + _ => {} + } + } } impl Dispatch - for WaylandClientStatePtr + for WaylandClientStatePtr { - fn event( - this: &mut Self, - _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, - event: zwp_primary_selection_device_v1::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + event: zwp_primary_selection_device_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - match event { - zwp_primary_selection_device_v1::Event::DataOffer { offer } => { - let old_offer = state.primary_data_offer.replace(DataOffer::new(offer)); - if let Some(old_offer) = old_offer { - old_offer.inner.destroy(); - } - } - zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => { - if data_offer.is_some() { - let offer = state.primary_data_offer.clone(); - state.clipboard.set_primary_offer(offer); - } else { - state.clipboard.set_primary_offer(None); - } - } - _ => {} - } - } + match event { + zwp_primary_selection_device_v1::Event::DataOffer { offer } => { + let old_offer = state.primary_data_offer.replace(DataOffer::new(offer)); + if let Some(old_offer) = old_offer { + old_offer.inner.destroy(); + } + } + zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => { + if data_offer.is_some() { + let offer = state.primary_data_offer.clone(); + state.clipboard.set_primary_offer(offer); + } else { + state.clipboard.set_primary_offer(None); + } + } + _ => {} + } + } - event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [ - zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()), - ]); + event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [ + zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()), + ]); } impl Dispatch - for WaylandClientStatePtr + for WaylandClientStatePtr { - fn event( - this: &mut Self, - _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, - event: zwp_primary_selection_offer_v1::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let mut state = client.borrow_mut(); + fn event( + this: &mut Self, + _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, + event: zwp_primary_selection_offer_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); - if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event - && let Some(offer) = state.primary_data_offer.as_mut() - { - offer.add_mime_type(mime_type); - } - } + if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event + && let Some(offer) = state.primary_data_offer.as_mut() + { + offer.add_mime_type(mime_type); + } + } } impl Dispatch - for WaylandClientStatePtr + for WaylandClientStatePtr { - fn event( - this: &mut Self, - selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, - event: zwp_primary_selection_source_v1::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let client = this.get_client(); - let state = client.borrow_mut(); + fn event( + this: &mut Self, + selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + event: zwp_primary_selection_source_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let state = client.borrow_mut(); - match event { - zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => { - state.clipboard.send_primary(mime_type, fd); - } - zwp_primary_selection_source_v1::Event::Cancelled => { - selection_source.destroy(); - } - _ => {} - } - } + match event { + zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => { + state.clipboard.send_primary(mime_type, fd); + } + zwp_primary_selection_source_v1::Event::Cancelled => { + selection_source.destroy(); + } + _ => {} + } + } } impl Dispatch for WaylandClientStatePtr { - fn event( - _: &mut Self, - _: &XdgWmDialogV1, - _: ::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } + fn event( + _: &mut Self, + _: &XdgWmDialogV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } } impl Dispatch for WaylandClientStatePtr { - fn event( - _state: &mut Self, - _proxy: &XdgDialogV1, - _event: ::Event, - _data: &(), - _conn: &Connection, - _qhandle: &QueueHandle, - ) { - } + fn event( + _state: &mut Self, + _proxy: &XdgDialogV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } } diff --git a/crates/gpui_linux/src/linux/wayland/clipboard.rs b/crates/gpui_linux/src/linux/wayland/clipboard.rs index 49c58724da..5c08eff809 100644 --- a/crates/gpui_linux/src/linux/wayland/clipboard.rs +++ b/crates/gpui_linux/src/linux/wayland/clipboard.rs @@ -1,7 +1,7 @@ use std::{ - fs::File, - io::{ErrorKind, Write}, - os::fd::{AsRawFd, BorrowedFd, OwnedFd}, + fs::File, + io::{ErrorKind, Write}, + os::fd::{AsRawFd, BorrowedFd, OwnedFd}, }; use calloop::{LoopHandle, PostAction}; @@ -15,246 +15,246 @@ use gpui::{ClipboardEntry, ClipboardItem, Image, ImageFormat, hash}; /// Text mime types that we'll offer to other programs. pub(crate) const TEXT_MIME_TYPES: [&str; 3] = - ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"]; + ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"]; pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; /// Text mime types that we'll accept from other programs. pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; pub(crate) struct Clipboard { - connection: Connection, - loop_handle: LoopHandle<'static, WaylandClientStatePtr>, - self_mime: String, + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + self_mime: String, - // Internal clipboard - contents: Option, - primary_contents: Option, + // Internal clipboard + contents: Option, + primary_contents: Option, - // External clipboard - cached_read: Option, - current_offer: Option>, - cached_primary_read: Option, - current_primary_offer: Option>, + // External clipboard + cached_read: Option, + current_offer: Option>, + cached_primary_read: Option, + current_primary_offer: Option>, } pub(crate) trait ReceiveData { - fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>); + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>); } impl ReceiveData for WlDataOffer { - fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { - self.receive(mime_type, fd); - } + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } } impl ReceiveData for ZwpPrimarySelectionOfferV1 { - fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { - self.receive(mime_type, fd); - } + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } } #[derive(Clone, Debug)] /// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types. pub(crate) struct DataOffer { - pub inner: T, - mime_types: Vec, + pub inner: T, + mime_types: Vec, } impl DataOffer { - pub fn new(offer: T) -> Self { - Self { - inner: offer, - mime_types: Vec::new(), - } - } + pub fn new(offer: T) -> Self { + Self { + inner: offer, + mime_types: Vec::new(), + } + } - pub fn add_mime_type(&mut self, mime_type: String) { - self.mime_types.push(mime_type) - } + pub fn add_mime_type(&mut self, mime_type: String) { + self.mime_types.push(mime_type) + } - fn has_mime_type(&self, mime_type: &str) -> bool { - self.mime_types.iter().any(|t| t == mime_type) - } + fn has_mime_type(&self, mime_type: &str) -> bool { + self.mime_types.iter().any(|t| t == mime_type) + } - fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option> { - let pipe = Pipe::new().unwrap(); - self.inner.receive_data(mime_type.to_string(), unsafe { - BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) - }); - let fd = pipe.read; - drop(pipe.write); + fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option> { + let pipe = Pipe::new().unwrap(); + self.inner.receive_data(mime_type.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); - connection.flush().unwrap(); + connection.flush().unwrap(); - match unsafe { read_fd(fd) } { - Ok(bytes) => Some(bytes), - Err(err) => { - log::error!("error reading clipboard pipe: {err:?}"); - None - } - } - } + match unsafe { read_fd(fd) } { + Ok(bytes) => Some(bytes), + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } - fn read_text(&self, connection: &Connection) -> Option { - let mime_type = self.mime_types.iter().find(|&mime_type| { - ALLOWED_TEXT_MIME_TYPES - .iter() - .any(|&allowed| allowed == mime_type) - })?; - let bytes = self.read_bytes(connection, mime_type)?; - let text_content = match String::from_utf8(bytes) { - Ok(content) => content, - Err(e) => { - log::error!("Failed to convert clipboard content to UTF-8: {}", e); - return None; - } - }; + fn read_text(&self, connection: &Connection) -> Option { + let mime_type = self.mime_types.iter().find(|&mime_type| { + ALLOWED_TEXT_MIME_TYPES + .iter() + .any(|&allowed| allowed == mime_type) + })?; + let bytes = self.read_bytes(connection, mime_type)?; + let text_content = match String::from_utf8(bytes) { + Ok(content) => content, + Err(e) => { + log::error!("Failed to convert clipboard content to UTF-8: {}", e); + return None; + } + }; - // Normalize the text to unix line endings, otherwise - // copying from eg: firefox inserts a lot of blank - // lines, and that is super annoying. - let result = text_content.replace("\r\n", "\n"); - Some(ClipboardItem::new_string(result)) - } + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = text_content.replace("\r\n", "\n"); + Some(ClipboardItem::new_string(result)) + } - fn read_image(&self, connection: &Connection) -> Option { - for format in ImageFormat::iter() { - let mime_type = format.mime_type(); - if !self.has_mime_type(mime_type) { - continue; - } + fn read_image(&self, connection: &Connection) -> Option { + for format in ImageFormat::iter() { + let mime_type = format.mime_type(); + if !self.has_mime_type(mime_type) { + continue; + } - if let Some(bytes) = self.read_bytes(connection, mime_type) { - let id = hash(&bytes); - return Some(ClipboardItem { - entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], - }); - } - } - None - } + if let Some(bytes) = self.read_bytes(connection, mime_type) { + let id = hash(&bytes); + return Some(ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + }); + } + } + None + } } impl Clipboard { - pub fn new( - connection: Connection, - loop_handle: LoopHandle<'static, WaylandClientStatePtr>, - ) -> Self { - Self { - connection, - loop_handle, - self_mime: format!("pid/{}", std::process::id()), + pub fn new( + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + ) -> Self { + Self { + connection, + loop_handle, + self_mime: format!("pid/{}", std::process::id()), - contents: None, - primary_contents: None, + contents: None, + primary_contents: None, - cached_read: None, - current_offer: None, - cached_primary_read: None, - current_primary_offer: None, - } - } + cached_read: None, + current_offer: None, + cached_primary_read: None, + current_primary_offer: None, + } + } - pub fn set(&mut self, item: ClipboardItem) { - self.contents = Some(item); - } + pub fn set(&mut self, item: ClipboardItem) { + self.contents = Some(item); + } - pub fn set_primary(&mut self, item: ClipboardItem) { - self.primary_contents = Some(item); - } + pub fn set_primary(&mut self, item: ClipboardItem) { + self.primary_contents = Some(item); + } - pub fn set_offer(&mut self, data_offer: Option>) { - self.cached_read = None; - self.current_offer = data_offer; - } + pub fn set_offer(&mut self, data_offer: Option>) { + self.cached_read = None; + self.current_offer = data_offer; + } - pub fn set_primary_offer(&mut self, data_offer: Option>) { - self.cached_primary_read = None; - self.current_primary_offer = data_offer; - } + pub fn set_primary_offer(&mut self, data_offer: Option>) { + self.cached_primary_read = None; + self.current_primary_offer = data_offer; + } - pub fn self_mime(&self) -> String { - self.self_mime.clone() - } + pub fn self_mime(&self) -> String { + self.self_mime.clone() + } - pub fn send(&self, _mime_type: String, fd: OwnedFd) { - if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) { - self.send_internal(fd, text.as_bytes().to_owned()); - } - } + pub fn send(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) { + self.send_internal(fd, text.as_bytes().to_owned()); + } + } - pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { - if let Some(text) = self - .primary_contents - .as_ref() - .and_then(|contents| contents.text()) - { - self.send_internal(fd, text.as_bytes().to_owned()); - } - } + pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self + .primary_contents + .as_ref() + .and_then(|contents| contents.text()) + { + self.send_internal(fd, text.as_bytes().to_owned()); + } + } - pub fn read(&mut self) -> Option { - let offer = self.current_offer.as_ref()?; - if let Some(cached) = self.cached_read.clone() { - return Some(cached); - } + pub fn read(&mut self) -> Option { + let offer = self.current_offer.as_ref()?; + if let Some(cached) = self.cached_read.clone() { + return Some(cached); + } - if offer.has_mime_type(&self.self_mime) { - return self.contents.clone(); - } + if offer.has_mime_type(&self.self_mime) { + return self.contents.clone(); + } - let item = offer - .read_text(&self.connection) - .or_else(|| offer.read_image(&self.connection))?; + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; - self.cached_read = Some(item.clone()); - Some(item) - } + self.cached_read = Some(item.clone()); + Some(item) + } - pub fn read_primary(&mut self) -> Option { - let offer = self.current_primary_offer.as_ref()?; - if let Some(cached) = self.cached_primary_read.clone() { - return Some(cached); - } + pub fn read_primary(&mut self) -> Option { + let offer = self.current_primary_offer.as_ref()?; + if let Some(cached) = self.cached_primary_read.clone() { + return Some(cached); + } - if offer.has_mime_type(&self.self_mime) { - return self.primary_contents.clone(); - } + if offer.has_mime_type(&self.self_mime) { + return self.primary_contents.clone(); + } - let item = offer - .read_text(&self.connection) - .or_else(|| offer.read_image(&self.connection))?; + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; - self.cached_primary_read = Some(item.clone()); - Some(item) - } + self.cached_primary_read = Some(item.clone()); + Some(item) + } - fn send_internal(&self, fd: OwnedFd, bytes: Vec) { - let mut written = 0; - self.loop_handle - .insert_source( - calloop::generic::Generic::new( - File::from(fd), - calloop::Interest::WRITE, - calloop::Mode::Level, - ), - move |_, file, _| { - let file = unsafe { file.get_mut() }; - loop { - match file.write(&bytes[written..]) { - Ok(n) if written + n == bytes.len() => { - written += n; - break Ok(PostAction::Remove); - } - Ok(n) => written += n, - Err(err) if err.kind() == ErrorKind::WouldBlock => { - break Ok(PostAction::Continue); - } - Err(_) => break Ok(PostAction::Remove), - } - } - }, - ) - .unwrap(); - } + fn send_internal(&self, fd: OwnedFd, bytes: Vec) { + let mut written = 0; + self.loop_handle + .insert_source( + calloop::generic::Generic::new( + File::from(fd), + calloop::Interest::WRITE, + calloop::Mode::Level, + ), + move |_, file, _| { + let file = unsafe { file.get_mut() }; + loop { + match file.write(&bytes[written..]) { + Ok(n) if written + n == bytes.len() => { + written += n; + break Ok(PostAction::Remove); + } + Ok(n) => written += n, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break Ok(PostAction::Continue); + } + Err(_) => break Ok(PostAction::Remove), + } + } + }, + ) + .unwrap(); + } } diff --git a/crates/gpui_linux/src/linux/wayland/cursor.rs b/crates/gpui_linux/src/linux/wayland/cursor.rs index 957fcf39be..474864870c 100644 --- a/crates/gpui_linux/src/linux/wayland/cursor.rs +++ b/crates/gpui_linux/src/linux/wayland/cursor.rs @@ -9,144 +9,144 @@ use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm}; use wayland_cursor::{CursorImageBuffer, CursorTheme}; pub(crate) struct Cursor { - loaded_theme: Option, - size: u32, - scaled_size: u32, - surface: WlSurface, - shm: WlShm, - connection: Connection, + loaded_theme: Option, + size: u32, + scaled_size: u32, + surface: WlSurface, + shm: WlShm, + connection: Connection, } pub(crate) struct LoadedTheme { - theme: CursorTheme, - name: Option, - scaled_size: u32, + theme: CursorTheme, + name: Option, + scaled_size: u32, } impl Drop for Cursor { - fn drop(&mut self) { - self.loaded_theme.take(); - self.surface.destroy(); - } + fn drop(&mut self) { + self.loaded_theme.take(); + self.surface.destroy(); + } } impl Cursor { - pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { - let mut this = Self { - loaded_theme: None, - size, - scaled_size: size, - surface: globals.compositor.create_surface(&globals.qh, ()), - shm: globals.shm.clone(), - connection: connection.clone(), - }; - this.set_theme_internal(None); - this - } + pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { + let mut this = Self { + loaded_theme: None, + size, + scaled_size: size, + surface: globals.compositor.create_surface(&globals.qh, ()), + shm: globals.shm.clone(), + connection: connection.clone(), + }; + this.set_theme_internal(None); + this + } - fn set_theme_internal(&mut self, theme_name: Option) { - if let Some(loaded_theme) = self.loaded_theme.as_ref() - && loaded_theme.name == theme_name - && loaded_theme.scaled_size == self.scaled_size - { - return; - } - let result = if let Some(theme_name) = theme_name.as_ref() { - CursorTheme::load_from_name( - &self.connection, - self.shm.clone(), - theme_name, - self.scaled_size, - ) - } else { - CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size) - }; - if let Some(theme) = result - .context("Wayland: Failed to load cursor theme") - .log_err() - { - self.loaded_theme = Some(LoadedTheme { - theme, - name: theme_name, - scaled_size: self.scaled_size, - }); - } - } + fn set_theme_internal(&mut self, theme_name: Option) { + if let Some(loaded_theme) = self.loaded_theme.as_ref() + && loaded_theme.name == theme_name + && loaded_theme.scaled_size == self.scaled_size + { + return; + } + let result = if let Some(theme_name) = theme_name.as_ref() { + CursorTheme::load_from_name( + &self.connection, + self.shm.clone(), + theme_name, + self.scaled_size, + ) + } else { + CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size) + }; + if let Some(theme) = result + .context("Wayland: Failed to load cursor theme") + .log_err() + { + self.loaded_theme = Some(LoadedTheme { + theme, + name: theme_name, + scaled_size: self.scaled_size, + }); + } + } - pub fn set_theme(&mut self, theme_name: String) { - self.set_theme_internal(Some(theme_name)); - } + pub fn set_theme(&mut self, theme_name: String) { + self.set_theme_internal(Some(theme_name)); + } - fn set_scaled_size(&mut self, scaled_size: u32) { - self.scaled_size = scaled_size; - let theme_name = self - .loaded_theme - .as_ref() - .and_then(|loaded_theme| loaded_theme.name.clone()); - self.set_theme_internal(theme_name); - } + fn set_scaled_size(&mut self, scaled_size: u32) { + self.scaled_size = scaled_size; + let theme_name = self + .loaded_theme + .as_ref() + .and_then(|loaded_theme| loaded_theme.name.clone()); + self.set_theme_internal(theme_name); + } - pub fn set_size(&mut self, size: u32) { - self.size = size; - self.set_scaled_size(size); - } + pub fn set_size(&mut self, size: u32) { + self.size = size; + self.set_scaled_size(size); + } - pub fn set_icon( - &mut self, - wl_pointer: &WlPointer, - serial_id: u32, - cursor_icon_names: &[&str], - scale: i32, - ) { - self.set_scaled_size(self.size * scale as u32); + pub fn set_icon( + &mut self, + wl_pointer: &WlPointer, + serial_id: u32, + cursor_icon_names: &[&str], + scale: i32, + ) { + self.set_scaled_size(self.size * scale as u32); - let Some(loaded_theme) = &mut self.loaded_theme else { - log::warn!("Wayland: Unable to load cursor themes"); - return; - }; - let theme = &mut loaded_theme.theme; + let Some(loaded_theme) = &mut self.loaded_theme else { + log::warn!("Wayland: Unable to load cursor themes"); + return; + }; + let theme = &mut loaded_theme.theme; - let buffer: &CursorImageBuffer; - 'outer: { - for cursor_icon_name in cursor_icon_names { - if let Some(cursor) = theme.get_cursor(cursor_icon_name) { - buffer = &cursor[0]; - break 'outer; - } - } + let buffer: &CursorImageBuffer; + 'outer: { + for cursor_icon_name in cursor_icon_names { + if let Some(cursor) = theme.get_cursor(cursor_icon_name) { + buffer = &cursor[0]; + break 'outer; + } + } - if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) { - buffer = &cursor[0]; - log_cursor_icon_warning(anyhow!( - "wayland: Unable to get cursor icon {:?}. \ + if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) { + buffer = &cursor[0]; + log_cursor_icon_warning(anyhow!( + "wayland: Unable to get cursor icon {:?}. \ Using default cursor icon: '{}'", - cursor_icon_names, - DEFAULT_CURSOR_ICON_NAME - )); - } else { - log_cursor_icon_warning(anyhow!( - "wayland: Unable to fallback on default cursor icon '{}' for theme '{}'", - DEFAULT_CURSOR_ICON_NAME, - loaded_theme.name.as_deref().unwrap_or("default") - )); - return; - } - } + cursor_icon_names, + DEFAULT_CURSOR_ICON_NAME + )); + } else { + log_cursor_icon_warning(anyhow!( + "wayland: Unable to fallback on default cursor icon '{}' for theme '{}'", + DEFAULT_CURSOR_ICON_NAME, + loaded_theme.name.as_deref().unwrap_or("default") + )); + return; + } + } - let (width, height) = buffer.dimensions(); - let (hot_x, hot_y) = buffer.hotspot(); + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); - self.surface.set_buffer_scale(scale); + self.surface.set_buffer_scale(scale); - wl_pointer.set_cursor( - serial_id, - Some(&self.surface), - hot_x as i32 / scale, - hot_y as i32 / scale, - ); + wl_pointer.set_cursor( + serial_id, + Some(&self.surface), + hot_x as i32 / scale, + hot_y as i32 / scale, + ); - self.surface.attach(Some(buffer), 0, 0); - self.surface.damage(0, 0, width as i32, height as i32); - self.surface.commit(); - } + self.surface.attach(Some(buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); + } } diff --git a/crates/gpui_linux/src/linux/wayland/display.rs b/crates/gpui_linux/src/linux/wayland/display.rs index 8fa9122d62..f03ac72cb0 100644 --- a/crates/gpui_linux/src/linux/wayland/display.rs +++ b/crates/gpui_linux/src/linux/wayland/display.rs @@ -1,6 +1,6 @@ use std::{ - fmt::Debug, - hash::{Hash, Hasher}, + fmt::Debug, + hash::{Hash, Hasher}, }; use anyhow::Context as _; @@ -11,32 +11,32 @@ use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay}; #[derive(Debug, Clone)] pub(crate) struct WaylandDisplay { - /// The ID of the wl_output object - pub id: ObjectId, - pub name: Option, - pub bounds: Bounds, + /// The ID of the wl_output object + pub id: ObjectId, + pub name: Option, + pub bounds: Bounds, } impl Hash for WaylandDisplay { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } + fn hash(&self, state: &mut H) { + self.id.hash(state); + } } impl PlatformDisplay for WaylandDisplay { - fn id(&self) -> DisplayId { - DisplayId::new(self.id.protocol_id() as u64) - } + fn id(&self) -> DisplayId { + DisplayId::new(self.id.protocol_id() as u64) + } - fn uuid(&self) -> anyhow::Result { - let name = self - .name - .as_ref() - .context("Wayland display does not have a name")?; - Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes())) - } + fn uuid(&self) -> anyhow::Result { + let name = self + .name + .as_ref() + .context("Wayland display does not have a name")?; + Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes())) + } - fn bounds(&self) -> Bounds { - self.bounds - } + fn bounds(&self) -> Bounds { + self.bounds + } } diff --git a/crates/gpui_linux/src/linux/wayland/layer_shell.rs b/crates/gpui_linux/src/linux/wayland/layer_shell.rs index a400552065..35fc259bd5 100644 --- a/crates/gpui_linux/src/linux/wayland/layer_shell.rs +++ b/crates/gpui_linux/src/linux/wayland/layer_shell.rs @@ -3,24 +3,24 @@ pub use gpui::layer_shell::*; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; pub(crate) fn wayland_layer(layer: Layer) -> zwlr_layer_shell_v1::Layer { - match layer { - Layer::Background => zwlr_layer_shell_v1::Layer::Background, - Layer::Bottom => zwlr_layer_shell_v1::Layer::Bottom, - Layer::Top => zwlr_layer_shell_v1::Layer::Top, - Layer::Overlay => zwlr_layer_shell_v1::Layer::Overlay, - } + match layer { + Layer::Background => zwlr_layer_shell_v1::Layer::Background, + Layer::Bottom => zwlr_layer_shell_v1::Layer::Bottom, + Layer::Top => zwlr_layer_shell_v1::Layer::Top, + Layer::Overlay => zwlr_layer_shell_v1::Layer::Overlay, + } } pub(crate) fn wayland_anchor(anchor: Anchor) -> zwlr_layer_surface_v1::Anchor { - zwlr_layer_surface_v1::Anchor::from_bits_truncate(anchor.bits()) + zwlr_layer_surface_v1::Anchor::from_bits_truncate(anchor.bits()) } pub(crate) fn wayland_keyboard_interactivity( - value: KeyboardInteractivity, + value: KeyboardInteractivity, ) -> zwlr_layer_surface_v1::KeyboardInteractivity { - match value { - KeyboardInteractivity::None => zwlr_layer_surface_v1::KeyboardInteractivity::None, - KeyboardInteractivity::Exclusive => zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive, - KeyboardInteractivity::OnDemand => zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand, - } + match value { + KeyboardInteractivity::None => zwlr_layer_surface_v1::KeyboardInteractivity::None, + KeyboardInteractivity::Exclusive => zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive, + KeyboardInteractivity::OnDemand => zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand, + } } diff --git a/crates/gpui_linux/src/linux/wayland/scroll.rs b/crates/gpui_linux/src/linux/wayland/scroll.rs index 5a57f80196..d386b9cb63 100644 --- a/crates/gpui_linux/src/linux/wayland/scroll.rs +++ b/crates/gpui_linux/src/linux/wayland/scroll.rs @@ -1,11 +1,11 @@ use std::{ - collections::VecDeque, - time::{Duration, Instant}, + collections::VecDeque, + time::{Duration, Instant}, }; use super::window::WaylandWindowStatePtr; use gpui::{ - Modifiers, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchPhase, point, px, + Modifiers, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchPhase, point, px, }; const KINETIC_SCROLL_HISTORY_WINDOW: Duration = Duration::from_millis(150); @@ -14,415 +14,415 @@ const KINETIC_SCROLL_STOP_VELOCITY: f32 = 5.0; const KINETIC_SCROLL_MAX_VELOCITY: f32 = 6000.0; pub(crate) struct KineticScrollController { - history: KineticScrollHistory, - id: u64, - scroll: Option, - finger_active: bool, - finger_start_pending: bool, - finger_stop_pending: bool, + history: KineticScrollHistory, + id: u64, + scroll: Option, + finger_active: bool, + finger_start_pending: bool, + finger_stop_pending: bool, } struct KineticScrollHistory { - entries: VecDeque<(Instant, Point)>, - displacement: Point, + entries: VecDeque<(Instant, Point)>, + displacement: Point, } struct KineticScroll { - id: u64, - window: WaylandWindowStatePtr, - position: Point, - modifiers: Modifiers, - velocity: Point, - last_time: Instant, + id: u64, + window: WaylandWindowStatePtr, + position: Point, + modifiers: Modifiers, + velocity: Point, + last_time: Instant, } impl KineticScrollController { - pub(crate) fn new() -> Self { - Self { - history: KineticScrollHistory::new(), - id: 0, - scroll: None, - finger_active: false, - finger_start_pending: false, - finger_stop_pending: false, - } - } + pub(crate) fn new() -> Self { + Self { + history: KineticScrollHistory::new(), + id: 0, + scroll: None, + finger_active: false, + finger_start_pending: false, + finger_stop_pending: false, + } + } - pub(crate) fn start_finger_scroll(&mut self) { - self.id += 1; - self.scroll = None; - self.finger_stop_pending = false; - if !self.finger_active { - self.finger_active = true; - self.finger_start_pending = true; - self.history.clear(); - } - } + pub(crate) fn start_finger_scroll(&mut self) { + self.id += 1; + self.scroll = None; + self.finger_stop_pending = false; + if !self.finger_active { + self.finger_active = true; + self.finger_start_pending = true; + self.history.clear(); + } + } - pub(crate) fn stop_finger_scroll(&mut self) -> bool { - if self.finger_active { - self.finger_stop_pending = true; - true - } else { - false - } - } + pub(crate) fn stop_finger_scroll(&mut self) -> bool { + if self.finger_active { + self.finger_stop_pending = true; + true + } else { + false + } + } - pub(crate) fn touch_phase(&mut self) -> TouchPhase { - if self.finger_start_pending { - self.finger_start_pending = false; - TouchPhase::Started - } else { - TouchPhase::Moved - } - } + pub(crate) fn touch_phase(&mut self) -> TouchPhase { + if self.finger_start_pending { + self.finger_start_pending = false; + TouchPhase::Started + } else { + TouchPhase::Moved + } + } - pub(crate) fn record_delta(&mut self, time: Instant, delta: Point) { - self.history.push(time, delta); - } + pub(crate) fn record_delta(&mut self, time: Instant, delta: Point) { + self.history.push(time, delta); + } - pub(crate) fn has_pending_stop(&self) -> bool { - self.finger_stop_pending - } + pub(crate) fn has_pending_stop(&self) -> bool { + self.finger_stop_pending + } - pub(crate) fn finish_pending_stop( - &mut self, - window: WaylandWindowStatePtr, - position: Point, - modifiers: Modifiers, - ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { - self.finger_stop_pending = false; - self.finger_active = false; - self.finger_start_pending = false; - let velocity = self.history.velocity(Instant::now()); - self.history.clear(); - self.start(window, position, modifiers, velocity) - } + pub(crate) fn finish_pending_stop( + &mut self, + window: WaylandWindowStatePtr, + position: Point, + modifiers: Modifiers, + ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { + self.finger_stop_pending = false; + self.finger_active = false; + self.finger_start_pending = false; + let velocity = self.history.velocity(Instant::now()); + self.history.clear(); + self.start(window, position, modifiers, velocity) + } - pub(crate) fn tick( - &mut self, - window: &WaylandWindowStatePtr, - ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { - let kinetic_scroll = self.scroll.as_mut()?; - if kinetic_scroll.id != self.id || !kinetic_scroll.window.ptr_eq(window) { - return None; - } + pub(crate) fn tick( + &mut self, + window: &WaylandWindowStatePtr, + ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { + let kinetic_scroll = self.scroll.as_mut()?; + if kinetic_scroll.id != self.id || !kinetic_scroll.window.ptr_eq(window) { + return None; + } - let now = Instant::now(); - let elapsed = now - .duration_since(kinetic_scroll.last_time) - .as_secs_f32() - .min(0.05); - kinetic_scroll.last_time = now; + let now = Instant::now(); + let elapsed = now + .duration_since(kinetic_scroll.last_time) + .as_secs_f32() + .min(0.05); + kinetic_scroll.last_time = now; - let delta = point( - px(f32::from(kinetic_scroll.velocity.x) * elapsed), - px(f32::from(kinetic_scroll.velocity.y) * elapsed), - ); - let velocity_multiplier = (-KINETIC_SCROLL_FRICTION * elapsed).exp(); - kinetic_scroll.velocity.x = px(f32::from(kinetic_scroll.velocity.x) * velocity_multiplier); - kinetic_scroll.velocity.y = px(f32::from(kinetic_scroll.velocity.y) * velocity_multiplier); + let delta = point( + px(f32::from(kinetic_scroll.velocity.x) * elapsed), + px(f32::from(kinetic_scroll.velocity.y) * elapsed), + ); + let velocity_multiplier = (-KINETIC_SCROLL_FRICTION * elapsed).exp(); + kinetic_scroll.velocity.x = px(f32::from(kinetic_scroll.velocity.x) * velocity_multiplier); + kinetic_scroll.velocity.y = px(f32::from(kinetic_scroll.velocity.y) * velocity_multiplier); - let finished = is_kinetic_scroll_stopped(kinetic_scroll.velocity); + let finished = is_kinetic_scroll_stopped(kinetic_scroll.velocity); - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: kinetic_scroll.position, - delta: ScrollDelta::Pixels(delta), - modifiers: kinetic_scroll.modifiers, - touch_phase: if finished { - TouchPhase::Ended - } else { - TouchPhase::Moved - }, - }); + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: kinetic_scroll.position, + delta: ScrollDelta::Pixels(delta), + modifiers: kinetic_scroll.modifiers, + touch_phase: if finished { + TouchPhase::Ended + } else { + TouchPhase::Moved + }, + }); - let window = kinetic_scroll.window.clone(); - if finished { - self.finger_active = false; - self.finger_start_pending = false; - self.finger_stop_pending = false; - self.scroll = None; - self.history.clear(); - } + let window = kinetic_scroll.window.clone(); + if finished { + self.finger_active = false; + self.finger_start_pending = false; + self.finger_stop_pending = false; + self.scroll = None; + self.history.clear(); + } - Some((window, input)) - } + Some((window, input)) + } - pub(crate) fn cancel(&mut self) -> Option<(WaylandWindowStatePtr, PlatformInput)> { - let kinetic_scroll = self.scroll.take()?; + pub(crate) fn cancel(&mut self) -> Option<(WaylandWindowStatePtr, PlatformInput)> { + let kinetic_scroll = self.scroll.take()?; - self.id += 1; - self.finger_active = false; - self.finger_start_pending = false; - self.finger_stop_pending = false; - self.history.clear(); + self.id += 1; + self.finger_active = false; + self.finger_start_pending = false; + self.finger_stop_pending = false; + self.history.clear(); - Some(( - kinetic_scroll.window, - PlatformInput::ScrollWheel(ScrollWheelEvent { - position: kinetic_scroll.position, - delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), - modifiers: kinetic_scroll.modifiers, - touch_phase: TouchPhase::Ended, - }), - )) - } + Some(( + kinetic_scroll.window, + PlatformInput::ScrollWheel(ScrollWheelEvent { + position: kinetic_scroll.position, + delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), + modifiers: kinetic_scroll.modifiers, + touch_phase: TouchPhase::Ended, + }), + )) + } - fn start( - &mut self, - window: WaylandWindowStatePtr, - position: Point, - modifiers: Modifiers, - velocity: Point, - ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { - self.id += 1; - if is_kinetic_scroll_stopped(velocity) { - return Some(( - window, - PlatformInput::ScrollWheel(ScrollWheelEvent { - position, - delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), - modifiers, - touch_phase: TouchPhase::Ended, - }), - )); - } + fn start( + &mut self, + window: WaylandWindowStatePtr, + position: Point, + modifiers: Modifiers, + velocity: Point, + ) -> Option<(WaylandWindowStatePtr, PlatformInput)> { + self.id += 1; + if is_kinetic_scroll_stopped(velocity) { + return Some(( + window, + PlatformInput::ScrollWheel(ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), + modifiers, + touch_phase: TouchPhase::Ended, + }), + )); + } - let id = self.id; - self.scroll = Some(KineticScroll { - id, - window, - position, - modifiers, - velocity, - last_time: Instant::now(), - }); - None - } + let id = self.id; + self.scroll = Some(KineticScroll { + id, + window, + position, + modifiers, + velocity, + last_time: Instant::now(), + }); + None + } } impl KineticScrollHistory { - fn new() -> Self { - Self { - entries: VecDeque::new(), - displacement: point(px(0.0), px(0.0)), - } - } + fn new() -> Self { + Self { + entries: VecDeque::new(), + displacement: point(px(0.0), px(0.0)), + } + } - fn push(&mut self, time: Instant, delta: Point) { - self.entries.push_back((time, delta)); - self.displacement += delta; - let cutoff = time - KINETIC_SCROLL_HISTORY_WINDOW; - while self - .entries - .front() - .is_some_and(|(entry_time, _)| *entry_time < cutoff) - { - let delta = self - .entries - .pop_front() - .map(|(_, delta)| delta) - .unwrap_or(point(px(0.0), px(0.0))); - self.displacement -= delta; - } - } + fn push(&mut self, time: Instant, delta: Point) { + self.entries.push_back((time, delta)); + self.displacement += delta; + let cutoff = time - KINETIC_SCROLL_HISTORY_WINDOW; + while self + .entries + .front() + .is_some_and(|(entry_time, _)| *entry_time < cutoff) + { + let delta = self + .entries + .pop_front() + .map(|(_, delta)| delta) + .unwrap_or(point(px(0.0), px(0.0))); + self.displacement -= delta; + } + } - fn velocity(&self, now: Instant) -> Point { - let Some((first_time, _)) = self.entries.front() else { - return point(px(0.0), px(0.0)); - }; + fn velocity(&self, now: Instant) -> Point { + let Some((first_time, _)) = self.entries.front() else { + return point(px(0.0), px(0.0)); + }; - let duration = now.duration_since(*first_time).as_secs_f32(); - if duration == 0.0 { - return point(px(0.0), px(0.0)); - } + let duration = now.duration_since(*first_time).as_secs_f32(); + if duration == 0.0 { + return point(px(0.0), px(0.0)); + } - let delta = self.displacement; + let delta = self.displacement; - point( - px((f32::from(delta.x) / duration) - .clamp(-KINETIC_SCROLL_MAX_VELOCITY, KINETIC_SCROLL_MAX_VELOCITY)), - px((f32::from(delta.y) / duration) - .clamp(-KINETIC_SCROLL_MAX_VELOCITY, KINETIC_SCROLL_MAX_VELOCITY)), - ) - } + point( + px((f32::from(delta.x) / duration) + .clamp(-KINETIC_SCROLL_MAX_VELOCITY, KINETIC_SCROLL_MAX_VELOCITY)), + px((f32::from(delta.y) / duration) + .clamp(-KINETIC_SCROLL_MAX_VELOCITY, KINETIC_SCROLL_MAX_VELOCITY)), + ) + } - fn clear(&mut self) { - self.entries.clear(); - self.displacement = point(px(0.0), px(0.0)); - } + fn clear(&mut self) { + self.entries.clear(); + self.displacement = point(px(0.0), px(0.0)); + } } fn is_kinetic_scroll_stopped(velocity: Point) -> bool { - f32::from(velocity.x).abs() < KINETIC_SCROLL_STOP_VELOCITY - && f32::from(velocity.y).abs() < KINETIC_SCROLL_STOP_VELOCITY + f32::from(velocity.x).abs() < KINETIC_SCROLL_STOP_VELOCITY + && f32::from(velocity.y).abs() < KINETIC_SCROLL_STOP_VELOCITY } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_is_kinetic_scroll_stopped() { - assert!(is_kinetic_scroll_stopped(point(px(0.0), px(0.0)))); - assert!(is_kinetic_scroll_stopped(point(px(4.9), px(4.9)))); - assert!(is_kinetic_scroll_stopped(point(px(-4.9), px(4.9)))); - assert!(!is_kinetic_scroll_stopped(point(px(5.0), px(0.0)))); - assert!(!is_kinetic_scroll_stopped(point(px(0.0), px(5.0)))); - assert!(!is_kinetic_scroll_stopped(point(px(100.0), px(100.0)))); - } + #[test] + fn test_is_kinetic_scroll_stopped() { + assert!(is_kinetic_scroll_stopped(point(px(0.0), px(0.0)))); + assert!(is_kinetic_scroll_stopped(point(px(4.9), px(4.9)))); + assert!(is_kinetic_scroll_stopped(point(px(-4.9), px(4.9)))); + assert!(!is_kinetic_scroll_stopped(point(px(5.0), px(0.0)))); + assert!(!is_kinetic_scroll_stopped(point(px(0.0), px(5.0)))); + assert!(!is_kinetic_scroll_stopped(point(px(100.0), px(100.0)))); + } - #[test] - fn test_history_velocity_empty() { - let history = KineticScrollHistory::new(); - let velocity = history.velocity(Instant::now()); - assert_eq!(f32::from(velocity.x), 0.0); - assert_eq!(f32::from(velocity.y), 0.0); - } + #[test] + fn test_history_velocity_empty() { + let history = KineticScrollHistory::new(); + let velocity = history.velocity(Instant::now()); + assert_eq!(f32::from(velocity.x), 0.0); + assert_eq!(f32::from(velocity.y), 0.0); + } - #[test] - fn test_history_velocity_single_entry() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push( - now - Duration::from_millis(100), - point(px(100.0), px(200.0)), - ); - let velocity = history.velocity(now); - assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); - assert!((f32::from(velocity.y) - 2000.0).abs() < 1.0); - } + #[test] + fn test_history_velocity_single_entry() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push( + now - Duration::from_millis(100), + point(px(100.0), px(200.0)), + ); + let velocity = history.velocity(now); + assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); + assert!((f32::from(velocity.y) - 2000.0).abs() < 1.0); + } - #[test] - fn test_history_velocity_multiple_entries() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push(now - Duration::from_millis(100), point(px(50.0), px(0.0))); - history.push(now - Duration::from_millis(50), point(px(50.0), px(100.0))); - let velocity = history.velocity(now); - assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); - assert!((f32::from(velocity.y) - 1000.0).abs() < 1.0); - } + #[test] + fn test_history_velocity_multiple_entries() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push(now - Duration::from_millis(100), point(px(50.0), px(0.0))); + history.push(now - Duration::from_millis(50), point(px(50.0), px(100.0))); + let velocity = history.velocity(now); + assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); + assert!((f32::from(velocity.y) - 1000.0).abs() < 1.0); + } - #[test] - fn test_history_velocity_zero_duration() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push(now, point(px(100.0), px(200.0))); - let velocity = history.velocity(now); - assert_eq!(f32::from(velocity.x), 0.0); - assert_eq!(f32::from(velocity.y), 0.0); - } + #[test] + fn test_history_velocity_zero_duration() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push(now, point(px(100.0), px(200.0))); + let velocity = history.velocity(now); + assert_eq!(f32::from(velocity.x), 0.0); + assert_eq!(f32::from(velocity.y), 0.0); + } - #[test] - fn test_history_velocity_clamped() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push( - now - Duration::from_millis(1), - point(px(60000.0), px(-60000.0)), - ); - let velocity = history.velocity(now); - assert_eq!(f32::from(velocity.x), KINETIC_SCROLL_MAX_VELOCITY); - assert_eq!(f32::from(velocity.y), -KINETIC_SCROLL_MAX_VELOCITY); - } + #[test] + fn test_history_velocity_clamped() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push( + now - Duration::from_millis(1), + point(px(60000.0), px(-60000.0)), + ); + let velocity = history.velocity(now); + assert_eq!(f32::from(velocity.x), KINETIC_SCROLL_MAX_VELOCITY); + assert_eq!(f32::from(velocity.y), -KINETIC_SCROLL_MAX_VELOCITY); + } - #[test] - fn test_history_prunes_old_entries() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push( - now - Duration::from_millis(300), - point(px(9999.0), px(9999.0)), - ); - history.push( - now - Duration::from_millis(100), - point(px(100.0), px(100.0)), - ); - let velocity = history.velocity(now); - assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); - assert!((f32::from(velocity.y) - 1000.0).abs() < 1.0); - assert_eq!(history.entries.len(), 1); - } + #[test] + fn test_history_prunes_old_entries() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push( + now - Duration::from_millis(300), + point(px(9999.0), px(9999.0)), + ); + history.push( + now - Duration::from_millis(100), + point(px(100.0), px(100.0)), + ); + let velocity = history.velocity(now); + assert!((f32::from(velocity.x) - 1000.0).abs() < 1.0); + assert!((f32::from(velocity.y) - 1000.0).abs() < 1.0); + assert_eq!(history.entries.len(), 1); + } - #[test] - fn test_history_clear() { - let mut history = KineticScrollHistory::new(); - let now = Instant::now(); - history.push(now, point(px(100.0), px(100.0))); - assert_eq!(history.entries.len(), 1); - history.clear(); - assert_eq!(history.entries.len(), 0); - } + #[test] + fn test_history_clear() { + let mut history = KineticScrollHistory::new(); + let now = Instant::now(); + history.push(now, point(px(100.0), px(100.0))); + assert_eq!(history.entries.len(), 1); + history.clear(); + assert_eq!(history.entries.len(), 0); + } - #[test] - fn test_touch_phase_sequence() { - let mut scroller = KineticScrollController::new(); - assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); + #[test] + fn test_touch_phase_sequence() { + let mut scroller = KineticScrollController::new(); + assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); - scroller.start_finger_scroll(); - assert!(matches!(scroller.touch_phase(), TouchPhase::Started)); - assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); - assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); - } + scroller.start_finger_scroll(); + assert!(matches!(scroller.touch_phase(), TouchPhase::Started)); + assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); + assert!(matches!(scroller.touch_phase(), TouchPhase::Moved)); + } - #[test] - fn test_start_finger_scroll_id_increments() { - let mut scroller = KineticScrollController::new(); - let id_before = scroller.id; - scroller.start_finger_scroll(); - assert_eq!(scroller.id, id_before + 1); - } + #[test] + fn test_start_finger_scroll_id_increments() { + let mut scroller = KineticScrollController::new(); + let id_before = scroller.id; + scroller.start_finger_scroll(); + assert_eq!(scroller.id, id_before + 1); + } - #[test] - fn test_start_finger_scroll_resets_stop_pending() { - let mut scroller = KineticScrollController::new(); - scroller.start_finger_scroll(); - assert!(scroller.stop_finger_scroll()); - assert!(scroller.has_pending_stop()); - scroller.start_finger_scroll(); - assert!(!scroller.has_pending_stop()); - } + #[test] + fn test_start_finger_scroll_resets_stop_pending() { + let mut scroller = KineticScrollController::new(); + scroller.start_finger_scroll(); + assert!(scroller.stop_finger_scroll()); + assert!(scroller.has_pending_stop()); + scroller.start_finger_scroll(); + assert!(!scroller.has_pending_stop()); + } - #[test] - fn test_start_finger_scroll_does_not_reset_active() { - let mut scroller = KineticScrollController::new(); - scroller.start_finger_scroll(); - assert!(scroller.finger_active); - assert!(scroller.finger_start_pending); + #[test] + fn test_start_finger_scroll_does_not_reset_active() { + let mut scroller = KineticScrollController::new(); + scroller.start_finger_scroll(); + assert!(scroller.finger_active); + assert!(scroller.finger_start_pending); - scroller.touch_phase(); - assert!(!scroller.finger_start_pending); + scroller.touch_phase(); + assert!(!scroller.finger_start_pending); - scroller.start_finger_scroll(); - assert!(scroller.finger_active); - assert!(!scroller.finger_start_pending); - } + scroller.start_finger_scroll(); + assert!(scroller.finger_active); + assert!(!scroller.finger_start_pending); + } - #[test] - fn test_stop_finger_scroll_when_inactive() { - let mut scroller = KineticScrollController::new(); - assert!(!scroller.stop_finger_scroll()); - assert!(!scroller.has_pending_stop()); - } + #[test] + fn test_stop_finger_scroll_when_inactive() { + let mut scroller = KineticScrollController::new(); + assert!(!scroller.stop_finger_scroll()); + assert!(!scroller.has_pending_stop()); + } - #[test] - fn test_stop_finger_scroll_when_active() { - let mut scroller = KineticScrollController::new(); - scroller.start_finger_scroll(); - assert!(scroller.stop_finger_scroll()); - assert!(scroller.has_pending_stop()); - } + #[test] + fn test_stop_finger_scroll_when_active() { + let mut scroller = KineticScrollController::new(); + scroller.start_finger_scroll(); + assert!(scroller.stop_finger_scroll()); + assert!(scroller.has_pending_stop()); + } - #[test] - fn test_record_delta_and_has_pending_stop() { - let mut scroller = KineticScrollController::new(); - scroller.start_finger_scroll(); - scroller.record_delta(Instant::now(), point(px(10.0), px(20.0))); - assert_eq!(scroller.history.entries.len(), 1); - assert!(!scroller.has_pending_stop()); + #[test] + fn test_record_delta_and_has_pending_stop() { + let mut scroller = KineticScrollController::new(); + scroller.start_finger_scroll(); + scroller.record_delta(Instant::now(), point(px(10.0), px(20.0))); + assert_eq!(scroller.history.entries.len(), 1); + assert!(!scroller.has_pending_stop()); - scroller.stop_finger_scroll(); - assert!(scroller.has_pending_stop()); - } + scroller.stop_finger_scroll(); + assert!(scroller.has_pending_stop()); + } } diff --git a/crates/gpui_linux/src/linux/wayland/serial.rs b/crates/gpui_linux/src/linux/wayland/serial.rs index ffb405727b..51618e652a 100644 --- a/crates/gpui_linux/src/linux/wayland/serial.rs +++ b/crates/gpui_linux/src/linux/wayland/serial.rs @@ -2,66 +2,66 @@ use collections::HashMap; #[derive(Debug, Hash, PartialEq, Eq)] pub(crate) enum SerialKind { - DataDevice, - InputMethod, - MouseEnter, - MousePress, - KeyPress, + DataDevice, + InputMethod, + MouseEnter, + MousePress, + KeyPress, } #[derive(Debug)] struct SerialData { - serial: u32, + serial: u32, } impl SerialData { - fn new(value: u32) -> Self { - Self { serial: value } - } + fn new(value: u32) -> Self { + Self { serial: value } + } } #[derive(Debug)] /// Helper for tracking of different serial kinds. pub(crate) struct SerialTracker { - serials: HashMap, + serials: HashMap, } impl SerialTracker { - pub fn new() -> Self { - Self { - serials: HashMap::default(), - } - } + pub fn new() -> Self { + Self { + serials: HashMap::default(), + } + } - pub fn update(&mut self, kind: SerialKind, value: u32) { - self.serials.insert(kind, SerialData::new(value)); - } + pub fn update(&mut self, kind: SerialKind, value: u32) { + self.serials.insert(kind, SerialData::new(value)); + } - /// Returns the latest tracked serial of the provided [`SerialKind`] - /// - /// Will return 0 if not tracked. - pub fn get(&self, kind: SerialKind) -> u32 { - self.serials - .get(&kind) - .map(|serial_data| serial_data.serial) - .unwrap_or(0) - } + /// Returns the latest tracked serial of the provided [`SerialKind`] + /// + /// Will return 0 if not tracked. + pub fn get(&self, kind: SerialKind) -> u32 { + self.serials + .get(&kind) + .map(|serial_data| serial_data.serial) + .unwrap_or(0) + } - /// Returns the most recent serial across all tracked kinds. - /// - /// Wayland compositor serial numbers are monotonically increasing, so the - /// highest value is always the most recently received one. This is the - /// correct serial to use for [`set_selection`] when the triggering event - /// may have been a mouse press rather than a key press: using 0 (the - /// default when a kind has never been seen) causes compositors to silently - /// reject the request. - /// - /// Returns 0 only if no serial of any kind has been received yet. - pub fn get_latest(&self) -> u32 { - self.serials - .values() - .map(|serial_data| serial_data.serial) - .max() - .unwrap_or(0) - } + /// Returns the most recent serial across all tracked kinds. + /// + /// Wayland compositor serial numbers are monotonically increasing, so the + /// highest value is always the most recently received one. This is the + /// correct serial to use for [`set_selection`] when the triggering event + /// may have been a mouse press rather than a key press: using 0 (the + /// default when a kind has never been seen) causes compositors to silently + /// reject the request. + /// + /// Returns 0 only if no serial of any kind has been received yet. + pub fn get_latest(&self) -> u32 { + self.serials + .values() + .map(|serial_data| serial_data.serial) + .max() + .unwrap_or(0) + } } diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index c8d3819429..b381890317 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -1,9 +1,9 @@ use std::{ - cell::{Ref, RefCell, RefMut}, - ffi::c_void, - ptr::NonNull, - rc::Rc, - sync::Arc, + cell::{Ref, RefCell, RefMut}, + ffi::c_void, + ptr::NonNull, + rc::Rc, + sync::Arc, }; use collections::{FxHashSet, HashMap}; @@ -13,16 +13,16 @@ use raw_window_handle as rwh; use wayland_backend::client::ObjectId; use wayland_client::WEnum; use wayland_client::{ - Proxy, - protocol::{wl_output, wl_surface}, + Proxy, + protocol::{wl_output, wl_surface}, }; use wayland_protocols::wp::viewporter::client::wp_viewport; use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; use wayland_protocols::xdg::shell::client::xdg_surface; use wayland_protocols::xdg::shell::client::xdg_toplevel::{self}; use wayland_protocols::{ - wp::fractional_scale::v1::client::wp_fractional_scale_v1, - xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, + wp::fractional_scale::v1::client::wp_fractional_scale_v1, + xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, }; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur; use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1; @@ -30,33 +30,33 @@ use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1; use crate::linux::wayland::{display::WaylandDisplay, serial::SerialKind}; use crate::linux::{Globals, Output, WaylandClientStatePtr, get_window}; use gpui::{ - AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, GpuSpecs, Modifiers, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, Tiling, - WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, - WindowDecorations, WindowKind, WindowParams, layer_shell::LayerShellNotSupportedError, px, - size, + AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, GpuSpecs, Modifiers, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, Tiling, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, + WindowDecorations, WindowKind, WindowParams, layer_shell::LayerShellNotSupportedError, px, + size, }; use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig, wgpu}; #[derive(Default)] pub(crate) struct Callbacks { - request_frame: Option>, - input: Option gpui::DispatchEventResult>>, - active_status_change: Option>, - hover_status_change: Option>, - resize: Option, f32)>>, - moved: Option>, - should_close: Option bool>>, - close: Option>, - appearance_changed: Option>, - button_layout_changed: Option>, + request_frame: Option>, + input: Option gpui::DispatchEventResult>>, + active_status_change: Option>, + hover_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, + button_layout_changed: Option>, } #[derive(Debug, Clone, Copy)] struct RawWindow { - window: *mut c_void, - display: *mut c_void, + window: *mut c_void, + display: *mut c_void, } // Safety: The raw pointers in RawWindow point to Wayland surface/display @@ -66,1705 +66,1705 @@ unsafe impl Send for RawWindow {} unsafe impl Sync for RawWindow {} impl rwh::HasWindowHandle for RawWindow { - fn window_handle(&self) -> Result, rwh::HandleError> { - let window = NonNull::new(self.window).unwrap(); - let handle = rwh::WaylandWindowHandle::new(window); - Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) - } + fn window_handle(&self) -> Result, rwh::HandleError> { + let window = NonNull::new(self.window).unwrap(); + let handle = rwh::WaylandWindowHandle::new(window); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } } impl rwh::HasDisplayHandle for RawWindow { - fn display_handle(&self) -> Result, rwh::HandleError> { - let display = NonNull::new(self.display).unwrap(); - let handle = rwh::WaylandDisplayHandle::new(display); - Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) - } + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = NonNull::new(self.display).unwrap(); + let handle = rwh::WaylandDisplayHandle::new(display); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } } #[derive(Debug)] struct InProgressConfigure { - size: Option>, - fullscreen: bool, - maximized: bool, - resizing: bool, - tiling: Tiling, + size: Option>, + fullscreen: bool, + maximized: bool, + resizing: bool, + tiling: Tiling, } pub struct WaylandWindowState { - surface_state: WaylandSurfaceState, - acknowledged_first_configure: bool, - parent: Option, - children: FxHashSet, - pub surface: wl_surface::WlSurface, - app_id: Option, - appearance: WindowAppearance, - blur: Option, - viewport: Option, - outputs: HashMap, - display: Option<(ObjectId, Output)>, - globals: Globals, - renderer: WgpuRenderer, - bounds: Bounds, - scale: f32, - input_handler: Option, - decorations: WindowDecorations, - background_appearance: WindowBackgroundAppearance, - fullscreen: bool, - maximized: bool, - tiling: Tiling, - window_bounds: Bounds, - client: WaylandClientStatePtr, - handle: AnyWindowHandle, - active: bool, - hovered: bool, - pub(crate) force_render_after_recovery: bool, - renderer_presented: bool, - in_progress_configure: Option, - resize_throttle: bool, - in_progress_window_controls: Option, - window_controls: WindowControls, - client_inset: Option, - accesskit_adapter: Option, + surface_state: WaylandSurfaceState, + acknowledged_first_configure: bool, + parent: Option, + children: FxHashSet, + pub surface: wl_surface::WlSurface, + app_id: Option, + appearance: WindowAppearance, + blur: Option, + viewport: Option, + outputs: HashMap, + display: Option<(ObjectId, Output)>, + globals: Globals, + renderer: WgpuRenderer, + bounds: Bounds, + scale: f32, + input_handler: Option, + decorations: WindowDecorations, + background_appearance: WindowBackgroundAppearance, + fullscreen: bool, + maximized: bool, + tiling: Tiling, + window_bounds: Bounds, + client: WaylandClientStatePtr, + handle: AnyWindowHandle, + active: bool, + hovered: bool, + pub(crate) force_render_after_recovery: bool, + renderer_presented: bool, + in_progress_configure: Option, + resize_throttle: bool, + in_progress_window_controls: Option, + window_controls: WindowControls, + client_inset: Option, + accesskit_adapter: Option, } pub enum WaylandSurfaceState { - Xdg(WaylandXdgSurfaceState), - LayerShell(WaylandLayerSurfaceState), + Xdg(WaylandXdgSurfaceState), + LayerShell(WaylandLayerSurfaceState), } impl WaylandSurfaceState { - fn new( - surface: &wl_surface::WlSurface, - globals: &Globals, - params: &WindowParams, - parent: Option, - target_output: Option, - ) -> anyhow::Result { - // For layer_shell windows, create a layer surface instead of an xdg surface - if let WindowKind::LayerShell(options) = ¶ms.kind { - let Some(layer_shell) = globals.layer_shell.as_ref() else { - return Err(LayerShellNotSupportedError.into()); - }; + fn new( + surface: &wl_surface::WlSurface, + globals: &Globals, + params: &WindowParams, + parent: Option, + target_output: Option, + ) -> anyhow::Result { + // For layer_shell windows, create a layer surface instead of an xdg surface + if let WindowKind::LayerShell(options) = ¶ms.kind { + let Some(layer_shell) = globals.layer_shell.as_ref() else { + return Err(LayerShellNotSupportedError.into()); + }; - let layer_surface = layer_shell.get_layer_surface( - &surface, - target_output.as_ref(), - super::layer_shell::wayland_layer(options.layer), - options.namespace.clone(), - &globals.qh, - surface.id(), - ); + let layer_surface = layer_shell.get_layer_surface( + &surface, + target_output.as_ref(), + super::layer_shell::wayland_layer(options.layer), + options.namespace.clone(), + &globals.qh, + surface.id(), + ); - let width = f32::from(params.bounds.size.width); - let height = f32::from(params.bounds.size.height); - layer_surface.set_size(width as u32, height as u32); + let width = f32::from(params.bounds.size.width); + let height = f32::from(params.bounds.size.height); + layer_surface.set_size(width as u32, height as u32); - layer_surface.set_anchor(super::layer_shell::wayland_anchor(options.anchor)); - layer_surface.set_keyboard_interactivity( - super::layer_shell::wayland_keyboard_interactivity(options.keyboard_interactivity), - ); + layer_surface.set_anchor(super::layer_shell::wayland_anchor(options.anchor)); + layer_surface.set_keyboard_interactivity( + super::layer_shell::wayland_keyboard_interactivity(options.keyboard_interactivity), + ); - if let Some(margin) = options.margin { - layer_surface.set_margin( - f32::from(margin.0) as i32, - f32::from(margin.1) as i32, - f32::from(margin.2) as i32, - f32::from(margin.3) as i32, - ) - } + if let Some(margin) = options.margin { + layer_surface.set_margin( + f32::from(margin.0) as i32, + f32::from(margin.1) as i32, + f32::from(margin.2) as i32, + f32::from(margin.3) as i32, + ) + } - if let Some(exclusive_zone) = options.exclusive_zone { - layer_surface.set_exclusive_zone(f32::from(exclusive_zone) as i32); - } + if let Some(exclusive_zone) = options.exclusive_zone { + layer_surface.set_exclusive_zone(f32::from(exclusive_zone) as i32); + } - if let Some(exclusive_edge) = options.exclusive_edge { - layer_surface - .set_exclusive_edge(super::layer_shell::wayland_anchor(exclusive_edge)); - } + if let Some(exclusive_edge) = options.exclusive_edge { + layer_surface + .set_exclusive_edge(super::layer_shell::wayland_anchor(exclusive_edge)); + } - return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { - layer_surface, - })); - } + return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { + layer_surface, + })); + } - // All other WindowKinds result in a regular xdg surface - let xdg_surface = globals - .wm_base - .get_xdg_surface(&surface, &globals.qh, surface.id()); + // All other WindowKinds result in a regular xdg surface + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); - let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); - let xdg_parent = parent.as_ref().and_then(|w| w.toplevel()); + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + let xdg_parent = parent.as_ref().and_then(|w| w.toplevel()); - if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { - toplevel.set_parent(xdg_parent.as_ref()); - } + if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { + toplevel.set_parent(xdg_parent.as_ref()); + } - let dialog = if params.kind == WindowKind::Dialog { - let dialog = globals.dialog.as_ref().map(|dialog| { - let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ()); - xdg_dialog.set_modal(); - xdg_dialog - }); + let dialog = if params.kind == WindowKind::Dialog { + let dialog = globals.dialog.as_ref().map(|dialog| { + let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ()); + xdg_dialog.set_modal(); + xdg_dialog + }); - if let Some(parent) = parent.as_ref() { - parent.add_child(surface.id()); - } + if let Some(parent) = parent.as_ref() { + parent.add_child(surface.id()); + } - dialog - } else { - None - }; + dialog + } else { + None + }; - if let Some(size) = params.window_min_size { - toplevel.set_min_size(f32::from(size.width) as i32, f32::from(size.height) as i32); - } + if let Some(size) = params.window_min_size { + toplevel.set_min_size(f32::from(size.width) as i32, f32::from(size.height) as i32); + } - // Attempt to set up window decorations based on the requested configuration - let decoration = globals - .decoration_manager - .as_ref() - .map(|decoration_manager| { - decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) - }); + // Attempt to set up window decorations based on the requested configuration + let decoration = globals + .decoration_manager + .as_ref() + .map(|decoration_manager| { + decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) + }); - Ok(WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { - xdg_surface, - toplevel, - decoration, - dialog, - })) - } + Ok(WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration, + dialog, + })) + } } pub struct WaylandXdgSurfaceState { - xdg_surface: xdg_surface::XdgSurface, - toplevel: xdg_toplevel::XdgToplevel, - decoration: Option, - dialog: Option, + xdg_surface: xdg_surface::XdgSurface, + toplevel: xdg_toplevel::XdgToplevel, + decoration: Option, + dialog: Option, } pub struct WaylandLayerSurfaceState { - layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, } impl WaylandSurfaceState { - fn ack_configure(&self, serial: u32) { - match self { - WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { - xdg_surface.ack_configure(serial); - } - WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { - layer_surface.ack_configure(serial); - } - } - } + fn ack_configure(&self, serial: u32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.ack_configure(serial); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + layer_surface.ack_configure(serial); + } + } + } - fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> { - if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self { - decoration.as_ref() - } else { - None - } - } + fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self { + decoration.as_ref() + } else { + None + } + } - fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> { - if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self { - Some(toplevel) - } else { - None - } - } + fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self { + Some(toplevel) + } else { + None + } + } - fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) { - match self { - WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { - xdg_surface.set_window_geometry(x, y, width, height); - } - WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { - // cannot set window position of a layer surface - layer_surface.set_size(width as u32, height as u32); - } - } - } + fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.set_window_geometry(x, y, width, height); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + // cannot set window position of a layer surface + layer_surface.set_size(width as u32, height as u32); + } + } + } - fn set_exclusive_zone(&self, zone: i32) { - if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) = - self - { - layer_surface.set_exclusive_zone(zone); - } - } + fn set_exclusive_zone(&self, zone: i32) { + if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) = + self + { + layer_surface.set_exclusive_zone(zone); + } + } - fn destroy(&mut self) { - match self { - WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { - xdg_surface, - toplevel, - decoration: _decoration, - dialog, - }) => { - // drop the dialog before toplevel so compositor can explicitly unapply it's effects - if let Some(dialog) = dialog { - dialog.destroy(); - } + fn destroy(&mut self) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration: _decoration, + dialog, + }) => { + // drop the dialog before toplevel so compositor can explicitly unapply it's effects + if let Some(dialog) = dialog { + dialog.destroy(); + } - // The role object (toplevel) must always be destroyed before the xdg_surface. - // See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy - toplevel.destroy(); - xdg_surface.destroy(); - } - WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => { - layer_surface.destroy(); - } - } - } + // The role object (toplevel) must always be destroyed before the xdg_surface. + // See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy + toplevel.destroy(); + xdg_surface.destroy(); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => { + layer_surface.destroy(); + } + } + } } #[derive(Clone)] pub struct WaylandWindowStatePtr { - state: Rc>, - callbacks: Rc>, + state: Rc>, + callbacks: Rc>, } impl WaylandWindowState { - pub(crate) fn new( - handle: AnyWindowHandle, - surface: wl_surface::WlSurface, - surface_state: WaylandSurfaceState, - appearance: WindowAppearance, - viewport: Option, - client: WaylandClientStatePtr, - globals: Globals, - gpu_context: gpui_wgpu::GpuContext, - compositor_gpu: Option, - gpu_requirements: Option, - options: WindowParams, - parent: Option, - ) -> anyhow::Result { - let renderer = { - let raw_window = RawWindow { - window: surface.id().as_ptr().cast::(), - display: surface - .backend() - .upgrade() - .unwrap() - .display_ptr() - .cast::(), - }; - let config = WgpuSurfaceConfig { - size: Size { - width: DevicePixels(f32::from(options.bounds.size.width) as i32), - height: DevicePixels(f32::from(options.bounds.size.height) as i32), - }, - transparent: true, - // Prefer Mailbox to avoid blocking. Falls back to FIFO if Mailbox is unsupported. - preferred_present_mode: Some(wgpu::PresentMode::Mailbox), - }; - WgpuRenderer::new( - gpu_context, - &raw_window, - config, - compositor_gpu, - gpu_requirements, - )? - }; + pub(crate) fn new( + handle: AnyWindowHandle, + surface: wl_surface::WlSurface, + surface_state: WaylandSurfaceState, + appearance: WindowAppearance, + viewport: Option, + client: WaylandClientStatePtr, + globals: Globals, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + gpu_requirements: Option, + options: WindowParams, + parent: Option, + ) -> anyhow::Result { + let renderer = { + let raw_window = RawWindow { + window: surface.id().as_ptr().cast::(), + display: surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + let config = WgpuSurfaceConfig { + size: Size { + width: DevicePixels(f32::from(options.bounds.size.width) as i32), + height: DevicePixels(f32::from(options.bounds.size.height) as i32), + }, + transparent: true, + // Prefer Mailbox to avoid blocking. Falls back to FIFO if Mailbox is unsupported. + preferred_present_mode: Some(wgpu::PresentMode::Mailbox), + }; + WgpuRenderer::new( + gpu_context, + &raw_window, + config, + compositor_gpu, + gpu_requirements, + )? + }; - if let WaylandSurfaceState::Xdg(ref xdg_state) = surface_state { - if let Some(title) = options.titlebar.and_then(|titlebar| titlebar.title) { - xdg_state.toplevel.set_title(title.to_string()); - } - // Set max window size based on the GPU's maximum texture dimension. - // This prevents the window from being resized larger than what the GPU can render. - let max_texture_size = renderer.max_texture_size() as i32; - xdg_state - .toplevel - .set_max_size(max_texture_size, max_texture_size); - } + if let WaylandSurfaceState::Xdg(ref xdg_state) = surface_state { + if let Some(title) = options.titlebar.and_then(|titlebar| titlebar.title) { + xdg_state.toplevel.set_title(title.to_string()); + } + // Set max window size based on the GPU's maximum texture dimension. + // This prevents the window from being resized larger than what the GPU can render. + let max_texture_size = renderer.max_texture_size() as i32; + xdg_state + .toplevel + .set_max_size(max_texture_size, max_texture_size); + } - Ok(Self { - surface_state, - acknowledged_first_configure: false, - parent, - children: FxHashSet::default(), - surface, - app_id: None, - blur: None, - viewport, - globals, - outputs: HashMap::default(), - display: None, - renderer, - bounds: options.bounds, - scale: 1.0, - input_handler: None, - decorations: WindowDecorations::Client, - background_appearance: WindowBackgroundAppearance::Opaque, - fullscreen: false, - maximized: false, - tiling: Tiling::default(), - window_bounds: options.bounds, - in_progress_configure: None, - resize_throttle: false, - client, - appearance, - handle, - active: false, - hovered: false, - force_render_after_recovery: false, - renderer_presented: false, - in_progress_window_controls: None, - window_controls: WindowControls::default(), - client_inset: None, - accesskit_adapter: None, - }) - } + Ok(Self { + surface_state, + acknowledged_first_configure: false, + parent, + children: FxHashSet::default(), + surface, + app_id: None, + blur: None, + viewport, + globals, + outputs: HashMap::default(), + display: None, + renderer, + bounds: options.bounds, + scale: 1.0, + input_handler: None, + decorations: WindowDecorations::Client, + background_appearance: WindowBackgroundAppearance::Opaque, + fullscreen: false, + maximized: false, + tiling: Tiling::default(), + window_bounds: options.bounds, + in_progress_configure: None, + resize_throttle: false, + client, + appearance, + handle, + active: false, + hovered: false, + force_render_after_recovery: false, + renderer_presented: false, + in_progress_window_controls: None, + window_controls: WindowControls::default(), + client_inset: None, + accesskit_adapter: None, + }) + } - pub fn is_transparent(&self) -> bool { - self.decorations == WindowDecorations::Client - || self.background_appearance != WindowBackgroundAppearance::Opaque - } + pub fn is_transparent(&self) -> bool { + self.decorations == WindowDecorations::Client + || self.background_appearance != WindowBackgroundAppearance::Opaque + } - fn update_subpixel_layout(&mut self) { - use wayland_client::protocol::wl_output::Subpixel; - let is_bgr = self - .display - .as_ref() - .and_then(|(_, output)| output.subpixel) - .is_some_and(|s| s == Subpixel::HorizontalBgr); - self.renderer.set_subpixel_layout(is_bgr); - } + fn update_subpixel_layout(&mut self) { + use wayland_client::protocol::wl_output::Subpixel; + let is_bgr = self + .display + .as_ref() + .and_then(|(_, output)| output.subpixel) + .is_some_and(|s| s == Subpixel::HorizontalBgr); + self.renderer.set_subpixel_layout(is_bgr); + } - pub fn primary_output_scale(&mut self) -> i32 { - let mut scale = 1; - let mut current_output = self.display.take(); - for (id, output) in self.outputs.iter() { - if let Some((_, output_data)) = ¤t_output { - if output.scale > output_data.scale { - current_output = Some((id.clone(), output.clone())); - } - } else { - current_output = Some((id.clone(), output.clone())); - } - scale = scale.max(output.scale); - } - self.display = current_output; - scale - } + pub fn primary_output_scale(&mut self) -> i32 { + let mut scale = 1; + let mut current_output = self.display.take(); + for (id, output) in self.outputs.iter() { + if let Some((_, output_data)) = ¤t_output { + if output.scale > output_data.scale { + current_output = Some((id.clone(), output.clone())); + } + } else { + current_output = Some((id.clone(), output.clone())); + } + scale = scale.max(output.scale); + } + self.display = current_output; + scale + } - pub fn inset(&self) -> Pixels { - match self.decorations { - WindowDecorations::Server => px(0.0), - WindowDecorations::Client => self.client_inset.unwrap_or(px(0.0)), - } - } + pub fn inset(&self) -> Pixels { + match self.decorations { + WindowDecorations::Server => px(0.0), + WindowDecorations::Client => self.client_inset.unwrap_or(px(0.0)), + } + } } pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); pub enum ImeInput { - InsertText(String), - SetMarkedText(String), - UnmarkText, - DeleteText, + InsertText(String), + SetMarkedText(String), + UnmarkText, + DeleteText, } impl Drop for WaylandWindow { - fn drop(&mut self) { - let mut state = self.0.state.borrow_mut(); - let surface_id = state.surface.id(); - if let Some(parent) = state.parent.as_ref() { - parent.state.borrow_mut().children.remove(&surface_id); - } + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + let surface_id = state.surface.id(); + if let Some(parent) = state.parent.as_ref() { + parent.state.borrow_mut().children.remove(&surface_id); + } - let client = state.client.clone(); + let client = state.client.clone(); - state.renderer.destroy(); + state.renderer.destroy(); - // Destroy blur first, this has no dependencies. - if let Some(blur) = &state.blur { - blur.release(); - } + // Destroy blur first, this has no dependencies. + if let Some(blur) = &state.blur { + blur.release(); + } - // Decorations must be destroyed before the xdg state. - // See https://wayland.app/protocols/xdg-decoration-unstable-v1#zxdg_toplevel_decoration_v1 - if let Some(decoration) = &state.surface_state.decoration() { - decoration.destroy(); - } + // Decorations must be destroyed before the xdg state. + // See https://wayland.app/protocols/xdg-decoration-unstable-v1#zxdg_toplevel_decoration_v1 + if let Some(decoration) = &state.surface_state.decoration() { + decoration.destroy(); + } - // Surface state might contain xdg_toplevel/xdg_surface which can be destroyed now that - // decorations are gone. layer_surface has no dependencies. - state.surface_state.destroy(); + // Surface state might contain xdg_toplevel/xdg_surface which can be destroyed now that + // decorations are gone. layer_surface has no dependencies. + state.surface_state.destroy(); - // Viewport must be destroyed before the wl_surface. - // See https://wayland.app/protocols/viewporter#wp_viewport - if let Some(viewport) = &state.viewport { - viewport.destroy(); - } + // Viewport must be destroyed before the wl_surface. + // See https://wayland.app/protocols/viewporter#wp_viewport + if let Some(viewport) = &state.viewport { + viewport.destroy(); + } - // The wl_surface itself should always be destroyed last. - state.surface.destroy(); + // The wl_surface itself should always be destroyed last. + state.surface.destroy(); - let state_ptr = self.0.clone(); - state - .globals - .executor - .spawn(async move { - state_ptr.close(); - client.drop_window(&surface_id) - }) - .detach(); - drop(state); - } + let state_ptr = self.0.clone(); + state + .globals + .executor + .spawn(async move { + state_ptr.close(); + client.drop_window(&surface_id) + }) + .detach(); + drop(state); + } } impl WaylandWindow { - fn borrow(&self) -> Ref<'_, WaylandWindowState> { - self.0.state.borrow() - } + fn borrow(&self) -> Ref<'_, WaylandWindowState> { + self.0.state.borrow() + } - fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> { - self.0.state.borrow_mut() - } + fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> { + self.0.state.borrow_mut() + } - pub fn new( - handle: AnyWindowHandle, - globals: Globals, - gpu_context: gpui_wgpu::GpuContext, - compositor_gpu: Option, - gpu_requirements: Option, - client: WaylandClientStatePtr, - params: WindowParams, - appearance: WindowAppearance, - parent: Option, - target_output: Option, - ) -> anyhow::Result<(Self, ObjectId)> { - let surface = globals.compositor.create_surface(&globals.qh, ()); - let surface_state = - WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone(), target_output)?; + pub fn new( + handle: AnyWindowHandle, + globals: Globals, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + gpu_requirements: Option, + client: WaylandClientStatePtr, + params: WindowParams, + appearance: WindowAppearance, + parent: Option, + target_output: Option, + ) -> anyhow::Result<(Self, ObjectId)> { + let surface = globals.compositor.create_surface(&globals.qh, ()); + let surface_state = + WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone(), target_output)?; - if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { - fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); - } + if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { + fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); + } - let viewport = globals - .viewporter - .as_ref() - .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + let viewport = globals + .viewporter + .as_ref() + .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); - let this = Self(WaylandWindowStatePtr { - state: Rc::new(RefCell::new(WaylandWindowState::new( - handle, - surface.clone(), - surface_state, - appearance, - viewport, - client, - globals, - gpu_context, - compositor_gpu, - gpu_requirements, - params, - parent, - )?)), - callbacks: Rc::new(RefCell::new(Callbacks::default())), - }); + let this = Self(WaylandWindowStatePtr { + state: Rc::new(RefCell::new(WaylandWindowState::new( + handle, + surface.clone(), + surface_state, + appearance, + viewport, + client, + globals, + gpu_context, + compositor_gpu, + gpu_requirements, + params, + parent, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + }); - // Kick things off - surface.commit(); + // Kick things off + surface.commit(); - Ok((this, surface.id())) - } + Ok((this, surface.id())) + } } impl WaylandWindowStatePtr { - pub fn handle(&self) -> AnyWindowHandle { - self.state.borrow().handle - } + pub fn handle(&self) -> AnyWindowHandle { + self.state.borrow().handle + } - pub fn surface(&self) -> wl_surface::WlSurface { - self.state.borrow().surface.clone() - } + pub fn surface(&self) -> wl_surface::WlSurface { + self.state.borrow().surface.clone() + } - pub fn toplevel(&self) -> Option { - self.state.borrow().surface_state.toplevel().cloned() - } + pub fn toplevel(&self) -> Option { + self.state.borrow().surface_state.toplevel().cloned() + } - pub fn ptr_eq(&self, other: &Self) -> bool { - Rc::ptr_eq(&self.state, &other.state) - } + pub fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } - pub fn add_child(&self, child: ObjectId) { - let mut state = self.state.borrow_mut(); - state.children.insert(child); - } + pub fn add_child(&self, child: ObjectId) { + let mut state = self.state.borrow_mut(); + state.children.insert(child); + } - pub fn is_blocked(&self) -> bool { - let state = self.state.borrow(); - !state.children.is_empty() - } + pub fn is_blocked(&self) -> bool { + let state = self.state.borrow(); + !state.children.is_empty() + } - pub fn frame(&self) { - let mut state = self.state.borrow_mut(); - state.surface.frame(&state.globals.qh, state.surface.id()); - state.resize_throttle = false; - let force_render = state.force_render_after_recovery; - state.force_render_after_recovery = false; - drop(state); + pub fn frame(&self) { + let mut state = self.state.borrow_mut(); + state.surface.frame(&state.globals.qh, state.surface.id()); + state.resize_throttle = false; + let force_render = state.force_render_after_recovery; + state.force_render_after_recovery = false; + drop(state); - let mut cb = self.callbacks.borrow_mut(); - if let Some(fun) = cb.request_frame.as_mut() { - fun(RequestFrameOptions { - force_render, - ..Default::default() - }); - } - } + let mut cb = self.callbacks.borrow_mut(); + if let Some(fun) = cb.request_frame.as_mut() { + fun(RequestFrameOptions { + force_render, + ..Default::default() + }); + } + } - pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { - if let xdg_surface::Event::Configure { serial } = event { - { - let mut state = self.state.borrow_mut(); - if let Some(window_controls) = state.in_progress_window_controls.take() { - state.window_controls = window_controls; + pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { + if let xdg_surface::Event::Configure { serial } = event { + { + let mut state = self.state.borrow_mut(); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; - drop(state); - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { - appearance_changed(); - } - } - } - { - let mut state = self.state.borrow_mut(); + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + } + { + let mut state = self.state.borrow_mut(); - if let Some(mut configure) = state.in_progress_configure.take() { - let got_unmaximized = state.maximized && !configure.maximized; - state.fullscreen = configure.fullscreen; - state.maximized = configure.maximized; - state.tiling = configure.tiling; - // Limit interactive resizes to once per vblank - if configure.resizing && state.resize_throttle { - state.surface_state.ack_configure(serial); - return; - } else if configure.resizing { - state.resize_throttle = true; - } - if !configure.fullscreen && !configure.maximized { - configure.size = if got_unmaximized { - Some(state.window_bounds.size) - } else { - compute_outer_size(state.inset(), configure.size, state.tiling) - }; - if let Some(size) = configure.size { - state.window_bounds = Bounds { - origin: Point::default(), - size, - }; - } - } - drop(state); - if let Some(size) = configure.size { - self.resize(size); - } - } - } - let mut state = self.state.borrow_mut(); - state.surface_state.ack_configure(serial); + if let Some(mut configure) = state.in_progress_configure.take() { + let got_unmaximized = state.maximized && !configure.maximized; + state.fullscreen = configure.fullscreen; + state.maximized = configure.maximized; + state.tiling = configure.tiling; + // Limit interactive resizes to once per vblank + if configure.resizing && state.resize_throttle { + state.surface_state.ack_configure(serial); + return; + } else if configure.resizing { + state.resize_throttle = true; + } + if !configure.fullscreen && !configure.maximized { + configure.size = if got_unmaximized { + Some(state.window_bounds.size) + } else { + compute_outer_size(state.inset(), configure.size, state.tiling) + }; + if let Some(size) = configure.size { + state.window_bounds = Bounds { + origin: Point::default(), + size, + }; + } + } + drop(state); + if let Some(size) = configure.size { + self.resize(size); + } + } + } + let mut state = self.state.borrow_mut(); + state.surface_state.ack_configure(serial); - let window_geometry = inset_by_tiling( - state.bounds.map_origin(|_| px(0.0)), - state.inset(), - state.tiling, - ) - .map(|v| f32::from(v) as i32) - .map_size(|v| if v <= 0 { 1 } else { v }); + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + .map(|v| f32::from(v) as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); - state.surface_state.set_geometry( - window_geometry.origin.x, - window_geometry.origin.y, - window_geometry.size.width, - window_geometry.size.height, - ); + state.surface_state.set_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); - let request_frame_callback = !state.acknowledged_first_configure; - if request_frame_callback { - state.acknowledged_first_configure = true; - drop(state); - self.frame(); - } - } - } + let request_frame_callback = !state.acknowledged_first_configure; + if request_frame_callback { + state.acknowledged_first_configure = true; + drop(state); + self.frame(); + } + } + } - pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { - if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { - match mode { - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { - self.state.borrow_mut().decorations = WindowDecorations::Server; - let callback = self.callbacks.borrow_mut().appearance_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().appearance_changed = Some(fun); - } - } - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { - self.state.borrow_mut().decorations = WindowDecorations::Client; - // Update background to be transparent - let callback = self.callbacks.borrow_mut().appearance_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().appearance_changed = Some(fun); - } - } - WEnum::Value(_) => { - log::warn!("Unknown decoration mode"); - } - WEnum::Unknown(v) => { - log::warn!("Unknown decoration mode: {}", v); - } - } - } - } + pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { + if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { + match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Server; + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Client; + // Update background to be transparent + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + } + } + } + } - pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { - if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { - self.rescale(scale as f32 / 120.0); - } - } + pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { + if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { + self.rescale(scale as f32 / 120.0); + } + } - pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { - match event { - xdg_toplevel::Event::Configure { - width, - height, - states, - } => { - let size = if width == 0 || height == 0 { - None - } else { - Some(size(px(width as f32), px(height as f32))) - }; + pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + let size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; - let states = extract_states::(&states); + let states = extract_states::(&states); - let mut tiling = Tiling::default(); - let mut fullscreen = false; - let mut maximized = false; - let mut resizing = false; + let mut tiling = Tiling::default(); + let mut fullscreen = false; + let mut maximized = false; + let mut resizing = false; - for state in states { - match state { - xdg_toplevel::State::Maximized => { - maximized = true; - } - xdg_toplevel::State::Fullscreen => { - fullscreen = true; - } - xdg_toplevel::State::Resizing => resizing = true, - xdg_toplevel::State::TiledTop => { - tiling.top = true; - } - xdg_toplevel::State::TiledLeft => { - tiling.left = true; - } - xdg_toplevel::State::TiledRight => { - tiling.right = true; - } - xdg_toplevel::State::TiledBottom => { - tiling.bottom = true; - } - _ => { - // noop - } - } - } + for state in states { + match state { + xdg_toplevel::State::Maximized => { + maximized = true; + } + xdg_toplevel::State::Fullscreen => { + fullscreen = true; + } + xdg_toplevel::State::Resizing => resizing = true, + xdg_toplevel::State::TiledTop => { + tiling.top = true; + } + xdg_toplevel::State::TiledLeft => { + tiling.left = true; + } + xdg_toplevel::State::TiledRight => { + tiling.right = true; + } + xdg_toplevel::State::TiledBottom => { + tiling.bottom = true; + } + _ => { + // noop + } + } + } - if fullscreen || maximized { - tiling = Tiling::tiled(); - } + if fullscreen || maximized { + tiling = Tiling::tiled(); + } - let mut state = self.state.borrow_mut(); - state.in_progress_configure = Some(InProgressConfigure { - size, - fullscreen, - maximized, - resizing, - tiling, - }); + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen, + maximized, + resizing, + tiling, + }); - false - } - xdg_toplevel::Event::Close => { - let mut cb = self.callbacks.borrow_mut(); - if let Some(mut should_close) = cb.should_close.take() { - let result = (should_close)(); - cb.should_close = Some(should_close); - if result { - drop(cb); - self.close(); - } - result - } else { - true - } - } - xdg_toplevel::Event::WmCapabilities { capabilities } => { - let mut window_controls = WindowControls { - maximize: false, - minimize: false, - fullscreen: false, - window_menu: false, - }; + false + } + xdg_toplevel::Event::Close => { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + if result { + drop(cb); + self.close(); + } + result + } else { + true + } + } + xdg_toplevel::Event::WmCapabilities { capabilities } => { + let mut window_controls = WindowControls { + maximize: false, + minimize: false, + fullscreen: false, + window_menu: false, + }; - let states = extract_states::(&capabilities); + let states = extract_states::(&capabilities); - for state in states { - match state { - xdg_toplevel::WmCapabilities::Maximize => { - window_controls.maximize = true; - } - xdg_toplevel::WmCapabilities::Minimize => { - window_controls.minimize = true; - } - xdg_toplevel::WmCapabilities::Fullscreen => { - window_controls.fullscreen = true; - } - xdg_toplevel::WmCapabilities::WindowMenu => { - window_controls.window_menu = true; - } - _ => {} - } - } + for state in states { + match state { + xdg_toplevel::WmCapabilities::Maximize => { + window_controls.maximize = true; + } + xdg_toplevel::WmCapabilities::Minimize => { + window_controls.minimize = true; + } + xdg_toplevel::WmCapabilities::Fullscreen => { + window_controls.fullscreen = true; + } + xdg_toplevel::WmCapabilities::WindowMenu => { + window_controls.window_menu = true; + } + _ => {} + } + } - let mut state = self.state.borrow_mut(); - state.in_progress_window_controls = Some(window_controls); - false - } - _ => false, - } - } + let mut state = self.state.borrow_mut(); + state.in_progress_window_controls = Some(window_controls); + false + } + _ => false, + } + } - pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool { - match event { - zwlr_layer_surface_v1::Event::Configure { - width, - height, - serial, - } => { - let size = if width == 0 || height == 0 { - None - } else { - Some(size(px(width as f32), px(height as f32))) - }; + pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool { + match event { + zwlr_layer_surface_v1::Event::Configure { + width, + height, + serial, + } => { + let size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; - let mut state = self.state.borrow_mut(); - state.in_progress_configure = Some(InProgressConfigure { - size, - fullscreen: false, - maximized: false, - resizing: false, - tiling: Tiling::default(), - }); - drop(state); + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen: false, + maximized: false, + resizing: false, + tiling: Tiling::default(), + }); + drop(state); - // just do the same thing we'd do as an xdg_surface - self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial }); + // just do the same thing we'd do as an xdg_surface + self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial }); - false - } - zwlr_layer_surface_v1::Event::Closed => { - // unlike xdg, we don't have a choice here: the surface is closing. - true - } - _ => false, - } - } + false + } + zwlr_layer_surface_v1::Event::Closed => { + // unlike xdg, we don't have a choice here: the surface is closing. + true + } + _ => false, + } + } - #[allow(clippy::mutable_key_type)] - pub fn handle_surface_event( - &self, - event: wl_surface::Event, - outputs: HashMap, - ) { - let mut state = self.state.borrow_mut(); + #[allow(clippy::mutable_key_type)] + pub fn handle_surface_event( + &self, + event: wl_surface::Event, + outputs: HashMap, + ) { + let mut state = self.state.borrow_mut(); - match event { - wl_surface::Event::Enter { output } => { - let id = output.id(); + match event { + wl_surface::Event::Enter { output } => { + let id = output.id(); - let Some(output) = outputs.get(&id) else { - return; - }; + let Some(output) = outputs.get(&id) else { + return; + }; - state.outputs.insert(id, output.clone()); + state.outputs.insert(id, output.clone()); - let scale = state.primary_output_scale(); - state.update_subpixel_layout(); + let scale = state.primary_output_scale(); + state.update_subpixel_layout(); - // We use `PreferredBufferScale` instead to set the scale if it's available - if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { - state.surface.set_buffer_scale(scale); - drop(state); - self.rescale(scale as f32); - } - } - wl_surface::Event::Leave { output } => { - state.outputs.remove(&output.id()); + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + } + wl_surface::Event::Leave { output } => { + state.outputs.remove(&output.id()); - let scale = state.primary_output_scale(); - state.update_subpixel_layout(); + let scale = state.primary_output_scale(); + state.update_subpixel_layout(); - // We use `PreferredBufferScale` instead to set the scale if it's available - if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { - state.surface.set_buffer_scale(scale); - drop(state); - self.rescale(scale as f32); - } - } - wl_surface::Event::PreferredBufferScale { factor } => { - // We use `WpFractionalScale` instead to set the scale if it's available - if state.globals.fractional_scale_manager.is_none() { - state.surface.set_buffer_scale(factor); - drop(state); - self.rescale(factor as f32); - } - } - _ => {} - } - } + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + } + wl_surface::Event::PreferredBufferScale { factor } => { + // We use `WpFractionalScale` instead to set the scale if it's available + if state.globals.fractional_scale_manager.is_none() { + state.surface.set_buffer_scale(factor); + drop(state); + self.rescale(factor as f32); + } + } + _ => {} + } + } - pub fn handle_ime(&self, ime: ImeInput) { - if self.is_blocked() { - return; - } - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - match ime { - ImeInput::InsertText(text) => { - input_handler.replace_text_in_range(None, &text); - } - ImeInput::SetMarkedText(text) => { - input_handler.replace_and_mark_text_in_range(None, &text, None); - } - ImeInput::UnmarkText => { - input_handler.unmark_text(); - } - ImeInput::DeleteText => { - if let Some(marked) = input_handler.marked_text_range() { - input_handler.replace_text_in_range(Some(marked), ""); - } - } - } - self.state.borrow_mut().input_handler = Some(input_handler); - } - } + pub fn handle_ime(&self, ime: ImeInput) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + match ime { + ImeInput::InsertText(text) => { + input_handler.replace_text_in_range(None, &text); + } + ImeInput::SetMarkedText(text) => { + input_handler.replace_and_mark_text_in_range(None, &text, None); + } + ImeInput::UnmarkText => { + input_handler.unmark_text(); + } + ImeInput::DeleteText => { + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + } + } + self.state.borrow_mut().input_handler = Some(input_handler); + } + } - pub fn get_ime_area(&self) -> Option> { - let mut state = self.state.borrow_mut(); - let mut bounds: Option> = None; - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - if let Some(selection) = input_handler.marked_text_range() { - bounds = input_handler.bounds_for_range(selection.start..selection.start); - } - self.state.borrow_mut().input_handler = Some(input_handler); - } - bounds - } + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(selection) = input_handler.marked_text_range() { + bounds = input_handler.bounds_for_range(selection.start..selection.start); + } + self.state.borrow_mut().input_handler = Some(input_handler); + } + bounds + } - pub fn set_size_and_scale(&self, size: Option>, scale: Option) { - let (size, scale) = { - let mut state = self.state.borrow_mut(); - if size.is_none_or(|size| size == state.bounds.size) - && scale.is_none_or(|scale| scale == state.scale) - { - return; - } - if let Some(size) = size { - state.bounds.size = size; - } - if let Some(scale) = scale { - state.scale = scale; - } - let device_bounds = state.bounds.to_device_pixels(state.scale); - state.renderer.update_drawable_size(device_bounds.size); - (state.bounds.size, state.scale) - }; + pub fn set_size_and_scale(&self, size: Option>, scale: Option) { + let (size, scale) = { + let mut state = self.state.borrow_mut(); + if size.is_none_or(|size| size == state.bounds.size) + && scale.is_none_or(|scale| scale == state.scale) + { + return; + } + if let Some(size) = size { + state.bounds.size = size; + } + if let Some(scale) = scale { + state.scale = scale; + } + let device_bounds = state.bounds.to_device_pixels(state.scale); + state.renderer.update_drawable_size(device_bounds.size); + (state.bounds.size, state.scale) + }; - let callback = self.callbacks.borrow_mut().resize.take(); - if let Some(mut fun) = callback { - fun(size, scale); - self.callbacks.borrow_mut().resize = Some(fun); - } + let callback = self.callbacks.borrow_mut().resize.take(); + if let Some(mut fun) = callback { + fun(size, scale); + self.callbacks.borrow_mut().resize = Some(fun); + } - { - let state = self.state.borrow(); - if let Some(viewport) = &state.viewport { - viewport - .set_destination(f32::from(size.width) as i32, f32::from(size.height) as i32); - } - } - } + { + let state = self.state.borrow(); + if let Some(viewport) = &state.viewport { + viewport + .set_destination(f32::from(size.width) as i32, f32::from(size.height) as i32); + } + } + } - pub fn resize(&self, size: Size) { - self.set_size_and_scale(Some(size), None); - } + pub fn resize(&self, size: Size) { + self.set_size_and_scale(Some(size), None); + } - pub fn rescale(&self, scale: f32) { - self.set_size_and_scale(None, Some(scale)); - } + pub fn rescale(&self, scale: f32) { + self.set_size_and_scale(None, Some(scale)); + } - pub fn close(&self) { - let state = self.state.borrow(); - let client = state.client.get_client(); - #[allow(clippy::mutable_key_type)] - let children = state.children.clone(); - drop(state); + pub fn close(&self) { + let state = self.state.borrow(); + let client = state.client.get_client(); + #[allow(clippy::mutable_key_type)] + let children = state.children.clone(); + drop(state); - for child in children { - let mut client_state = client.borrow_mut(); - let window = get_window(&mut client_state, &child); - drop(client_state); + for child in children { + let mut client_state = client.borrow_mut(); + let window = get_window(&mut client_state, &child); + drop(client_state); - if let Some(child) = window { - child.close(); - } - } - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(fun) = callbacks.close.take() { - fun() - } - } + if let Some(child) = window { + child.close(); + } + } + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } - pub fn handle_input(&self, input: PlatformInput) { - if self.is_blocked() { - return; - } - let callback = self.callbacks.borrow_mut().input.take(); - if let Some(mut fun) = callback { - let result = fun(input.clone()); - self.callbacks.borrow_mut().input = Some(fun); - if !result.propagate { - return; - } - } - if let PlatformInput::KeyDown(event) = input - && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) - && let Some(key_char) = &event.keystroke.key_char - { - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.replace_text_in_range(None, key_char); - self.state.borrow_mut().input_handler = Some(input_handler); - } - } - } + pub fn handle_input(&self, input: PlatformInput) { + if self.is_blocked() { + return; + } + let callback = self.callbacks.borrow_mut().input.take(); + if let Some(mut fun) = callback { + let result = fun(input.clone()); + self.callbacks.borrow_mut().input = Some(fun); + if !result.propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input + && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) + && let Some(key_char) = &event.keystroke.key_char + { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, key_char); + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + } - pub fn set_focused(&self, focus: bool) { - self.state.borrow_mut().active = focus; - let callback = self.callbacks.borrow_mut().active_status_change.take(); - if let Some(mut fun) = callback { - fun(focus); - self.callbacks.borrow_mut().active_status_change = Some(fun); - } - if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { - adapter.update_window_focus_state(focus); - } - } + pub fn set_focused(&self, focus: bool) { + self.state.borrow_mut().active = focus; + let callback = self.callbacks.borrow_mut().active_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().active_status_change = Some(fun); + } + if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { + adapter.update_window_focus_state(focus); + } + } - pub fn set_hovered(&self, focus: bool) { - let callback = self.callbacks.borrow_mut().hover_status_change.take(); - if let Some(mut fun) = callback { - fun(focus); - self.callbacks.borrow_mut().hover_status_change = Some(fun); - } - } + pub fn set_hovered(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().hover_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().hover_status_change = Some(fun); + } + } - pub fn set_appearance(&mut self, appearance: WindowAppearance) { - self.state.borrow_mut().appearance = appearance; + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + self.state.borrow_mut().appearance = appearance; - let callback = self.callbacks.borrow_mut().appearance_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().appearance_changed = Some(fun); - } - } + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } - pub fn set_button_layout(&self) { - let callback = self.callbacks.borrow_mut().button_layout_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().button_layout_changed = Some(fun); - } - } + pub fn set_button_layout(&self) { + let callback = self.callbacks.borrow_mut().button_layout_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().button_layout_changed = Some(fun); + } + } - pub fn primary_output_scale(&self) -> i32 { - self.state.borrow_mut().primary_output_scale() - } + pub fn primary_output_scale(&self) -> i32 { + self.state.borrow_mut().primary_output_scale() + } } fn extract_states<'a, S: TryFrom + 'a>(states: &'a [u8]) -> impl Iterator + 'a where - >::Error: 'a, + >::Error: 'a, { - states - .chunks_exact(4) - .flat_map(TryInto::<[u8; 4]>::try_into) - .map(u32::from_ne_bytes) - .flat_map(S::try_from) + states + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(S::try_from) } impl rwh::HasWindowHandle for WaylandWindow { - fn window_handle(&self) -> Result, rwh::HandleError> { - let surface = self.0.surface().id().as_ptr() as *mut libc::c_void; - let c_ptr = NonNull::new(surface).ok_or(rwh::HandleError::Unavailable)?; - let handle = rwh::WaylandWindowHandle::new(c_ptr); - let raw_handle = rwh::RawWindowHandle::Wayland(handle); - Ok(unsafe { rwh::WindowHandle::borrow_raw(raw_handle) }) - } + fn window_handle(&self) -> Result, rwh::HandleError> { + let surface = self.0.surface().id().as_ptr() as *mut libc::c_void; + let c_ptr = NonNull::new(surface).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandWindowHandle::new(c_ptr); + let raw_handle = rwh::RawWindowHandle::Wayland(handle); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw_handle) }) + } } impl rwh::HasDisplayHandle for WaylandWindow { - fn display_handle(&self) -> Result, rwh::HandleError> { - let display = self - .0 - .surface() - .backend() - .upgrade() - .ok_or(rwh::HandleError::Unavailable)? - .display_ptr() as *mut libc::c_void; + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = self + .0 + .surface() + .backend() + .upgrade() + .ok_or(rwh::HandleError::Unavailable)? + .display_ptr() as *mut libc::c_void; - let c_ptr = NonNull::new(display).ok_or(rwh::HandleError::Unavailable)?; - let handle = rwh::WaylandDisplayHandle::new(c_ptr); - let raw_handle = rwh::RawDisplayHandle::Wayland(handle); - Ok(unsafe { rwh::DisplayHandle::borrow_raw(raw_handle) }) - } + let c_ptr = NonNull::new(display).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandDisplayHandle::new(c_ptr); + let raw_handle = rwh::RawDisplayHandle::Wayland(handle); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(raw_handle) }) + } } impl PlatformWindow for WaylandWindow { - fn bounds(&self) -> Bounds { - self.borrow().bounds - } - - fn is_maximized(&self) -> bool { - self.borrow().maximized - } - - fn window_bounds(&self) -> WindowBounds { - let state = self.borrow(); - if state.fullscreen { - WindowBounds::Fullscreen(state.window_bounds) - } else if state.maximized { - WindowBounds::Maximized(state.window_bounds) - } else { - drop(state); - WindowBounds::Windowed(self.bounds()) - } - } - - fn inner_window_bounds(&self) -> WindowBounds { - let state = self.borrow(); - if state.fullscreen { - WindowBounds::Fullscreen(state.window_bounds) - } else if state.maximized { - WindowBounds::Maximized(state.window_bounds) - } else { - let inset = state.inset(); - drop(state); - WindowBounds::Windowed(self.bounds().inset(inset)) - } - } - - fn content_size(&self) -> Size { - self.borrow().bounds.size - } - - fn resize(&mut self, size: Size) { - let state = self.borrow(); - let state_ptr = self.0.clone(); - - // Keep window geometry consistent with configure handling. On Wayland, window geometry is - // surface-local: resizing should not attempt to translate the window; the compositor - // controls placement. We also account for client-side decoration insets and tiling. - let window_geometry = inset_by_tiling( - Bounds { - origin: Point::default(), - size, - }, - state.inset(), - state.tiling, - ) - .map(|v| f32::from(v) as i32) - .map_size(|v| if v <= 0 { 1 } else { v }); - - state.surface_state.set_geometry( - window_geometry.origin.x, - window_geometry.origin.y, - window_geometry.size.width, - window_geometry.size.height, - ); - - state - .globals - .executor - .spawn(async move { state_ptr.resize(size) }) - .detach(); - } - - fn scale_factor(&self) -> f32 { - self.borrow().scale - } - - fn appearance(&self) -> WindowAppearance { - self.borrow().appearance - } - - fn display(&self) -> Option> { - let state = self.borrow(); - state.display.as_ref().map(|(id, display)| { - Rc::new(WaylandDisplay { - id: id.clone(), - name: display.name.clone(), - bounds: display.bounds.to_pixels(state.scale), - }) as Rc - }) - } - - fn mouse_position(&self) -> Point { - self.borrow() - .client - .get_client() - .borrow() - .mouse_location - .unwrap_or_default() - } - - fn modifiers(&self) -> Modifiers { - self.borrow().client.get_client().borrow().modifiers - } - - fn capslock(&self) -> Capslock { - self.borrow().client.get_client().borrow().capslock - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.borrow_mut().input_handler = Some(input_handler); - } - - fn take_input_handler(&mut self) -> Option { - self.borrow_mut().input_handler.take() - } - - fn prompt( - &self, - _level: PromptLevel, - _msg: &str, - _detail: Option<&str>, - _answers: &[PromptButton], - ) -> Option> { - None - } - - fn activate(&self) { - // Try to request an activation token. Even though the activation is likely going to be rejected, - // KWin and Mutter can use the app_id to visually indicate we're requesting attention. - let state = self.borrow(); - if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone()) - { - state.client.set_pending_activation(state.surface.id()); - let token = activation.get_activation_token(&state.globals.qh, ()); - // The serial isn't exactly important here, since the activation is probably going to be rejected anyway. - let serial = state.client.get_serial(SerialKind::MousePress); - token.set_app_id(app_id); - token.set_serial(serial, &state.globals.seat); - token.set_surface(&state.surface); - token.commit(); - } - } - - fn is_active(&self) -> bool { - self.borrow().active - } - - fn is_hovered(&self) -> bool { - self.borrow().hovered - } - - fn set_title(&mut self, title: &str) { - if let Some(toplevel) = self.borrow().surface_state.toplevel() { - toplevel.set_title(title.to_string()); - } - } - - fn set_app_id(&mut self, app_id: &str) { - let mut state = self.borrow_mut(); - if let Some(toplevel) = state.surface_state.toplevel() { - toplevel.set_app_id(app_id.to_owned()); - } - state.app_id = Some(app_id.to_owned()); - } - - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - let mut state = self.borrow_mut(); - state.background_appearance = background_appearance; - update_window(state); - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - self.borrow().background_appearance - } - - fn is_subpixel_rendering_supported(&self) -> bool { - let client = self.borrow().client.get_client(); - let state = client.borrow(); - state - .gpu_context - .borrow() - .as_ref() - .is_some_and(|ctx| ctx.supports_dual_source_blending()) - } - - fn minimize(&self) { - if let Some(toplevel) = self.borrow().surface_state.toplevel() { - toplevel.set_minimized(); - } - } - - fn zoom(&self) { - let state = self.borrow(); - if let Some(toplevel) = state.surface_state.toplevel() { - if !state.maximized { - toplevel.set_maximized(); - } else { - toplevel.unset_maximized(); - } - } - } - - fn toggle_fullscreen(&self) { - let state = self.borrow(); - if let Some(toplevel) = state.surface_state.toplevel() { - if !state.fullscreen { - toplevel.set_fullscreen(None); - } else { - toplevel.unset_fullscreen(); - } - } - } - - fn is_fullscreen(&self) -> bool { - self.borrow().fullscreen - } - - fn on_request_frame(&self, callback: Box) { - self.0.callbacks.borrow_mut().request_frame = Some(callback); - } - - fn on_input(&self, callback: Box gpui::DispatchEventResult>) { - self.0.callbacks.borrow_mut().input = Some(callback); - } - - fn on_active_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().active_status_change = Some(callback); - } - - fn on_hover_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().hover_status_change = Some(callback); - } - - fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.borrow_mut().resize = Some(callback); - } - - fn on_moved(&self, callback: Box) { - self.0.callbacks.borrow_mut().moved = Some(callback); - } - - fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.borrow_mut().should_close = Some(callback); - } - - fn on_close(&self, callback: Box) { - self.0.callbacks.borrow_mut().close = Some(callback); - } - - fn on_hit_test_window_control(&self, _callback: Box Option>) { - } - - fn on_appearance_changed(&self, callback: Box) { - self.0.callbacks.borrow_mut().appearance_changed = Some(callback); - } - - fn on_button_layout_changed(&self, callback: Box) { - self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); - } - - fn draw(&self, scene: &Scene) { - let mut state = self.borrow_mut(); - - if state.renderer.device_lost() { - let raw_window = RawWindow { - window: state.surface.id().as_ptr().cast::(), - display: state - .surface - .backend() - .upgrade() - .unwrap() - .display_ptr() - .cast::(), - }; - match state.renderer.recover(&raw_window) { - Ok(()) => {} - Err(err) => { - log::warn!("GPU recovery failed, will retry on next frame: {err}"); - } - } - - state.force_render_after_recovery = true; - return; - } - - state.renderer_presented = state.renderer.draw(scene); - - if state.renderer.needs_redraw() { - state.force_render_after_recovery = true; - } - } - - fn completed_frame(&self) { - let mut state = self.borrow_mut(); - - // Work around a bug in old versions of wlroots where committing without a buffer attached - // can cause invalid synchronization that leads to graphical corruption. - if !state.renderer_presented { - state.surface.commit(); - } - - state.renderer_presented = false; - } - - fn sprite_atlas(&self) -> Arc { - let state = self.borrow(); - state.renderer.sprite_atlas().clone() - } - - fn show_window_menu(&self, position: Point) { - let state = self.borrow(); - let serial = state.client.get_serial(SerialKind::MousePress); - if let Some(toplevel) = state.surface_state.toplevel() { - toplevel.show_window_menu( - &state.globals.seat, - serial, - f32::from(position.x) as i32, - f32::from(position.y) as i32, - ); - } - } - - fn start_window_move(&self) { - let state = self.borrow(); - let serial = state.client.get_serial(SerialKind::MousePress); - if let Some(toplevel) = state.surface_state.toplevel() { - toplevel._move(&state.globals.seat, serial); - } - } - - fn start_window_resize(&self, edge: gpui::ResizeEdge) { - let state = self.borrow(); - if let Some(toplevel) = state.surface_state.toplevel() { - toplevel.resize( - &state.globals.seat, - state.client.get_serial(SerialKind::MousePress), - edge.to_xdg(), - ) - } - } - - fn set_input_region(&self, rects: &[Bounds]) { - let state = self.borrow(); - if rects.is_empty() { - state.surface.set_input_region(None); - } else { - let region = state - .globals - .compositor - .create_region(&state.globals.qh, ()); - rects - .iter() - .map(|rect| rect.map(|pixels| f32::from(pixels) as i32)) - .for_each(|rect| { - region.add( - rect.origin.x, - rect.origin.y, - rect.size.width, - rect.size.height, - ) - }); - state.surface.set_input_region(Some(®ion)); - region.destroy(); - } - state.surface.commit(); - } - - fn set_exclusive_zone(&self, zone: Pixels) { - let state = self.borrow(); - state - .surface_state - .set_exclusive_zone(f32::from(zone) as i32); - state.surface.commit(); - } - - fn window_decorations(&self) -> Decorations { - let state = self.borrow(); - match state.decorations { - WindowDecorations::Server => Decorations::Server, - WindowDecorations::Client => Decorations::Client { - tiling: state.tiling, - }, - } - } - - fn request_decorations(&self, decorations: WindowDecorations) { - let mut state = self.borrow_mut(); - match state.surface_state.decoration().as_ref() { - Some(decoration) => { - decoration.set_mode(decorations.to_xdg()); - state.decorations = decorations; - update_window(state); - } - None => { - if matches!(decorations, WindowDecorations::Server) { - log::info!( - "Server-side decorations requested, but the Wayland server does not support them. Falling back to client-side decorations." - ); - } - state.decorations = WindowDecorations::Client; - update_window(state); - } - } - } - - fn window_controls(&self) -> WindowControls { - self.borrow().window_controls - } - - fn set_client_inset(&self, inset: Pixels) { - let mut state = self.borrow_mut(); - if Some(inset) != state.client_inset { - state.client_inset = Some(inset); - update_window(state); - } - } - - fn update_ime_position(&self, bounds: Bounds) { - let state = self.borrow(); - state.client.update_ime_position(bounds); - } - - fn gpu_specs(&self) -> Option { - self.borrow().renderer.gpu_specs().into() - } - - fn gpu_context(&self) -> Option> { - let (device, queue) = self.borrow().renderer.gpu_context(); - Some(Box::new((device, queue))) - } - - fn gpu_device_lost(&self) -> Option { - // Only loads an atomic flag — safe even mid-recovery, when - // `gpu_context` would panic on the torn-down resources. - Some(self.borrow().renderer.device_lost()) - } - - fn play_system_bell(&self) { - let state = self.borrow(); - let surface = if state.surface_state.toplevel().is_some() { - Some(&state.surface) - } else { - None - }; - if let Some(bell) = state.globals.system_bell.as_ref() { - bell.ring(surface); - } - } - - fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { - let activation_handler = TrivialActivationHandler { - callback: callbacks.activation, - }; - let action_handler = TrivialActionHandler(callbacks.action); - let deactivation_handler = TrivialDeactivationHandler { - callback: callbacks.deactivation, - }; - - let adapter = - accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); - - self.borrow_mut().accesskit_adapter = Some(adapter); - } - - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { - let mut state = self.borrow_mut(); - if let Some(adapter) = state.accesskit_adapter.as_mut() { - adapter.update_if_active(|| tree_update); - } - } - - fn a11y_update_window_bounds(&self) { - // Wayland doesn't expose window position, so this is a no-op - } + fn bounds(&self) -> Bounds { + self.borrow().bounds + } + + fn is_maximized(&self) -> bool { + self.borrow().maximized + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + drop(state); + WindowBounds::Windowed(self.bounds()) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + let inset = state.inset(); + drop(state); + WindowBounds::Windowed(self.bounds().inset(inset)) + } + } + + fn content_size(&self) -> Size { + self.borrow().bounds.size + } + + fn resize(&mut self, size: Size) { + let state = self.borrow(); + let state_ptr = self.0.clone(); + + // Keep window geometry consistent with configure handling. On Wayland, window geometry is + // surface-local: resizing should not attempt to translate the window; the compositor + // controls placement. We also account for client-side decoration insets and tiling. + let window_geometry = inset_by_tiling( + Bounds { + origin: Point::default(), + size, + }, + state.inset(), + state.tiling, + ) + .map(|v| f32::from(v) as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.surface_state.set_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + state + .globals + .executor + .spawn(async move { state_ptr.resize(size) }) + .detach(); + } + + fn scale_factor(&self) -> f32 { + self.borrow().scale + } + + fn appearance(&self) -> WindowAppearance { + self.borrow().appearance + } + + fn display(&self) -> Option> { + let state = self.borrow(); + state.display.as_ref().map(|(id, display)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: display.name.clone(), + bounds: display.bounds.to_pixels(state.scale), + }) as Rc + }) + } + + fn mouse_position(&self) -> Point { + self.borrow() + .client + .get_client() + .borrow() + .mouse_location + .unwrap_or_default() + } + + fn modifiers(&self) -> Modifiers { + self.borrow().client.get_client().borrow().modifiers + } + + fn capslock(&self) -> Capslock { + self.borrow().client.get_client().borrow().capslock + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + // Try to request an activation token. Even though the activation is likely going to be rejected, + // KWin and Mutter can use the app_id to visually indicate we're requesting attention. + let state = self.borrow(); + if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone()) + { + state.client.set_pending_activation(state.surface.id()); + let token = activation.get_activation_token(&state.globals.qh, ()); + // The serial isn't exactly important here, since the activation is probably going to be rejected anyway. + let serial = state.client.get_serial(SerialKind::MousePress); + token.set_app_id(app_id); + token.set_serial(serial, &state.globals.seat); + token.set_surface(&state.surface); + token.commit(); + } + } + + fn is_active(&self) -> bool { + self.borrow().active + } + + fn is_hovered(&self) -> bool { + self.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_title(title.to_string()); + } + } + + fn set_app_id(&mut self, app_id: &str) { + let mut state = self.borrow_mut(); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.set_app_id(app_id.to_owned()); + } + state.app_id = Some(app_id.to_owned()); + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.borrow_mut(); + state.background_appearance = background_appearance; + update_window(state); + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.borrow().background_appearance + } + + fn is_subpixel_rendering_supported(&self) -> bool { + let client = self.borrow().client.get_client(); + let state = client.borrow(); + state + .gpu_context + .borrow() + .as_ref() + .is_some_and(|ctx| ctx.supports_dual_source_blending()) + } + + fn minimize(&self) { + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_minimized(); + } + } + + fn zoom(&self) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.maximized { + toplevel.set_maximized(); + } else { + toplevel.unset_maximized(); + } + } + } + + fn toggle_fullscreen(&self) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.fullscreen { + toplevel.set_fullscreen(None); + } else { + toplevel.unset_fullscreen(); + } + } + } + + fn is_fullscreen(&self) -> bool { + self.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box gpui::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hover_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn on_button_layout_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut state = self.borrow_mut(); + + if state.renderer.device_lost() { + let raw_window = RawWindow { + window: state.surface.id().as_ptr().cast::(), + display: state + .surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + match state.renderer.recover(&raw_window) { + Ok(()) => {} + Err(err) => { + log::warn!("GPU recovery failed, will retry on next frame: {err}"); + } + } + + state.force_render_after_recovery = true; + return; + } + + state.renderer_presented = state.renderer.draw(scene); + + if state.renderer.needs_redraw() { + state.force_render_after_recovery = true; + } + } + + fn completed_frame(&self) { + let mut state = self.borrow_mut(); + + // Work around a bug in old versions of wlroots where committing without a buffer attached + // can cause invalid synchronization that leads to graphical corruption. + if !state.renderer_presented { + state.surface.commit(); + } + + state.renderer_presented = false; + } + + fn sprite_atlas(&self) -> Arc { + let state = self.borrow(); + state.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.show_window_menu( + &state.globals.seat, + serial, + f32::from(position.x) as i32, + f32::from(position.y) as i32, + ); + } + } + + fn start_window_move(&self) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel._move(&state.globals.seat, serial); + } + } + + fn start_window_resize(&self, edge: gpui::ResizeEdge) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.resize( + &state.globals.seat, + state.client.get_serial(SerialKind::MousePress), + edge.to_xdg(), + ) + } + } + + fn set_input_region(&self, rects: &[Bounds]) { + let state = self.borrow(); + if rects.is_empty() { + state.surface.set_input_region(None); + } else { + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + rects + .iter() + .map(|rect| rect.map(|pixels| f32::from(pixels) as i32)) + .for_each(|rect| { + region.add( + rect.origin.x, + rect.origin.y, + rect.size.width, + rect.size.height, + ) + }); + state.surface.set_input_region(Some(®ion)); + region.destroy(); + } + state.surface.commit(); + } + + fn set_exclusive_zone(&self, zone: Pixels) { + let state = self.borrow(); + state + .surface_state + .set_exclusive_zone(f32::from(zone) as i32); + state.surface.commit(); + } + + fn window_decorations(&self) -> Decorations { + let state = self.borrow(); + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => Decorations::Client { + tiling: state.tiling, + }, + } + } + + fn request_decorations(&self, decorations: WindowDecorations) { + let mut state = self.borrow_mut(); + match state.surface_state.decoration().as_ref() { + Some(decoration) => { + decoration.set_mode(decorations.to_xdg()); + state.decorations = decorations; + update_window(state); + } + None => { + if matches!(decorations, WindowDecorations::Server) { + log::info!( + "Server-side decorations requested, but the Wayland server does not support them. Falling back to client-side decorations." + ); + } + state.decorations = WindowDecorations::Client; + update_window(state); + } + } + } + + fn window_controls(&self) -> WindowControls { + self.borrow().window_controls + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.borrow_mut(); + if Some(inset) != state.client_inset { + state.client_inset = Some(inset); + update_window(state); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let state = self.borrow(); + state.client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.borrow().renderer.gpu_specs().into() + } + + fn gpu_context(&self) -> Option> { + let (device, queue) = self.borrow().renderer.gpu_context(); + Some(Box::new((device, queue))) + } + + fn gpu_device_lost(&self) -> Option { + // Only loads an atomic flag — safe even mid-recovery, when + // `gpu_context` would panic on the torn-down resources. + Some(self.borrow().renderer.device_lost()) + } + + fn play_system_bell(&self) { + let state = self.borrow(); + let surface = if state.surface_state.toplevel().is_some() { + Some(&state.surface) + } else { + None + }; + if let Some(bell) = state.globals.system_bell.as_ref() { + bell.ring(surface); + } + } + + fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { + let activation_handler = TrivialActivationHandler { + callback: callbacks.activation, + }; + let action_handler = TrivialActionHandler(callbacks.action); + let deactivation_handler = TrivialDeactivationHandler { + callback: callbacks.deactivation, + }; + + let adapter = + accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); + + self.borrow_mut().accesskit_adapter = Some(adapter); + } + + fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + let mut state = self.borrow_mut(); + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.update_if_active(|| tree_update); + } + } + + fn a11y_update_window_bounds(&self) { + // Wayland doesn't expose window position, so this is a no-op + } } struct TrivialActivationHandler { - callback: Box Option + Send + 'static>, + callback: Box Option + Send + 'static>, } impl accesskit::ActivationHandler for TrivialActivationHandler { - fn request_initial_tree(&mut self) -> Option { - (self.callback)() - } + fn request_initial_tree(&mut self) -> Option { + (self.callback)() + } } struct TrivialActionHandler(Box); impl accesskit::ActionHandler for TrivialActionHandler { - fn do_action(&mut self, request: accesskit::ActionRequest) { - (self.0)(request); - } + fn do_action(&mut self, request: accesskit::ActionRequest) { + (self.0)(request); + } } struct TrivialDeactivationHandler { - callback: Box, + callback: Box, } impl accesskit::DeactivationHandler for TrivialDeactivationHandler { - fn deactivate_accessibility(&mut self) { - (self.callback)(); - } + fn deactivate_accessibility(&mut self) { + (self.callback)(); + } } fn update_window(mut state: RefMut) { - let opaque = !state.is_transparent(); + let opaque = !state.is_transparent(); - state.renderer.update_transparency(!opaque); - let opaque_area = state.window_bounds.map(|v| f32::from(v) as i32); - opaque_area.inset(f32::from(state.inset()) as i32); + state.renderer.update_transparency(!opaque); + let opaque_area = state.window_bounds.map(|v| f32::from(v) as i32); + opaque_area.inset(f32::from(state.inset()) as i32); - let region = state - .globals - .compositor - .create_region(&state.globals.qh, ()); - region.add( - opaque_area.origin.x, - opaque_area.origin.y, - opaque_area.size.width, - opaque_area.size.height, - ); + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + region.add( + opaque_area.origin.x, + opaque_area.origin.y, + opaque_area.size.width, + opaque_area.size.height, + ); - // Note that rounded corners make this rectangle API hard to work with. - // As this is common when using CSD, let's just disable this API. - if state.background_appearance == WindowBackgroundAppearance::Opaque - && state.decorations == WindowDecorations::Server - { - // Promise the compositor that this region of the window surface - // contains no transparent pixels. This allows the compositor to skip - // updating whatever is behind the surface for better performance. - state.surface.set_opaque_region(Some(®ion)); - } else { - state.surface.set_opaque_region(None); - } + // Note that rounded corners make this rectangle API hard to work with. + // As this is common when using CSD, let's just disable this API. + if state.background_appearance == WindowBackgroundAppearance::Opaque + && state.decorations == WindowDecorations::Server + { + // Promise the compositor that this region of the window surface + // contains no transparent pixels. This allows the compositor to skip + // updating whatever is behind the surface for better performance. + state.surface.set_opaque_region(Some(®ion)); + } else { + state.surface.set_opaque_region(None); + } - if let Some(ref blur_manager) = state.globals.blur_manager { - if state.background_appearance == WindowBackgroundAppearance::Blurred { - if state.blur.is_none() { - let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); - state.blur = Some(blur); - } - state.blur.as_ref().unwrap().commit(); - } else { - // It probably doesn't hurt to clear the blur for opaque windows - blur_manager.unset(&state.surface); - if let Some(b) = state.blur.take() { - b.release() - } - } - } + if let Some(ref blur_manager) = state.globals.blur_manager { + if state.background_appearance == WindowBackgroundAppearance::Blurred { + if state.blur.is_none() { + let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); + state.blur = Some(blur); + } + state.blur.as_ref().unwrap().commit(); + } else { + // It probably doesn't hurt to clear the blur for opaque windows + blur_manager.unset(&state.surface); + if let Some(b) = state.blur.take() { + b.release() + } + } + } - region.destroy(); + region.destroy(); } pub(crate) trait WindowDecorationsExt { - fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode; + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode; } impl WindowDecorationsExt for WindowDecorations { - fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { - match self { - WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, - WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, - } - } + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { + match self { + WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, + WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + } + } } pub(crate) trait ResizeEdgeWaylandExt { - fn to_xdg(self) -> xdg_toplevel::ResizeEdge; + fn to_xdg(self) -> xdg_toplevel::ResizeEdge; } impl ResizeEdgeWaylandExt for ResizeEdge { - fn to_xdg(self) -> xdg_toplevel::ResizeEdge { - match self { - ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, - ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, - ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right, - ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight, - ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom, - ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, - ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left, - ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft, - } - } + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { + match self { + ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, + ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, + ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right, + ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight, + ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom, + ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, + ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left, + ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft, + } + } } /// The configuration event is in terms of the window geometry, which we are constantly /// updating to account for the client decorations. But that's not the area we want to render /// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets fn compute_outer_size( - inset: Pixels, - new_size: Option>, - tiling: Tiling, + inset: Pixels, + new_size: Option>, + tiling: Tiling, ) -> Option> { - new_size.map(|mut new_size| { - if !tiling.top { - new_size.height += inset; - } - if !tiling.bottom { - new_size.height += inset; - } - if !tiling.left { - new_size.width += inset; - } - if !tiling.right { - new_size.width += inset; - } + new_size.map(|mut new_size| { + if !tiling.top { + new_size.height += inset; + } + if !tiling.bottom { + new_size.height += inset; + } + if !tiling.left { + new_size.width += inset; + } + if !tiling.right { + new_size.width += inset; + } - new_size - }) + new_size + }) } fn inset_by_tiling(mut bounds: Bounds, inset: Pixels, tiling: Tiling) -> Bounds { - if !tiling.top { - bounds.origin.y += inset; - bounds.size.height -= inset; - } - if !tiling.bottom { - bounds.size.height -= inset; - } - if !tiling.left { - bounds.origin.x += inset; - bounds.size.width -= inset; - } - if !tiling.right { - bounds.size.width -= inset; - } + if !tiling.top { + bounds.origin.y += inset; + bounds.size.height -= inset; + } + if !tiling.bottom { + bounds.size.height -= inset; + } + if !tiling.left { + bounds.origin.x += inset; + bounds.size.width -= inset; + } + if !tiling.right { + bounds.size.width -= inset; + } - bounds + bounds } diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 96f6cbfc1b..77af84654b 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -1,8 +1,8 @@ use anyhow::{Context as _, anyhow}; use ashpd::WindowIdentifier; use calloop::{ - EventLoop, LoopHandle, RegistrationToken, - generic::{FdWrapper, Generic}, + EventLoop, LoopHandle, RegistrationToken, + generic::{FdWrapper, Generic}, }; use collections::HashMap; use core::str; @@ -10,59 +10,59 @@ use gpui::{Capslock, TaskTiming, profiler}; use log::Level; use smallvec::SmallVec; use std::{ - cell::RefCell, - collections::{BTreeMap, HashSet}, - ops::Deref, - path::PathBuf, - rc::{Rc, Weak}, - time::{Duration, Instant}, + cell::RefCell, + collections::{BTreeMap, HashSet}, + ops::Deref, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, }; use url::Url; use util::ResultExt as _; use x11rb::{ - connection::{Connection, RequestConnection}, - cursor, - errors::ConnectionError, - protocol::randr::ConnectionExt as _, - protocol::xinput::ConnectionExt, - protocol::xkb::ConnectionExt as _, - protocol::xproto::{ - AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, - ConnectionExt as _, EventMask, Visibility, - }, - protocol::{Event, dri3, randr, render, xinput, xkb, xproto}, - resource_manager::Database, - wrapper::ConnectionExt as _, - xcb_ffi::XCBConnection, + connection::{Connection, RequestConnection}, + cursor, + errors::ConnectionError, + protocol::randr::ConnectionExt as _, + protocol::xinput::ConnectionExt, + protocol::xkb::ConnectionExt as _, + protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, + ConnectionExt as _, EventMask, Visibility, + }, + protocol::{Event, dri3, randr, render, xinput, xkb, xproto}, + resource_manager::Database, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, }; use xim::{AttributeName, Client, InputStyle, x11rb::X11rbClient}; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; use xkbcommon::xkb::{self as xkbc, STATE_LAYOUT_EFFECTIVE}; use super::{ - ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent, - XimHandler, button_or_scroll_from_event_detail, check_reply, - clipboard::{self, Clipboard}, - get_reply, get_valuator_axis_index, handle_connection_error, modifiers_from_state, - pressed_button_from_mask, xcb_flush, + ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent, + XimHandler, button_or_scroll_from_event_detail, check_reply, + clipboard::{self, Clipboard}, + get_reply, get_valuator_axis_index, handle_connection_error, modifiers_from_state, + pressed_button_from_mask, xcb_flush, }; use crate::linux::{ - DEFAULT_CURSOR_ICON_NAME, LinuxClient, capslock_from_xkb, cursor_style_to_icon_names, - get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb, - keystroke_underlying_dead_key, log_cursor_icon_warning, modifiers_from_xkb, open_uri_internal, - platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}, - reveal_path_internal, - xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, + DEFAULT_CURSOR_ICON_NAME, LinuxClient, capslock_from_xkb, cursor_style_to_icon_names, + get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb, + keystroke_underlying_dead_key, log_cursor_icon_warning, modifiers_from_xkb, open_uri_internal, + platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}, + reveal_path_internal, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }; use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; use gpui::{ - AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, - PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size, - TouchPhase, WindowButtonLayout, WindowParams, point, px, + AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, + PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size, + TouchPhase, WindowButtonLayout, WindowParams, point, px, }; use gpui_wgpu::{CompositorGpuHint, GpuContext}; @@ -79,3050 +79,3050 @@ pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR"; pub(crate) struct WindowRef { - window: X11WindowStatePtr, - refresh_state: Option, - expose_event_received: bool, - last_visibility: Visibility, - is_mapped: bool, + window: X11WindowStatePtr, + refresh_state: Option, + expose_event_received: bool, + last_visibility: Visibility, + is_mapped: bool, } impl WindowRef { - pub fn handle(&self) -> AnyWindowHandle { - self.window.state.borrow().handle - } + pub fn handle(&self) -> AnyWindowHandle { + self.window.state.borrow().handle + } } impl Deref for WindowRef { - type Target = X11WindowStatePtr; + type Target = X11WindowStatePtr; - fn deref(&self) -> &Self::Target { - &self.window - } + fn deref(&self) -> &Self::Target { + &self.window + } } enum RefreshState { - Hidden { - refresh_rate: Duration, - }, - PeriodicRefresh { - refresh_rate: Duration, - event_loop_token: RegistrationToken, - }, + Hidden { + refresh_rate: Duration, + }, + PeriodicRefresh { + refresh_rate: Duration, + event_loop_token: RegistrationToken, + }, } #[derive(Debug)] #[non_exhaustive] pub enum EventHandlerError { - XCBConnectionError(ConnectionError), - XIMClientError(xim::ClientError), + XCBConnectionError(ConnectionError), + XIMClientError(xim::ClientError), } impl std::error::Error for EventHandlerError {} impl std::fmt::Display for EventHandlerError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EventHandlerError::XCBConnectionError(err) => err.fmt(f), - EventHandlerError::XIMClientError(err) => err.fmt(f), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EventHandlerError::XCBConnectionError(err) => err.fmt(f), + EventHandlerError::XIMClientError(err) => err.fmt(f), + } + } } impl From for EventHandlerError { - fn from(err: ConnectionError) -> Self { - EventHandlerError::XCBConnectionError(err) - } + fn from(err: ConnectionError) -> Self { + EventHandlerError::XCBConnectionError(err) + } } impl From for EventHandlerError { - fn from(err: xim::ClientError) -> Self { - EventHandlerError::XIMClientError(err) - } + fn from(err: xim::ClientError) -> Self { + EventHandlerError::XIMClientError(err) + } } #[derive(Debug, Default)] pub struct Xdnd { - other_window: xproto::Window, - drag_type: u32, - retrieved: bool, - position: Point, + other_window: xproto::Window, + drag_type: u32, + retrieved: bool, + position: Point, } #[derive(Debug)] struct PointerDeviceState { - horizontal: ScrollAxisState, - vertical: ScrollAxisState, + horizontal: ScrollAxisState, + vertical: ScrollAxisState, } #[derive(Debug, Default)] struct ScrollAxisState { - /// Valuator number for looking up this axis's scroll value. - valuator_number: Option, - /// Conversion factor from scroll units to lines. - multiplier: f32, - /// Last scroll value for calculating scroll delta. - /// - /// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes. - /// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event. - /// The consequence of not invalidating it can be large invalid deltas, which are much more user visible. - scroll_value: Option, + /// Valuator number for looking up this axis's scroll value. + valuator_number: Option, + /// Conversion factor from scroll units to lines. + multiplier: f32, + /// Last scroll value for calculating scroll delta. + /// + /// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes. + /// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event. + /// The consequence of not invalidating it can be large invalid deltas, which are much more user visible. + scroll_value: Option, } pub struct X11ClientState { - pub(crate) loop_handle: LoopHandle<'static, X11Client>, - pub(crate) event_loop: Option>, + pub(crate) loop_handle: LoopHandle<'static, X11Client>, + pub(crate) event_loop: Option>, - pub(crate) last_click: Instant, - pub(crate) last_mouse_button: Option, - pub(crate) last_location: Point, - pub(crate) current_count: usize, - pub(crate) pinch_scale: f32, + pub(crate) last_click: Instant, + pub(crate) last_mouse_button: Option, + pub(crate) last_location: Point, + pub(crate) current_count: usize, + pub(crate) pinch_scale: f32, - pub(crate) gpu_context: GpuContext, - pub(crate) compositor_gpu: Option, - pub(crate) gpu_requirements: Option, + pub(crate) gpu_context: GpuContext, + pub(crate) compositor_gpu: Option, + pub(crate) gpu_requirements: Option, - pub(crate) scale_factor: f32, + pub(crate) scale_factor: f32, - xkb_context: xkbc::Context, - pub(crate) xcb_connection: Rc, - xkb_device_id: i32, - client_side_decorations_supported: bool, - pub(crate) x_root_index: usize, - pub(crate) resource_database: Database, - pub(crate) atoms: XcbAtoms, - pub(crate) windows: HashMap, - pub(crate) mouse_focused_window: Option, - pub(crate) keyboard_focused_window: Option, - pub(crate) xkb: xkbc::State, - keyboard_layout: LinuxKeyboardLayout, - pub(crate) ximc: Option>>, - pub(crate) xim_handler: Option, - pub modifiers: Modifiers, - pub capslock: Capslock, - // TODO: Can the other updates to `modifiers` be removed so that this is unnecessary? - // capslock logic was done analog to modifiers - pub last_modifiers_changed_event: Modifiers, - pub last_capslock_changed_event: Capslock, + xkb_context: xkbc::Context, + pub(crate) xcb_connection: Rc, + xkb_device_id: i32, + client_side_decorations_supported: bool, + pub(crate) x_root_index: usize, + pub(crate) resource_database: Database, + pub(crate) atoms: XcbAtoms, + pub(crate) windows: HashMap, + pub(crate) mouse_focused_window: Option, + pub(crate) keyboard_focused_window: Option, + pub(crate) xkb: xkbc::State, + keyboard_layout: LinuxKeyboardLayout, + pub(crate) ximc: Option>>, + pub(crate) xim_handler: Option, + pub modifiers: Modifiers, + pub capslock: Capslock, + // TODO: Can the other updates to `modifiers` be removed so that this is unnecessary? + // capslock logic was done analog to modifiers + pub last_modifiers_changed_event: Modifiers, + pub last_capslock_changed_event: Capslock, - pub(crate) compose_state: Option, - pub(crate) pre_edit_text: Option, - pub(crate) composing: bool, - pub(crate) pre_key_char_down: Option, - pub(crate) cursor_handle: cursor::Handle, - pub(crate) cursor_styles: HashMap, - pub(crate) cursor_cache: HashMap>, - pub(crate) invisible_cursor_cache: Option, - pub(crate) cursor_hidden_window: Option, + pub(crate) compose_state: Option, + pub(crate) pre_edit_text: Option, + pub(crate) composing: bool, + pub(crate) pre_key_char_down: Option, + pub(crate) cursor_handle: cursor::Handle, + pub(crate) cursor_styles: HashMap, + pub(crate) cursor_cache: HashMap>, + pub(crate) invisible_cursor_cache: Option, + pub(crate) cursor_hidden_window: Option, - pointer_device_states: BTreeMap, + pointer_device_states: BTreeMap, - pub(crate) supports_xinput_gestures: bool, + pub(crate) supports_xinput_gestures: bool, - pub(crate) common: LinuxCommon, - pub(crate) clipboard: Clipboard, - pub(crate) clipboard_item: Option, - pub(crate) xdnd_state: Xdnd, + pub(crate) common: LinuxCommon, + pub(crate) clipboard: Clipboard, + pub(crate) clipboard_item: Option, + pub(crate) xdnd_state: Xdnd, } #[derive(Clone)] pub struct X11ClientStatePtr(pub Weak>); impl X11ClientStatePtr { - pub fn get_client(&self) -> Option { - self.0.upgrade().map(X11Client) - } + pub fn get_client(&self) -> Option { + self.0.upgrade().map(X11Client) + } - pub fn drop_window(&self, x_window: u32) { - let Some(client) = self.get_client() else { - return; - }; - let mut state = client.0.borrow_mut(); + pub fn drop_window(&self, x_window: u32) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); - if let Some(window_ref) = state.windows.remove(&x_window) - && let Some(RefreshState::PeriodicRefresh { - event_loop_token, .. - }) = window_ref.refresh_state - { - state.loop_handle.remove(event_loop_token); - } - if state.mouse_focused_window == Some(x_window) { - state.mouse_focused_window = None; - } - if state.keyboard_focused_window == Some(x_window) { - state.keyboard_focused_window = None; - } - if state.cursor_hidden_window == Some(x_window) { - state.cursor_hidden_window = None; - } - state.cursor_styles.remove(&x_window); - } + if let Some(window_ref) = state.windows.remove(&x_window) + && let Some(RefreshState::PeriodicRefresh { + event_loop_token, .. + }) = window_ref.refresh_state + { + state.loop_handle.remove(event_loop_token); + } + if state.mouse_focused_window == Some(x_window) { + state.mouse_focused_window = None; + } + if state.keyboard_focused_window == Some(x_window) { + state.keyboard_focused_window = None; + } + if state.cursor_hidden_window == Some(x_window) { + state.cursor_hidden_window = None; + } + state.cursor_styles.remove(&x_window); + } - pub fn update_ime_position(&self, bounds: Bounds) { - let Some(client) = self.get_client() else { - return; - }; - let mut state = client.0.borrow_mut(); - if state.composing || state.ximc.is_none() { - return; - } + pub fn update_ime_position(&self, bounds: Bounds) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); + if state.composing || state.ximc.is_none() { + return; + } - let Some(mut ximc) = state.ximc.take() else { - log::error!("bug: xim connection not set"); - return; - }; - let Some(xim_handler) = state.xim_handler.take() else { - log::error!("bug: xim handler not set"); - state.ximc = Some(ximc); - return; - }; - let scaled_bounds = bounds.scale(state.scale_factor); - let ic_attributes = ximc - .build_ic_attributes() - .push( - xim::AttributeName::InputStyle, - xim::InputStyle::PREEDIT_CALLBACKS, - ) - .push(xim::AttributeName::ClientWindow, xim_handler.window) - .push(xim::AttributeName::FocusWindow, xim_handler.window) - .nested_list(xim::AttributeName::PreeditAttributes, |b| { - b.push( - xim::AttributeName::SpotLocation, - xim::Point { - x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16, - y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16, - }, - ); - }) - .build(); - let _ = ximc - .set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) - .log_err(); - state.ximc = Some(ximc); - state.xim_handler = Some(xim_handler); - } + let Some(mut ximc) = state.ximc.take() else { + log::error!("bug: xim connection not set"); + return; + }; + let Some(xim_handler) = state.xim_handler.take() else { + log::error!("bug: xim handler not set"); + state.ximc = Some(ximc); + return; + }; + let scaled_bounds = bounds.scale(state.scale_factor); + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16, + y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16, + }, + ); + }) + .build(); + let _ = ximc + .set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .log_err(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + } } #[derive(Clone)] pub(crate) struct X11Client(pub(crate) Rc>); impl X11Client { - pub(crate) fn new() -> anyhow::Result { - let event_loop = EventLoop::try_new()?; - - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); - - let handle = event_loop.handle(); - - handle - .insert_source(main_receiver, { - let handle = handle.clone(); - move |event, _, _: &mut X11Client| { - if let calloop::channel::Event::Msg(runnable) = event { - // Insert the runnables as idle callbacks, so we make sure that user-input and X11 - // events have higher priority and runnables are only worked off after the event - // callbacks. - handle.insert_idle(|_| { - let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - profiler::add_task_timing(timing); - - runnable.run(); - - let end = Instant::now(); - timing.end = Some(end); - profiler::add_task_timing(timing); - }); - } - } - }) - .map_err(|err| { - anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}") - })?; - - let (xcb_connection, x_root_index) = XCBConnection::connect(None)?; - xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?; - xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?; - xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?; - xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?; - - // Announce to X server that XInput up to 2.4 is supported. - // Version 2.4 is needed for gesture events (GesturePinchBegin/Update/End). - // The server responds with the highest version it supports; if < 2.4, - // we must not request gesture event masks in XISelectEvents. - let xinput_version = get_reply( - || "XInput XiQueryVersion failed", - xcb_connection.xinput_xi_query_version(2, 4), - )?; - assert!( - xinput_version.major_version >= 2, - "XInput version >= 2 required." - ); - let supports_xinput_gestures = xinput_version.major_version > 2 - || (xinput_version.major_version == 2 && xinput_version.minor_version >= 4); - log::info!( - "XInput version: {}.{}, gesture support: {}", - xinput_version.major_version, - xinput_version.minor_version, - supports_xinput_gestures, - ); - - let pointer_device_states = - current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default(); - - let atoms = XcbAtoms::new(&xcb_connection) - .context("Failed to get XCB atoms")? - .reply() - .context("Failed to get XCB atoms")?; - - let root = xcb_connection.setup().roots[0].root; - let compositor_present = check_compositor_present(&xcb_connection, root); - let gtk_frame_extents_supported = - check_gtk_frame_extents_supported(&xcb_connection, &atoms, root); - let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported; - log::info!( - "x11: compositor present: {}, gtk_frame_extents_supported: {}", - compositor_present, - gtk_frame_extents_supported - ); - - let xkb = get_reply( - || "Failed to initialize XKB extension", - xcb_connection - .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION), - )?; - assert!(xkb.supported); - - let events = xkb::EventType::STATE_NOTIFY - | xkb::EventType::MAP_NOTIFY - | xkb::EventType::NEW_KEYBOARD_NOTIFY; - let map_notify_parts = xkb::MapPart::KEY_TYPES - | xkb::MapPart::KEY_SYMS - | xkb::MapPart::MODIFIER_MAP - | xkb::MapPart::EXPLICIT_COMPONENTS - | xkb::MapPart::KEY_ACTIONS - | xkb::MapPart::KEY_BEHAVIORS - | xkb::MapPart::VIRTUAL_MODS - | xkb::MapPart::VIRTUAL_MOD_MAP; - check_reply( - || "Failed to select XKB events", - xcb_connection.xkb_select_events( - xkb::ID::USE_CORE_KBD.into(), - 0u8.into(), - events, - map_notify_parts, - map_notify_parts, - &xkb::SelectEventsAux::new(), - ), - )?; - - let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); - let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection); - let xkb_state = { - let xkb_keymap = xkbc::x11::keymap_new_from_device( - &xkb_context, - &xcb_connection, - xkb_device_id, - xkbc::KEYMAP_COMPILE_NO_FLAGS, - ); - xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) - }; - let compose_state = get_xkb_compose_state(&xkb_context); - let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE); - let layout_name = xkb_state - .get_keymap() - .layout_get_name(layout_idx) - .to_string(); - let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into()); - - let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) - .context("Failed to create resource database")?; - let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); - let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) - .context("Failed to initialize cursor theme handler")? - .reply() - .context("Failed to initialize cursor theme handler")?; - - let clipboard = Clipboard::new().context("Failed to initialize clipboard")?; - - let screen = &xcb_connection.setup().roots[x_root_index]; - let compositor_gpu = detect_compositor_gpu(&xcb_connection, screen); - - let xcb_connection = Rc::new(xcb_connection); - - let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok(); - let xim_handler = if ximc.is_some() { - Some(XimHandler::new()) - } else { - None - }; - - // Safety: Safe if xcb::Connection always returns a valid fd - let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; - - handle - .insert_source( - Generic::new_with_error::( - fd, - calloop::Interest::READ, - calloop::Mode::Level, - ), - { - let xcb_connection = xcb_connection.clone(); - move |_readiness, _, client| { - client.process_x11_events(&xcb_connection)?; - Ok(calloop::PostAction::Continue) - } - }, - ) - .map_err(|err| anyhow!("Failed to initialize X11 event source: {err:?}"))?; - - handle - .insert_source(XDPEventSource::new(&common.background_executor), { - move |event, _, client| match event { - XDPEvent::WindowAppearance(appearance) => { - client.with_common(|common| common.appearance = appearance); - for window in client.0.borrow_mut().windows.values_mut() { - window.window.set_appearance(appearance); - } - } - XDPEvent::ButtonLayout(layout_str) => { - let layout = WindowButtonLayout::parse(&layout_str) - .log_err() - .unwrap_or_else(WindowButtonLayout::linux_default); - client.with_common(|common| common.button_layout = layout); - for window in client.0.borrow_mut().windows.values_mut() { - window.window.set_button_layout(); - } - } - XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { - // noop, X11 manages this for us. - } - } - }) - .map_err(|err| anyhow!("Failed to initialize XDP event source: {err:?}"))?; - - xcb_flush(&xcb_connection); - - Ok(X11Client(Rc::new(RefCell::new(X11ClientState { - modifiers: Modifiers::default(), - capslock: Capslock::default(), - last_modifiers_changed_event: Modifiers::default(), - last_capslock_changed_event: Capslock::default(), - event_loop: Some(event_loop), - loop_handle: handle, - common, - last_click: Instant::now(), - last_mouse_button: None, - last_location: Point::new(px(0.0), px(0.0)), - current_count: 0, - pinch_scale: 1.0, - gpu_context: Rc::new(RefCell::new(None)), - compositor_gpu, - gpu_requirements: None, - scale_factor, - - xkb_context, - xcb_connection, - xkb_device_id, - client_side_decorations_supported, - x_root_index, - resource_database, - atoms, - windows: HashMap::default(), - mouse_focused_window: None, - keyboard_focused_window: None, - xkb: xkb_state, - keyboard_layout, - ximc, - xim_handler, - - compose_state, - pre_edit_text: None, - pre_key_char_down: None, - composing: false, - - cursor_handle, - cursor_styles: HashMap::default(), - cursor_cache: HashMap::default(), - cursor_hidden_window: None, - invisible_cursor_cache: None, - - pointer_device_states, - - supports_xinput_gestures, - - clipboard, - clipboard_item: None, - xdnd_state: Xdnd::default(), - })))) - } - - pub fn process_x11_events( - &self, - xcb_connection: &XCBConnection, - ) -> Result<(), EventHandlerError> { - loop { - let mut events = Vec::new(); - let mut windows_to_refresh = HashSet::new(); - - let mut last_key_release = None; - - // event handlers for new keyboard / remapping refresh the state without using event - // details, this deduplicates them. - let mut last_keymap_change_event: Option = None; - - loop { - match xcb_connection.poll_for_event() { - Ok(Some(event)) => { - match event { - Event::Expose(expose_event) => { - windows_to_refresh.insert(expose_event.window); - } - Event::KeyRelease(_) => { - if let Some(last_keymap_change_event) = - last_keymap_change_event.take() - { - if let Some(last_key_release) = last_key_release.take() { - events.push(last_key_release); - } - events.push(last_keymap_change_event); - } - - last_key_release = Some(event); - } - Event::KeyPress(key_press) => { - if let Some(last_keymap_change_event) = - last_keymap_change_event.take() - { - if let Some(last_key_release) = last_key_release.take() { - events.push(last_key_release); - } - events.push(last_keymap_change_event); - } - - if let Some(Event::KeyRelease(key_release)) = - last_key_release.take() - { - // We ignore that last KeyRelease if it's too close to this KeyPress, - // suggesting that it's auto-generated by X11 as a key-repeat event. - if key_release.detail != key_press.detail - || key_press.time.saturating_sub(key_release.time) > 20 - { - events.push(Event::KeyRelease(key_release)); - } - } - events.push(Event::KeyPress(key_press)); - } - Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { - if let Some(release_event) = last_key_release.take() { - events.push(release_event); - } - last_keymap_change_event = Some(event); - } - _ => { - if let Some(release_event) = last_key_release.take() { - events.push(release_event); - } - events.push(event); - } - } - } - Ok(None) => { - break; - } - Err(err @ ConnectionError::IoError(..)) => { - return Err(EventHandlerError::from(err)); - } - Err(err) => { - let err = handle_connection_error(err); - log::warn!("error while polling for X11 events: {err:?}"); - break; - } - } - } - - if let Some(release_event) = last_key_release.take() { - events.push(release_event); - } - if let Some(keymap_change_event) = last_keymap_change_event.take() { - events.push(keymap_change_event); - } - - if events.is_empty() && windows_to_refresh.is_empty() { - break; - } - - for window in windows_to_refresh.into_iter() { - let mut state = self.0.borrow_mut(); - if let Some(window) = state.windows.get_mut(&window) { - window.expose_event_received = true; - } - } - - for event in events.into_iter() { - let mut state = self.0.borrow_mut(); - if !state.has_xim() { - drop(state); - self.handle_event(event); - continue; - } - - let Some((mut ximc, mut xim_handler)) = state.take_xim() else { - continue; - }; - let xim_connected = xim_handler.connected; - drop(state); - - let xim_filtered = ximc.filter_event(&event, &mut xim_handler); - let xim_callback_event = xim_handler.last_callback_event.take(); - - let mut state = self.0.borrow_mut(); - state.restore_xim(ximc, xim_handler); - drop(state); - - if let Some(event) = xim_callback_event { - self.handle_xim_callback_event(event); - } - - match xim_filtered { - Ok(handled) => { - if handled { - continue; - } - if xim_connected { - self.xim_handle_event(event); - } else { - self.handle_event(event); - } - } - Err(err) => { - // this might happen when xim server crashes on one of the events - // we do lose 1-2 keys when crash happens since there is no reliable way to get that info - // luckily, x11 sends us window not found error when xim server crashes upon further key press - // hence we fall back to handle_event - log::error!("XIMClientError: {}", err); - let mut state = self.0.borrow_mut(); - state.take_xim(); - drop(state); - self.handle_event(event); - } - } - } - } - Ok(()) - } - - pub fn enable_ime(&self) { - let mut state = self.0.borrow_mut(); - if !state.has_xim() { - return; - } - - let Some((mut ximc, xim_handler)) = state.take_xim() else { - return; - }; - let mut ic_attributes = ximc - .build_ic_attributes() - .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) - .push(AttributeName::ClientWindow, xim_handler.window) - .push(AttributeName::FocusWindow, xim_handler.window); - - let window_id = state.keyboard_focused_window; - drop(state); - if let Some(window_id) = window_id { - let Some(window) = self.get_window(window_id) else { - log::error!("Failed to get window for IME positioning"); - let mut state = self.0.borrow_mut(); - state.ximc = Some(ximc); - state.xim_handler = Some(xim_handler); - return; - }; - if let Some(scaled_area) = window.get_ime_area() { - ic_attributes = - ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { - b.push( - xim::AttributeName::SpotLocation, - xim::Point { - x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, - y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, - }, - ); - }); - } - } - ximc.create_ic(xim_handler.im_id, ic_attributes.build()) - .ok(); - let mut state = self.0.borrow_mut(); - state.restore_xim(ximc, xim_handler); - } - - pub fn reset_ime(&self) { - let mut state = self.0.borrow_mut(); - state.composing = false; - if let Some(mut ximc) = state.ximc.take() { - if let Some(xim_handler) = state.xim_handler.as_ref() { - ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok(); - } else { - log::error!("bug: xim handler not set in reset_ime"); - } - state.ximc = Some(ximc); - } - } - - pub(crate) fn get_window(&self, win: xproto::Window) -> Option { - let state = self.0.borrow(); - state - .windows - .get(&win) - .filter(|window_reference| !window_reference.window.state.borrow().destroyed) - .map(|window_reference| window_reference.window.clone()) - } - - fn handle_event(&self, event: Event) -> Option<()> { - match event { - Event::UnmapNotify(event) => { - let mut state = self.0.borrow_mut(); - if let Some(window_ref) = state.windows.get_mut(&event.window) { - window_ref.is_mapped = false; - } - state.update_refresh_loop(event.window); - } - Event::MapNotify(event) => { - let mut state = self.0.borrow_mut(); - if let Some(window_ref) = state.windows.get_mut(&event.window) { - window_ref.is_mapped = true; - } - state.update_refresh_loop(event.window); - } - Event::VisibilityNotify(event) => { - let mut state = self.0.borrow_mut(); - if let Some(window_ref) = state.windows.get_mut(&event.window) { - window_ref.last_visibility = event.state; - } - state.update_refresh_loop(event.window); - } - Event::ClientMessage(event) => { - let window = self.get_window(event.window)?; - let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); - let mut state = self.0.borrow_mut(); - - if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() { - // window "x" button clicked by user - // Rest of the close logic is handled in drop_window() - drop(state); - window.close(); - state = self.0.borrow_mut(); - } else if atom == state.atoms._NET_WM_SYNC_REQUEST { - window.state.borrow_mut().last_sync_counter = - Some(x11rb::protocol::sync::Int64 { - lo: arg2, - hi: arg3 as i32, - }) - } - - if event.type_ == state.atoms.XdndEnter { - state.xdnd_state.other_window = atom; - if (arg1 & 0x1) == 0x1 { - state.xdnd_state.drag_type = xdnd_get_supported_atom( - &state.xcb_connection, - &state.atoms, - state.xdnd_state.other_window, - ); - } else { - if let Some(atom) = [arg2, arg3, arg4] - .into_iter() - .find(|atom| xdnd_is_atom_supported(*atom, &state.atoms)) - { - state.xdnd_state.drag_type = atom; - } - } - } else if event.type_ == state.atoms.XdndLeave { - let position = state.xdnd_state.position; - drop(state); - window - .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); - window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {})); - self.0.borrow_mut().xdnd_state = Xdnd::default(); - } else if event.type_ == state.atoms.XdndPosition { - if let Ok(pos) = get_reply( - || "Failed to query pointer position", - state.xcb_connection.query_pointer(event.window), - ) { - state.xdnd_state.position = - Point::new(px(pos.win_x as f32), px(pos.win_y as f32)); - } - if !state.xdnd_state.retrieved { - check_reply( - || "Failed to convert selection for drag and drop", - state.xcb_connection.convert_selection( - event.window, - state.atoms.XdndSelection, - state.xdnd_state.drag_type, - state.atoms.XDND_DATA, - arg3, - ), - ) - .log_err(); - } - xdnd_send_status( - &state.xcb_connection, - &state.atoms, - event.window, - state.xdnd_state.other_window, - arg4, - ); - let position = state.xdnd_state.position; - drop(state); - window - .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); - } else if event.type_ == state.atoms.XdndDrop { - xdnd_send_finished( - &state.xcb_connection, - &state.atoms, - event.window, - state.xdnd_state.other_window, - ); - let position = state.xdnd_state.position; - drop(state); - window - .handle_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); - self.0.borrow_mut().xdnd_state = Xdnd::default(); - } - } - Event::SelectionNotify(event) => { - let window = self.get_window(event.requestor)?; - let state = self.0.borrow_mut(); - let reply = get_reply( - || "Failed to get XDND_DATA", - state.xcb_connection.get_property( - false, - event.requestor, - state.atoms.XDND_DATA, - AtomEnum::ANY, - 0, - 1024, - ), - ) - .log_err(); - let Some(reply) = reply else { - return Some(()); - }; - if let Ok(file_list) = str::from_utf8(&reply.value) { - let paths: SmallVec<[_; 2]> = file_list - .lines() - .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| match url.to_file_path() { - Ok(url) => Some(url), - Err(()) => { - log::error!("Failed turn {url:?} into a file path"); - None - } - }) - .collect(); - let input = PlatformInput::FileDrop(FileDropEvent::Entered { - position: state.xdnd_state.position, - paths: gpui::ExternalPaths(paths), - }); - drop(state); - window.handle_input(input); - self.0.borrow_mut().xdnd_state.retrieved = true; - } - } - Event::ConfigureNotify(event) => { - let bounds = Bounds { - origin: Point { - x: event.x.into(), - y: event.y.into(), - }, - size: Size { - width: event.width.into(), - height: event.height.into(), - }, - }; - let window = self.get_window(event.window)?; - window - .set_bounds(bounds) - .context("X11: Failed to set window bounds") - .log_err(); - } - Event::PropertyNotify(event) => { - let window = self.get_window(event.window)?; - window - .property_notify(event) - .context("X11: Failed to handle property notify") - .log_err(); - } - Event::FocusIn(event) => { - let window = self.get_window(event.event)?; - window.set_active(true); - let mut state = self.0.borrow_mut(); - state.keyboard_focused_window = Some(event.event); - if let Some(handler) = state.xim_handler.as_mut() { - handler.window = event.event; - } - drop(state); - self.enable_ime(); - } - Event::FocusOut(event) => { - let window = self.get_window(event.event)?; - window.set_active(false); - let mut state = self.0.borrow_mut(); - // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) - reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); - state.keyboard_focused_window = None; - if let Some(compose_state) = state.compose_state.as_mut() { - compose_state.reset(); - } - state.pre_edit_text.take(); - state.restore_cursor_after_hide(); - drop(state); - self.reset_ime(); - window.handle_ime_delete(); - } - Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { - let mut state = self.0.borrow_mut(); - let xkb_state = { - let xkb_keymap = xkbc::x11::keymap_new_from_device( - &state.xkb_context, - &state.xcb_connection, - state.xkb_device_id, - xkbc::KEYMAP_COMPILE_NO_FLAGS, - ); - xkbc::x11::state_new_from_device( - &xkb_keymap, - &state.xcb_connection, - state.xkb_device_id, - ) - }; - state.xkb = xkb_state; - drop(state); - self.handle_keyboard_layout_change(); - } - Event::XkbStateNotify(event) => { - let mut state = self.0.borrow_mut(); - let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); - let new_layout = u32::from(event.group); - state.xkb.update_mask( - event.base_mods.into(), - event.latched_mods.into(), - event.locked_mods.into(), - event.base_group as u32, - event.latched_group as u32, - event.locked_group.into(), - ); - let modifiers = modifiers_from_xkb(&state.xkb); - let capslock = capslock_from_xkb(&state.xkb); - if state.last_modifiers_changed_event == modifiers - && state.last_capslock_changed_event == capslock - { - drop(state); - } else { - let focused_window_id = state.keyboard_focused_window?; - state.modifiers = modifiers; - state.last_modifiers_changed_event = modifiers; - state.capslock = capslock; - state.last_capslock_changed_event = capslock; - drop(state); - - let focused_window = self.get_window(focused_window_id)?; - focused_window.handle_input(PlatformInput::ModifiersChanged( - ModifiersChangedEvent { - modifiers, - capslock, - }, - )); - } - - if new_layout != old_layout { - self.handle_keyboard_layout_change(); - } - } - Event::KeyPress(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - - let modifiers = modifiers_from_state(event.state); - state.modifiers = modifiers; - state.pre_key_char_down.take(); - let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); - - let keystroke = { - let code = event.detail.into(); - let mut keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); - let keysym = key_event_state.key_get_one_sym(code); - - if keysym.is_modifier_key() { - return Some(()); - } - - if let Some(mut compose_state) = state.compose_state.take() { - compose_state.feed(keysym); - match compose_state.status() { - xkbc::Status::Composed => { - state.pre_edit_text.take(); - keystroke.key_char = compose_state.utf8(); - if let Some(keysym) = compose_state.keysym() { - keystroke.key = xkbc::keysym_get_name(keysym); - } - } - xkbc::Status::Composing => { - keystroke.key_char = None; - state.pre_edit_text = compose_state - .utf8() - .or(keystroke_underlying_dead_key(keysym)); - let pre_edit = - state.pre_edit_text.clone().unwrap_or(String::default()); - drop(state); - window.handle_ime_preedit(pre_edit); - state = self.0.borrow_mut(); - } - xkbc::Status::Cancelled => { - let pre_edit = state.pre_edit_text.take(); - drop(state); - if let Some(pre_edit) = pre_edit { - window.handle_ime_commit(pre_edit); - } - if let Some(current_key) = keystroke_underlying_dead_key(keysym) { - window.handle_ime_preedit(current_key); - } - state = self.0.borrow_mut(); - compose_state.feed(keysym); - } - _ => {} - } - state.compose_state = Some(compose_state); - } - keystroke - }; - drop(state); - window.handle_input(PlatformInput::KeyDown(gpui::KeyDownEvent { - keystroke, - is_held: false, - prefer_character_input: false, - })); - } - Event::KeyRelease(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - - let modifiers = modifiers_from_state(event.state); - state.modifiers = modifiers; - let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); - - let keystroke = { - let code = event.detail.into(); - let keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); - let keysym = key_event_state.key_get_one_sym(code); - - if keysym.is_modifier_key() { - return Some(()); - } - - keystroke - }; - drop(state); - window.handle_input(PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke })); - } - Event::XinputButtonPress(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - - if state.composing && state.ximc.is_some() { - drop(state); - self.reset_ime(); - window.handle_ime_unmark(); - state = self.0.borrow_mut(); - } else if let Some(text) = state.pre_edit_text.take() { - if let Some(compose_state) = state.compose_state.as_mut() { - compose_state.reset(); - } - drop(state); - window.handle_ime_commit(text); - state = self.0.borrow_mut(); - } - match button_or_scroll_from_event_detail(event.detail) { - Some(ButtonOrScroll::Button(button)) => { - let click_elapsed = state.last_click.elapsed(); - if click_elapsed < DOUBLE_CLICK_INTERVAL - && state - .last_mouse_button - .is_some_and(|prev_button| prev_button == button) - && is_within_click_distance(state.last_location, position) - { - state.current_count += 1; - } else { - state.current_count = 1; - } - - state.last_click = Instant::now(); - state.last_mouse_button = Some(button); - state.last_location = position; - let current_count = state.current_count; - - drop(state); - window.handle_input(PlatformInput::MouseDown(gpui::MouseDownEvent { - button, - position, - modifiers, - click_count: current_count, - first_mouse: false, - })); - } - Some(ButtonOrScroll::Scroll(direction)) => { - drop(state); - // Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events. - // Since handling those events does the scrolling, they are skipped here. - if !event - .flags - .contains(xinput::PointerEventFlags::POINTER_EMULATED) - { - let scroll_delta = match direction { - ScrollDirection::Up => Point::new(0.0, SCROLL_LINES), - ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES), - ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0), - ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0), - }; - window.handle_input(PlatformInput::ScrollWheel( - make_scroll_wheel_event(position, scroll_delta, modifiers), - )); - } - } - None => { - log::error!("Unknown x11 button: {}", event.detail); - } - } - } - Event::XinputButtonRelease(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - match button_or_scroll_from_event_detail(event.detail) { - Some(ButtonOrScroll::Button(button)) => { - let click_count = state.current_count; - drop(state); - window.handle_input(PlatformInput::MouseUp(gpui::MouseUpEvent { - button, - position, - modifiers, - click_count, - })); - } - Some(ButtonOrScroll::Scroll(_)) => {} - None => {} - } - } - Event::XinputMotion(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - state.restore_cursor_after_hide(); - if window.is_blocked() { - // We want to set the cursor to the default arrow - // when the window is blocked - let style = CursorStyle::Arrow; - - let current_style = state - .cursor_styles - .get(&window.x_window) - .unwrap_or(&CursorStyle::Arrow); - if *current_style != style - && let Some(cursor) = state.get_cursor_icon(style) - { - state.cursor_styles.insert(window.x_window, style); - check_reply( - || "Failed to set cursor style", - state.xcb_connection.change_window_attributes( - window.x_window, - &ChangeWindowAttributesAux { - cursor: Some(cursor), - ..Default::default() - }, - ), - ) - .log_err(); - state.xcb_connection.flush().log_err(); - }; - } - let pressed_button = pressed_button_from_mask(event.button_mask[0]); - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - drop(state); - - if event.valuator_mask[0] & 3 != 0 { - window.handle_input(PlatformInput::MouseMove(gpui::MouseMoveEvent { - position, - pressed_button, - modifiers, - })); - } - - state = self.0.borrow_mut(); - if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); - drop(state); - if let Some(scroll_delta) = scroll_delta { - window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( - position, - scroll_delta, - modifiers, - ))); - } - } - } - Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => { - let window = self.get_window(event.event)?; - window.set_hovered(true); - let mut state = self.0.borrow_mut(); - state.mouse_focused_window = Some(event.event); - state.restore_cursor_after_hide(); - } - Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => { - let mut state = self.0.borrow_mut(); - - // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) - reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); - state.mouse_focused_window = None; - let pressed_button = pressed_button_from_mask(event.buttons[0]); - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - drop(state); - - let window = self.get_window(event.event)?; - window.handle_input(PlatformInput::MouseExited(gpui::MouseExitEvent { - pressed_button, - position, - modifiers, - })); - window.set_hovered(false); - } - Event::XinputHierarchy(event) => { - let mut state = self.0.borrow_mut(); - // Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values. - // Any change to a device invalidates its scroll values. - for info in event.infos { - if is_pointer_device(info.type_) { - state.pointer_device_states.remove(&info.deviceid); - } - } - if let Some(pointer_device_states) = current_pointer_device_states( - &state.xcb_connection, - &state.pointer_device_states, - ) { - state.pointer_device_states = pointer_device_states; - } - } - Event::XinputDeviceChanged(event) => { - let mut state = self.0.borrow_mut(); - if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - reset_pointer_device_scroll_positions(pointer); - } - } - Event::XinputGesturePinchBegin(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - state.pinch_scale = 1.0; - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - drop(state); - window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { - position, - delta: 0.0, - modifiers, - phase: gpui::TouchPhase::Started, - })); - } - Event::XinputGesturePinchUpdate(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - // scale is in FP16.16 format: divide by 65536 to get the float value - let new_absolute_scale = event.scale as f32 / 65536.0; - let previous_scale = state.pinch_scale; - let zoom_delta = new_absolute_scale - previous_scale; - state.pinch_scale = new_absolute_scale; - drop(state); - window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { - position, - delta: zoom_delta, - modifiers, - phase: gpui::TouchPhase::Moved, - })); - } - Event::XinputGesturePinchEnd(event) => { - let window = self.get_window(event.event)?; - let mut state = self.0.borrow_mut(); - state.pinch_scale = 1.0; - let modifiers = modifiers_from_xinput_info(event.mods); - state.modifiers = modifiers; - let position = point( - px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), - px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), - ); - drop(state); - window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { - position, - delta: 0.0, - modifiers, - phase: gpui::TouchPhase::Ended, - })); - } - _ => {} - }; - - Some(()) - } - - fn handle_xim_callback_event(&self, event: XimCallbackEvent) { - match event { - XimCallbackEvent::XimXEvent(event) => { - self.handle_event(event); - } - XimCallbackEvent::XimCommitEvent(window, text) => { - self.xim_handle_commit(window, text); - } - XimCallbackEvent::XimPreeditEvent(window, text) => { - self.xim_handle_preedit(window, text); - } - }; - } - - fn xim_handle_event(&self, event: Event) -> Option<()> { - match event { - Event::KeyPress(event) | Event::KeyRelease(event) => { - let mut state = self.0.borrow_mut(); - state.pre_key_char_down = Some(keystroke_from_xkb( - &state.xkb, - state.modifiers, - event.detail.into(), - )); - let (mut ximc, mut xim_handler) = state.take_xim()?; - drop(state); - xim_handler.window = event.event; - ximc.forward_event( - xim_handler.im_id, - xim_handler.ic_id, - xim::ForwardEventFlag::empty(), - &event, - ) - .context("X11: Failed to forward XIM event") - .log_err(); - let mut state = self.0.borrow_mut(); - state.restore_xim(ximc, xim_handler); - drop(state); - } - event => { - self.handle_event(event); - } - } - Some(()) - } - - fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { - let Some(window) = self.get_window(window) else { - log::error!("bug: Failed to get window for XIM commit"); - return None; - }; - let mut state = self.0.borrow_mut(); - state.composing = false; - drop(state); - window.handle_ime_commit(text); - Some(()) - } - - fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> { - let Some(window) = self.get_window(window) else { - log::error!("bug: Failed to get window for XIM preedit"); - return None; - }; - - let mut state = self.0.borrow_mut(); - let (mut ximc, xim_handler) = state.take_xim()?; - state.composing = !text.is_empty(); - drop(state); - window.handle_ime_preedit(text); - - if let Some(scaled_area) = window.get_ime_area() { - let ic_attributes = ximc - .build_ic_attributes() - .push( - xim::AttributeName::InputStyle, - xim::InputStyle::PREEDIT_CALLBACKS, - ) - .push(xim::AttributeName::ClientWindow, xim_handler.window) - .push(xim::AttributeName::FocusWindow, xim_handler.window) - .nested_list(xim::AttributeName::PreeditAttributes, |b| { - b.push( - xim::AttributeName::SpotLocation, - xim::Point { - x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, - y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, - }, - ); - }) - .build(); - ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) - .ok(); - } - let mut state = self.0.borrow_mut(); - state.restore_xim(ximc, xim_handler); - drop(state); - Some(()) - } - - fn handle_keyboard_layout_change(&self) { - let mut state = self.0.borrow_mut(); - let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); - let keymap = state.xkb.get_keymap(); - let layout_name = keymap.layout_get_name(layout_idx); - if layout_name != state.keyboard_layout.name() { - state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); - if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { - drop(state); - callback(); - state = self.0.borrow_mut(); - state.common.callbacks.keyboard_layout_change = Some(callback); - } - } - } + pub(crate) fn new() -> anyhow::Result { + let event_loop = EventLoop::try_new()?; + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut X11Client| { + if let calloop::channel::Event::Msg(runnable) = event { + // Insert the runnables as idle callbacks, so we make sure that user-input and X11 + // events have higher priority and runnables are only worked off after the event + // callbacks. + handle.insert_idle(|_| { + let start = Instant::now(); + let location = runnable.metadata().location; + let mut timing = TaskTiming { + location, + start, + end: None, + }; + profiler::add_task_timing(timing); + + runnable.run(); + + let end = Instant::now(); + timing.end = Some(end); + profiler::add_task_timing(timing); + }); + } + } + }) + .map_err(|err| { + anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}") + })?; + + let (xcb_connection, x_root_index) = XCBConnection::connect(None)?; + xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?; + + // Announce to X server that XInput up to 2.4 is supported. + // Version 2.4 is needed for gesture events (GesturePinchBegin/Update/End). + // The server responds with the highest version it supports; if < 2.4, + // we must not request gesture event masks in XISelectEvents. + let xinput_version = get_reply( + || "XInput XiQueryVersion failed", + xcb_connection.xinput_xi_query_version(2, 4), + )?; + assert!( + xinput_version.major_version >= 2, + "XInput version >= 2 required." + ); + let supports_xinput_gestures = xinput_version.major_version > 2 + || (xinput_version.major_version == 2 && xinput_version.minor_version >= 4); + log::info!( + "XInput version: {}.{}, gesture support: {}", + xinput_version.major_version, + xinput_version.minor_version, + supports_xinput_gestures, + ); + + let pointer_device_states = + current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default(); + + let atoms = XcbAtoms::new(&xcb_connection) + .context("Failed to get XCB atoms")? + .reply() + .context("Failed to get XCB atoms")?; + + let root = xcb_connection.setup().roots[0].root; + let compositor_present = check_compositor_present(&xcb_connection, root); + let gtk_frame_extents_supported = + check_gtk_frame_extents_supported(&xcb_connection, &atoms, root); + let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported; + log::info!( + "x11: compositor present: {}, gtk_frame_extents_supported: {}", + compositor_present, + gtk_frame_extents_supported + ); + + let xkb = get_reply( + || "Failed to initialize XKB extension", + xcb_connection + .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION), + )?; + assert!(xkb.supported); + + let events = xkb::EventType::STATE_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::NEW_KEYBOARD_NOTIFY; + let map_notify_parts = xkb::MapPart::KEY_TYPES + | xkb::MapPart::KEY_SYMS + | xkb::MapPart::MODIFIER_MAP + | xkb::MapPart::EXPLICIT_COMPONENTS + | xkb::MapPart::KEY_ACTIONS + | xkb::MapPart::KEY_BEHAVIORS + | xkb::MapPart::VIRTUAL_MODS + | xkb::MapPart::VIRTUAL_MOD_MAP; + check_reply( + || "Failed to select XKB events", + xcb_connection.xkb_select_events( + xkb::ID::USE_CORE_KBD.into(), + 0u8.into(), + events, + map_notify_parts, + map_notify_parts, + &xkb::SelectEventsAux::new(), + ), + )?; + + let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &xkb_context, + &xcb_connection, + xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) + }; + let compose_state = get_xkb_compose_state(&xkb_context); + let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let layout_name = xkb_state + .get_keymap() + .layout_get_name(layout_idx) + .to_string(); + let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into()); + + let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) + .context("Failed to create resource database")?; + let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); + let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) + .context("Failed to initialize cursor theme handler")? + .reply() + .context("Failed to initialize cursor theme handler")?; + + let clipboard = Clipboard::new().context("Failed to initialize clipboard")?; + + let screen = &xcb_connection.setup().roots[x_root_index]; + let compositor_gpu = detect_compositor_gpu(&xcb_connection, screen); + + let xcb_connection = Rc::new(xcb_connection); + + let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok(); + let xim_handler = if ximc.is_some() { + Some(XimHandler::new()) + } else { + None + }; + + // Safety: Safe if xcb::Connection always returns a valid fd + let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; + + handle + .insert_source( + Generic::new_with_error::( + fd, + calloop::Interest::READ, + calloop::Mode::Level, + ), + { + let xcb_connection = xcb_connection.clone(); + move |_readiness, _, client| { + client.process_x11_events(&xcb_connection)?; + Ok(calloop::PostAction::Continue) + } + }, + ) + .map_err(|err| anyhow!("Failed to initialize X11 event source: {err:?}"))?; + + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + client.with_common(|common| common.appearance = appearance); + for window in client.0.borrow_mut().windows.values_mut() { + window.window.set_appearance(appearance); + } + } + XDPEvent::ButtonLayout(layout_str) => { + let layout = WindowButtonLayout::parse(&layout_str) + .log_err() + .unwrap_or_else(WindowButtonLayout::linux_default); + client.with_common(|common| common.button_layout = layout); + for window in client.0.borrow_mut().windows.values_mut() { + window.window.set_button_layout(); + } + } + XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { + // noop, X11 manages this for us. + } + } + }) + .map_err(|err| anyhow!("Failed to initialize XDP event source: {err:?}"))?; + + xcb_flush(&xcb_connection); + + Ok(X11Client(Rc::new(RefCell::new(X11ClientState { + modifiers: Modifiers::default(), + capslock: Capslock::default(), + last_modifiers_changed_event: Modifiers::default(), + last_capslock_changed_event: Capslock::default(), + event_loop: Some(event_loop), + loop_handle: handle, + common, + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::new(px(0.0), px(0.0)), + current_count: 0, + pinch_scale: 1.0, + gpu_context: Rc::new(RefCell::new(None)), + compositor_gpu, + gpu_requirements: None, + scale_factor, + + xkb_context, + xcb_connection, + xkb_device_id, + client_side_decorations_supported, + x_root_index, + resource_database, + atoms, + windows: HashMap::default(), + mouse_focused_window: None, + keyboard_focused_window: None, + xkb: xkb_state, + keyboard_layout, + ximc, + xim_handler, + + compose_state, + pre_edit_text: None, + pre_key_char_down: None, + composing: false, + + cursor_handle, + cursor_styles: HashMap::default(), + cursor_cache: HashMap::default(), + cursor_hidden_window: None, + invisible_cursor_cache: None, + + pointer_device_states, + + supports_xinput_gestures, + + clipboard, + clipboard_item: None, + xdnd_state: Xdnd::default(), + })))) + } + + pub fn process_x11_events( + &self, + xcb_connection: &XCBConnection, + ) -> Result<(), EventHandlerError> { + loop { + let mut events = Vec::new(); + let mut windows_to_refresh = HashSet::new(); + + let mut last_key_release = None; + + // event handlers for new keyboard / remapping refresh the state without using event + // details, this deduplicates them. + let mut last_keymap_change_event: Option = None; + + loop { + match xcb_connection.poll_for_event() { + Ok(Some(event)) => { + match event { + Event::Expose(expose_event) => { + windows_to_refresh.insert(expose_event.window); + } + Event::KeyRelease(_) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + last_key_release = Some(event); + } + Event::KeyPress(key_press) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + if let Some(Event::KeyRelease(key_release)) = + last_key_release.take() + { + // We ignore that last KeyRelease if it's too close to this KeyPress, + // suggesting that it's auto-generated by X11 as a key-repeat event. + if key_release.detail != key_press.detail + || key_press.time.saturating_sub(key_release.time) > 20 + { + events.push(Event::KeyRelease(key_release)); + } + } + events.push(Event::KeyPress(key_press)); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + last_keymap_change_event = Some(event); + } + _ => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + events.push(event); + } + } + } + Ok(None) => { + break; + } + Err(err @ ConnectionError::IoError(..)) => { + return Err(EventHandlerError::from(err)); + } + Err(err) => { + let err = handle_connection_error(err); + log::warn!("error while polling for X11 events: {err:?}"); + break; + } + } + } + + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + if let Some(keymap_change_event) = last_keymap_change_event.take() { + events.push(keymap_change_event); + } + + if events.is_empty() && windows_to_refresh.is_empty() { + break; + } + + for window in windows_to_refresh.into_iter() { + let mut state = self.0.borrow_mut(); + if let Some(window) = state.windows.get_mut(&window) { + window.expose_event_received = true; + } + } + + for event in events.into_iter() { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + drop(state); + self.handle_event(event); + continue; + } + + let Some((mut ximc, mut xim_handler)) = state.take_xim() else { + continue; + }; + let xim_connected = xim_handler.connected; + drop(state); + + let xim_filtered = ximc.filter_event(&event, &mut xim_handler); + let xim_callback_event = xim_handler.last_callback_event.take(); + + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + + if let Some(event) = xim_callback_event { + self.handle_xim_callback_event(event); + } + + match xim_filtered { + Ok(handled) => { + if handled { + continue; + } + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + Err(err) => { + // this might happen when xim server crashes on one of the events + // we do lose 1-2 keys when crash happens since there is no reliable way to get that info + // luckily, x11 sends us window not found error when xim server crashes upon further key press + // hence we fall back to handle_event + log::error!("XIMClientError: {}", err); + let mut state = self.0.borrow_mut(); + state.take_xim(); + drop(state); + self.handle_event(event); + } + } + } + } + Ok(()) + } + + pub fn enable_ime(&self) { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + return; + } + + let Some((mut ximc, xim_handler)) = state.take_xim() else { + return; + }; + let mut ic_attributes = ximc + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, xim_handler.window) + .push(AttributeName::FocusWindow, xim_handler.window); + + let window_id = state.keyboard_focused_window; + drop(state); + if let Some(window_id) = window_id { + let Some(window) = self.get_window(window_id) else { + log::error!("Failed to get window for IME positioning"); + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + return; + }; + if let Some(scaled_area) = window.get_ime_area() { + ic_attributes = + ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }); + } + } + ximc.create_ic(xim_handler.im_id, ic_attributes.build()) + .ok(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + } + + pub fn reset_ime(&self) { + let mut state = self.0.borrow_mut(); + state.composing = false; + if let Some(mut ximc) = state.ximc.take() { + if let Some(xim_handler) = state.xim_handler.as_ref() { + ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok(); + } else { + log::error!("bug: xim handler not set in reset_ime"); + } + state.ximc = Some(ximc); + } + } + + pub(crate) fn get_window(&self, win: xproto::Window) -> Option { + let state = self.0.borrow(); + state + .windows + .get(&win) + .filter(|window_reference| !window_reference.window.state.borrow().destroyed) + .map(|window_reference| window_reference.window.clone()) + } + + fn handle_event(&self, event: Event) -> Option<()> { + match event { + Event::UnmapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = false; + } + state.update_refresh_loop(event.window); + } + Event::MapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = true; + } + state.update_refresh_loop(event.window); + } + Event::VisibilityNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.last_visibility = event.state; + } + state.update_refresh_loop(event.window); + } + Event::ClientMessage(event) => { + let window = self.get_window(event.window)?; + let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); + let mut state = self.0.borrow_mut(); + + if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() { + // window "x" button clicked by user + // Rest of the close logic is handled in drop_window() + drop(state); + window.close(); + state = self.0.borrow_mut(); + } else if atom == state.atoms._NET_WM_SYNC_REQUEST { + window.state.borrow_mut().last_sync_counter = + Some(x11rb::protocol::sync::Int64 { + lo: arg2, + hi: arg3 as i32, + }) + } + + if event.type_ == state.atoms.XdndEnter { + state.xdnd_state.other_window = atom; + if (arg1 & 0x1) == 0x1 { + state.xdnd_state.drag_type = xdnd_get_supported_atom( + &state.xcb_connection, + &state.atoms, + state.xdnd_state.other_window, + ); + } else { + if let Some(atom) = [arg2, arg3, arg4] + .into_iter() + .find(|atom| xdnd_is_atom_supported(*atom, &state.atoms)) + { + state.xdnd_state.drag_type = atom; + } + } + } else if event.type_ == state.atoms.XdndLeave { + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {})); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } else if event.type_ == state.atoms.XdndPosition { + if let Ok(pos) = get_reply( + || "Failed to query pointer position", + state.xcb_connection.query_pointer(event.window), + ) { + state.xdnd_state.position = + Point::new(px(pos.win_x as f32), px(pos.win_y as f32)); + } + if !state.xdnd_state.retrieved { + check_reply( + || "Failed to convert selection for drag and drop", + state.xcb_connection.convert_selection( + event.window, + state.atoms.XdndSelection, + state.xdnd_state.drag_type, + state.atoms.XDND_DATA, + arg3, + ), + ) + .log_err(); + } + xdnd_send_status( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + arg4, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + } else if event.type_ == state.atoms.XdndDrop { + xdnd_send_finished( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } + } + Event::SelectionNotify(event) => { + let window = self.get_window(event.requestor)?; + let state = self.0.borrow_mut(); + let reply = get_reply( + || "Failed to get XDND_DATA", + state.xcb_connection.get_property( + false, + event.requestor, + state.atoms.XDND_DATA, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_err(); + let Some(reply) = reply else { + return Some(()); + }; + if let Ok(file_list) = str::from_utf8(&reply.value) { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| match url.to_file_path() { + Ok(url) => Some(url), + Err(()) => { + log::error!("Failed turn {url:?} into a file path"); + None + } + }) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: gpui::ExternalPaths(paths), + }); + drop(state); + window.handle_input(input); + self.0.borrow_mut().xdnd_state.retrieved = true; + } + } + Event::ConfigureNotify(event) => { + let bounds = Bounds { + origin: Point { + x: event.x.into(), + y: event.y.into(), + }, + size: Size { + width: event.width.into(), + height: event.height.into(), + }, + }; + let window = self.get_window(event.window)?; + window + .set_bounds(bounds) + .context("X11: Failed to set window bounds") + .log_err(); + } + Event::PropertyNotify(event) => { + let window = self.get_window(event.window)?; + window + .property_notify(event) + .context("X11: Failed to handle property notify") + .log_err(); + } + Event::FocusIn(event) => { + let window = self.get_window(event.event)?; + window.set_active(true); + let mut state = self.0.borrow_mut(); + state.keyboard_focused_window = Some(event.event); + if let Some(handler) = state.xim_handler.as_mut() { + handler.window = event.event; + } + drop(state); + self.enable_ime(); + } + Event::FocusOut(event) => { + let window = self.get_window(event.event)?; + window.set_active(false); + let mut state = self.0.borrow_mut(); + // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); + state.keyboard_focused_window = None; + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + state.pre_edit_text.take(); + state.restore_cursor_after_hide(); + drop(state); + self.reset_ime(); + window.handle_ime_delete(); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + let mut state = self.0.borrow_mut(); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &state.xkb_context, + &state.xcb_connection, + state.xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device( + &xkb_keymap, + &state.xcb_connection, + state.xkb_device_id, + ) + }; + state.xkb = xkb_state; + drop(state); + self.handle_keyboard_layout_change(); + } + Event::XkbStateNotify(event) => { + let mut state = self.0.borrow_mut(); + let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let new_layout = u32::from(event.group); + state.xkb.update_mask( + event.base_mods.into(), + event.latched_mods.into(), + event.locked_mods.into(), + event.base_group as u32, + event.latched_group as u32, + event.locked_group.into(), + ); + let modifiers = modifiers_from_xkb(&state.xkb); + let capslock = capslock_from_xkb(&state.xkb); + if state.last_modifiers_changed_event == modifiers + && state.last_capslock_changed_event == capslock + { + drop(state); + } else { + let focused_window_id = state.keyboard_focused_window?; + state.modifiers = modifiers; + state.last_modifiers_changed_event = modifiers; + state.capslock = capslock; + state.last_capslock_changed_event = capslock; + drop(state); + + let focused_window = self.get_window(focused_window_id)?; + focused_window.handle_input(PlatformInput::ModifiersChanged( + ModifiersChangedEvent { + modifiers, + capslock, + }, + )); + } + + if new_layout != old_layout { + self.handle_keyboard_layout_change(); + } + } + Event::KeyPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + state.pre_key_char_down.take(); + let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); + + let keystroke = { + let code = event.detail.into(); + let mut keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); + let keysym = key_event_state.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + if let Some(mut compose_state) = state.compose_state.take() { + compose_state.feed(keysym); + match compose_state.status() { + xkbc::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose_state.utf8(); + if let Some(keysym) = compose_state.keysym() { + keystroke.key = xkbc::keysym_get_name(keysym); + } + } + xkbc::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = compose_state + .utf8() + .or(keystroke_underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + window.handle_ime_preedit(pre_edit); + state = self.0.borrow_mut(); + } + xkbc::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + drop(state); + if let Some(pre_edit) = pre_edit { + window.handle_ime_commit(pre_edit); + } + if let Some(current_key) = keystroke_underlying_dead_key(keysym) { + window.handle_ime_preedit(current_key); + } + state = self.0.borrow_mut(); + compose_state.feed(keysym); + } + _ => {} + } + state.compose_state = Some(compose_state); + } + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyDown(gpui::KeyDownEvent { + keystroke, + is_held: false, + prefer_character_input: false, + })); + } + Event::KeyRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); + + let keystroke = { + let code = event.detail.into(); + let keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); + let keysym = key_event_state.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke })); + } + Event::XinputButtonPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + + if state.composing && state.ximc.is_some() { + drop(state); + self.reset_ime(); + window.handle_ime_unmark(); + state = self.0.borrow_mut(); + } else if let Some(text) = state.pre_edit_text.take() { + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + drop(state); + window.handle_ime_commit(text); + state = self.0.borrow_mut(); + } + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_elapsed = state.last_click.elapsed(); + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance(state.last_location, position) + { + state.current_count += 1; + } else { + state.current_count = 1; + } + + state.last_click = Instant::now(); + state.last_mouse_button = Some(button); + state.last_location = position; + let current_count = state.current_count; + + drop(state); + window.handle_input(PlatformInput::MouseDown(gpui::MouseDownEvent { + button, + position, + modifiers, + click_count: current_count, + first_mouse: false, + })); + } + Some(ButtonOrScroll::Scroll(direction)) => { + drop(state); + // Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events. + // Since handling those events does the scrolling, they are skipped here. + if !event + .flags + .contains(xinput::PointerEventFlags::POINTER_EMULATED) + { + let scroll_delta = match direction { + ScrollDirection::Up => Point::new(0.0, SCROLL_LINES), + ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES), + ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0), + ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0), + }; + window.handle_input(PlatformInput::ScrollWheel( + make_scroll_wheel_event(position, scroll_delta, modifiers), + )); + } + } + None => { + log::error!("Unknown x11 button: {}", event.detail); + } + } + } + Event::XinputButtonRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_count = state.current_count; + drop(state); + window.handle_input(PlatformInput::MouseUp(gpui::MouseUpEvent { + button, + position, + modifiers, + click_count, + })); + } + Some(ButtonOrScroll::Scroll(_)) => {} + None => {} + } + } + Event::XinputMotion(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.restore_cursor_after_hide(); + if window.is_blocked() { + // We want to set the cursor to the default arrow + // when the window is blocked + let style = CursorStyle::Arrow; + + let current_style = state + .cursor_styles + .get(&window.x_window) + .unwrap_or(&CursorStyle::Arrow); + if *current_style != style + && let Some(cursor) = state.get_cursor_icon(style) + { + state.cursor_styles.insert(window.x_window, style); + check_reply( + || "Failed to set cursor style", + state.xcb_connection.change_window_attributes( + window.x_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + state.xcb_connection.flush().log_err(); + }; + } + let pressed_button = pressed_button_from_mask(event.button_mask[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + if event.valuator_mask[0] & 3 != 0 { + window.handle_input(PlatformInput::MouseMove(gpui::MouseMoveEvent { + position, + pressed_button, + modifiers, + })); + } + + state = self.0.borrow_mut(); + if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); + drop(state); + if let Some(scroll_delta) = scroll_delta { + window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( + position, + scroll_delta, + modifiers, + ))); + } + } + } + Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => { + let window = self.get_window(event.event)?; + window.set_hovered(true); + let mut state = self.0.borrow_mut(); + state.mouse_focused_window = Some(event.event); + state.restore_cursor_after_hide(); + } + Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => { + let mut state = self.0.borrow_mut(); + + // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); + state.mouse_focused_window = None; + let pressed_button = pressed_button_from_mask(event.buttons[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + let window = self.get_window(event.event)?; + window.handle_input(PlatformInput::MouseExited(gpui::MouseExitEvent { + pressed_button, + position, + modifiers, + })); + window.set_hovered(false); + } + Event::XinputHierarchy(event) => { + let mut state = self.0.borrow_mut(); + // Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values. + // Any change to a device invalidates its scroll values. + for info in event.infos { + if is_pointer_device(info.type_) { + state.pointer_device_states.remove(&info.deviceid); + } + } + if let Some(pointer_device_states) = current_pointer_device_states( + &state.xcb_connection, + &state.pointer_device_states, + ) { + state.pointer_device_states = pointer_device_states; + } + } + Event::XinputDeviceChanged(event) => { + let mut state = self.0.borrow_mut(); + if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + reset_pointer_device_scroll_positions(pointer); + } + } + Event::XinputGesturePinchBegin(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Started, + })); + } + Event::XinputGesturePinchUpdate(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + // scale is in FP16.16 format: divide by 65536 to get the float value + let new_absolute_scale = event.scale as f32 / 65536.0; + let previous_scale = state.pinch_scale; + let zoom_delta = new_absolute_scale - previous_scale; + state.pinch_scale = new_absolute_scale; + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: zoom_delta, + modifiers, + phase: gpui::TouchPhase::Moved, + })); + } + Event::XinputGesturePinchEnd(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Ended, + })); + } + _ => {} + }; + + Some(()) + } + + fn handle_xim_callback_event(&self, event: XimCallbackEvent) { + match event { + XimCallbackEvent::XimXEvent(event) => { + self.handle_event(event); + } + XimCallbackEvent::XimCommitEvent(window, text) => { + self.xim_handle_commit(window, text); + } + XimCallbackEvent::XimPreeditEvent(window, text) => { + self.xim_handle_preedit(window, text); + } + }; + } + + fn xim_handle_event(&self, event: Event) -> Option<()> { + match event { + Event::KeyPress(event) | Event::KeyRelease(event) => { + let mut state = self.0.borrow_mut(); + state.pre_key_char_down = Some(keystroke_from_xkb( + &state.xkb, + state.modifiers, + event.detail.into(), + )); + let (mut ximc, mut xim_handler) = state.take_xim()?; + drop(state); + xim_handler.window = event.event; + ximc.forward_event( + xim_handler.im_id, + xim_handler.ic_id, + xim::ForwardEventFlag::empty(), + &event, + ) + .context("X11: Failed to forward XIM event") + .log_err(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + } + event => { + self.handle_event(event); + } + } + Some(()) + } + + fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM commit"); + return None; + }; + let mut state = self.0.borrow_mut(); + state.composing = false; + drop(state); + window.handle_ime_commit(text); + Some(()) + } + + fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM preedit"); + return None; + }; + + let mut state = self.0.borrow_mut(); + let (mut ximc, xim_handler) = state.take_xim()?; + state.composing = !text.is_empty(); + drop(state); + window.handle_ime_preedit(text); + + if let Some(scaled_area) = window.get_ime_area() { + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }) + .build(); + ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .ok(); + } + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + Some(()) + } + + fn handle_keyboard_layout_change(&self) { + let mut state = self.0.borrow_mut(); + let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let keymap = state.xkb.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + if layout_name != state.keyboard_layout.name() { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { + drop(state); + callback(); + state = self.0.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } + } } impl LinuxClient for X11Client { - fn compositor_name(&self) -> &'static str { - "X11" - } + fn compositor_name(&self) -> &'static str { + "X11" + } - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { - f(&mut self.0.borrow_mut().common) - } + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } - fn keyboard_layout(&self) -> Box { - let state = self.0.borrow(); - Box::new(state.keyboard_layout.clone()) - } + fn keyboard_layout(&self) -> Box { + let state = self.0.borrow(); + Box::new(state.keyboard_layout.clone()) + } - fn displays(&self) -> Vec> { - let state = self.0.borrow(); - let setup = state.xcb_connection.setup(); - setup - .roots - .iter() - .enumerate() - .filter_map(|(root_id, _)| { - Some(Rc::new( - X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, - ) as Rc) - }) - .collect() - } + fn displays(&self) -> Vec> { + let state = self.0.borrow(); + let setup = state.xcb_connection.setup(); + setup + .roots + .iter() + .enumerate() + .filter_map(|(root_id, _)| { + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, + ) as Rc) + }) + .collect() + } - fn primary_display(&self) -> Option> { - let state = self.0.borrow(); - X11Display::new( - &state.xcb_connection, - state.scale_factor, - state.x_root_index, - ) - .log_err() - .map(|display| Rc::new(display) as Rc) - } + fn primary_display(&self) -> Option> { + let state = self.0.borrow(); + X11Display::new( + &state.xcb_connection, + state.scale_factor, + state.x_root_index, + ) + .log_err() + .map(|display| Rc::new(display) as Rc) + } - fn display(&self, id: DisplayId) -> Option> { - let state = self.0.borrow(); + fn display(&self, id: DisplayId) -> Option> { + let state = self.0.borrow(); - Some(Rc::new( - X11Display::new( - &state.xcb_connection, - state.scale_factor, - u64::from(id) as usize, - ) - .ok()?, - )) - } + Some(Rc::new( + X11Display::new( + &state.xcb_connection, + state.scale_factor, + u64::from(id) as usize, + ) + .ok()?, + )) + } - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - true - } + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> futures::channel::oneshot::Receiver>>> - { - gpui::scap_screen_capture::scap_screen_sources(&self.0.borrow().common.foreground_executor) - } + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + gpui::scap_screen_capture::scap_screen_sources(&self.0.borrow().common.foreground_executor) + } - fn set_gpu_requirements(&self, requirements: Box) { - if let Ok(reqs) = requirements.downcast::() { - self.0.borrow_mut().gpu_requirements = Some(*reqs); - } else { - log::warn!("set_gpu_requirements: unexpected type, expected WgpuDeviceRequirements"); - } - } + fn set_gpu_requirements(&self, requirements: Box) { + if let Ok(reqs) = requirements.downcast::() { + self.0.borrow_mut().gpu_requirements = Some(*reqs); + } else { + log::warn!("set_gpu_requirements: unexpected type, expected WgpuDeviceRequirements"); + } + } - fn open_window( - &self, - handle: AnyWindowHandle, - params: WindowParams, - ) -> anyhow::Result> { - let mut state = self.0.borrow_mut(); - let parent_window = state - .keyboard_focused_window - .and_then(|focused_window| state.windows.get(&focused_window)) - .map(|w| w.window.clone()); - let x_window = state - .xcb_connection - .generate_id() - .context("X11: Failed to generate window ID")?; + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); + let parent_window = state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|w| w.window.clone()); + let x_window = state + .xcb_connection + .generate_id() + .context("X11: Failed to generate window ID")?; - let xcb_connection = state.xcb_connection.clone(); - let client_side_decorations_supported = state.client_side_decorations_supported; - let x_root_index = state.x_root_index; - let atoms = state.atoms; - let scale_factor = state.scale_factor; - let appearance = state.common.appearance; - let compositor_gpu = state.compositor_gpu.take(); - let gpu_requirements = state.gpu_requirements.clone(); - let supports_xinput_gestures = state.supports_xinput_gestures; - let is_bgr = state - .resource_database - .get_string("Xft.rgba", "Xft.Rgba") - .is_some_and(|v| v.eq_ignore_ascii_case("bgr")); - let window = X11Window::new( - handle, - X11ClientStatePtr(Rc::downgrade(&self.0)), - state.common.foreground_executor.clone(), - state.gpu_context.clone(), - compositor_gpu, - gpu_requirements, - params, - &xcb_connection, - client_side_decorations_supported, - x_root_index, - x_window, - &atoms, - scale_factor, - appearance, - parent_window, - supports_xinput_gestures, - is_bgr, - )?; - check_reply( - || "Failed to set XdndAware property", - state.xcb_connection.change_property32( - xproto::PropMode::REPLACE, - x_window, - state.atoms.XdndAware, - state.atoms.XA_ATOM, - &[5], - ), - ) - .log_err(); - xcb_flush(&state.xcb_connection); + let xcb_connection = state.xcb_connection.clone(); + let client_side_decorations_supported = state.client_side_decorations_supported; + let x_root_index = state.x_root_index; + let atoms = state.atoms; + let scale_factor = state.scale_factor; + let appearance = state.common.appearance; + let compositor_gpu = state.compositor_gpu.take(); + let gpu_requirements = state.gpu_requirements.clone(); + let supports_xinput_gestures = state.supports_xinput_gestures; + let is_bgr = state + .resource_database + .get_string("Xft.rgba", "Xft.Rgba") + .is_some_and(|v| v.eq_ignore_ascii_case("bgr")); + let window = X11Window::new( + handle, + X11ClientStatePtr(Rc::downgrade(&self.0)), + state.common.foreground_executor.clone(), + state.gpu_context.clone(), + compositor_gpu, + gpu_requirements, + params, + &xcb_connection, + client_side_decorations_supported, + x_root_index, + x_window, + &atoms, + scale_factor, + appearance, + parent_window, + supports_xinput_gestures, + is_bgr, + )?; + check_reply( + || "Failed to set XdndAware property", + state.xcb_connection.change_property32( + xproto::PropMode::REPLACE, + x_window, + state.atoms.XdndAware, + state.atoms.XA_ATOM, + &[5], + ), + ) + .log_err(); + xcb_flush(&state.xcb_connection); - let window_ref = WindowRef { - window: window.0.clone(), - refresh_state: None, - expose_event_received: false, - last_visibility: Visibility::UNOBSCURED, - is_mapped: false, - }; + let window_ref = WindowRef { + window: window.0.clone(), + refresh_state: None, + expose_event_received: false, + last_visibility: Visibility::UNOBSCURED, + is_mapped: false, + }; - state.windows.insert(x_window, window_ref); - Ok(Box::new(window)) - } + state.windows.insert(x_window, window_ref); + Ok(Box::new(window)) + } - fn set_cursor_style(&self, style: CursorStyle) { - let mut state = self.0.borrow_mut(); - let Some(focused_window) = state.mouse_focused_window else { - return; - }; - let current_style = state - .cursor_styles - .get(&focused_window) - .unwrap_or(&CursorStyle::Arrow); + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + let Some(focused_window) = state.mouse_focused_window else { + return; + }; + let current_style = state + .cursor_styles + .get(&focused_window) + .unwrap_or(&CursorStyle::Arrow); - let window = state - .mouse_focused_window - .and_then(|w| state.windows.get(&w)); + let window = state + .mouse_focused_window + .and_then(|w| state.windows.get(&w)); - let should_change = *current_style != style - && (window.is_none() || window.is_some_and(|w| !w.is_blocked())); + let should_change = *current_style != style + && (window.is_none() || window.is_some_and(|w| !w.is_blocked())); - if !should_change { - return; - } + if !should_change { + return; + } - state.cursor_styles.insert(focused_window, style); + state.cursor_styles.insert(focused_window, style); - // Don't clobber the invisible cursor; restore reads back from `cursor_styles`. - if state.cursor_hidden_window == Some(focused_window) { - return; - } + // Don't clobber the invisible cursor; restore reads back from `cursor_styles`. + if state.cursor_hidden_window == Some(focused_window) { + return; + } - let Some(cursor) = state.get_cursor_icon(style) else { - return; - }; + let Some(cursor) = state.get_cursor_icon(style) else { + return; + }; - check_reply( - || "Failed to set cursor style", - state.xcb_connection.change_window_attributes( - focused_window, - &ChangeWindowAttributesAux { - cursor: Some(cursor), - ..Default::default() - }, - ), - ) - .log_err(); - state.xcb_connection.flush().log_err(); - } + check_reply( + || "Failed to set cursor style", + state.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + state.xcb_connection.flush().log_err(); + } - fn hide_cursor_until_mouse_moves(&self) { - self.0.borrow_mut().hide_cursor_until_mouse_moves(); - } + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } - fn is_cursor_visible(&self) -> bool { - self.0.borrow().cursor_hidden_window.is_none() - } + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() + } - fn open_uri(&self, uri: &str) { - #[cfg(any(feature = "wayland", feature = "x11"))] - open_uri_internal( - self.with_common(|c| c.background_executor.clone()), - uri, - None, - ); - } + fn open_uri(&self, uri: &str) { + #[cfg(any(feature = "wayland", feature = "x11"))] + open_uri_internal( + self.with_common(|c| c.background_executor.clone()), + uri, + None, + ); + } - fn reveal_path(&self, path: PathBuf) { - #[cfg(any(feature = "x11", feature = "wayland"))] - reveal_path_internal( - self.with_common(|c| c.background_executor.clone()), - path, - None, - ); - } + fn reveal_path(&self, path: PathBuf) { + #[cfg(any(feature = "x11", feature = "wayland"))] + reveal_path_internal( + self.with_common(|c| c.background_executor.clone()), + path, + None, + ); + } - fn write_to_primary(&self, item: gpui::ClipboardItem) { - let state = self.0.borrow_mut(); - state - .clipboard - .set_text( - std::borrow::Cow::Owned(item.text().unwrap_or_default()), - clipboard::ClipboardKind::Primary, - clipboard::WaitConfig::None, - ) - .context("X11 Failed to write to clipboard (primary)") - .log_with_level(log::Level::Debug); - } + fn write_to_primary(&self, item: gpui::ClipboardItem) { + let state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Primary, + clipboard::WaitConfig::None, + ) + .context("X11 Failed to write to clipboard (primary)") + .log_with_level(log::Level::Debug); + } - fn write_to_clipboard(&self, item: gpui::ClipboardItem) { - let mut state = self.0.borrow_mut(); - state - .clipboard - .set_text( - std::borrow::Cow::Owned(item.text().unwrap_or_default()), - clipboard::ClipboardKind::Clipboard, - clipboard::WaitConfig::None, - ) - .context("X11: Failed to write to clipboard (clipboard)") - .log_with_level(log::Level::Debug); - state.clipboard_item.replace(item); - } + fn write_to_clipboard(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Clipboard, + clipboard::WaitConfig::None, + ) + .context("X11: Failed to write to clipboard (clipboard)") + .log_with_level(log::Level::Debug); + state.clipboard_item.replace(item); + } - fn read_from_primary(&self) -> Option { - let state = self.0.borrow_mut(); - state - .clipboard - .get_any(clipboard::ClipboardKind::Primary) - .context("X11: Failed to read from clipboard (primary)") - .log_with_level(log::Level::Debug) - } + fn read_from_primary(&self) -> Option { + let state = self.0.borrow_mut(); + state + .clipboard + .get_any(clipboard::ClipboardKind::Primary) + .context("X11: Failed to read from clipboard (primary)") + .log_with_level(log::Level::Debug) + } - fn read_from_clipboard(&self) -> Option { - let state = self.0.borrow_mut(); - // if the last copy was from this app, return our cached item - // which has metadata attached. - if state - .clipboard - .is_owner(clipboard::ClipboardKind::Clipboard) - { - return state.clipboard_item.clone(); - } - state - .clipboard - .get_any(clipboard::ClipboardKind::Clipboard) - .context("X11: Failed to read from clipboard (clipboard)") - .log_with_level(log::Level::Debug) - } + fn read_from_clipboard(&self) -> Option { + let state = self.0.borrow_mut(); + // if the last copy was from this app, return our cached item + // which has metadata attached. + if state + .clipboard + .is_owner(clipboard::ClipboardKind::Clipboard) + { + return state.clipboard_item.clone(); + } + state + .clipboard + .get_any(clipboard::ClipboardKind::Clipboard) + .context("X11: Failed to read from clipboard (clipboard)") + .log_with_level(log::Level::Debug) + } - fn run(&self) { - let Some(mut event_loop) = self - .0 - .borrow_mut() - .event_loop - .take() - .context("X11Client::run called but it's already running") - .log_err() - else { - return; - }; + fn run(&self) { + let Some(mut event_loop) = self + .0 + .borrow_mut() + .event_loop + .take() + .context("X11Client::run called but it's already running") + .log_err() + else { + return; + }; - event_loop.run(None, &mut self.clone(), |_| {}).log_err(); - } + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } - fn active_window(&self) -> Option { - let state = self.0.borrow(); - state.keyboard_focused_window.and_then(|focused_window| { - state - .windows - .get(&focused_window) - .map(|window| window.handle()) - }) - } + fn active_window(&self) -> Option { + let state = self.0.borrow(); + state.keyboard_focused_window.and_then(|focused_window| { + state + .windows + .get(&focused_window) + .map(|window| window.handle()) + }) + } - fn window_stack(&self) -> Option> { - let state = self.0.borrow(); - let root = state.xcb_connection.setup().roots[state.x_root_index].root; + fn window_stack(&self) -> Option> { + let state = self.0.borrow(); + let root = state.xcb_connection.setup().roots[state.x_root_index].root; - let reply = state - .xcb_connection - .get_property( - false, - root, - state.atoms._NET_CLIENT_LIST_STACKING, - xproto::AtomEnum::WINDOW, - 0, - u32::MAX, - ) - .ok()? - .reply() - .ok()?; + let reply = state + .xcb_connection + .get_property( + false, + root, + state.atoms._NET_CLIENT_LIST_STACKING, + xproto::AtomEnum::WINDOW, + 0, + u32::MAX, + ) + .ok()? + .reply() + .ok()?; - let window_ids = reply - .value - .chunks_exact(4) - .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) - .collect::>(); + let window_ids = reply + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect::>(); - let mut handles = Vec::new(); + let mut handles = Vec::new(); - // We need to reverse, since _NET_CLIENT_LIST_STACKING has - // a back-to-front order. - // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html - for window_ref in window_ids - .iter() - .rev() - .filter_map(|&win| state.windows.get(&win)) - { - if !window_ref.window.state.borrow().destroyed { - handles.push(window_ref.handle()); - } - } + // We need to reverse, since _NET_CLIENT_LIST_STACKING has + // a back-to-front order. + // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html + for window_ref in window_ids + .iter() + .rev() + .filter_map(|&win| state.windows.get(&win)) + { + if !window_ref.window.state.borrow().destroyed { + handles.push(window_ref.handle()); + } + } - Some(handles) - } + Some(handles) + } - fn window_identifier(&self) -> impl Future> + Send + 'static { - let state = self.0.borrow(); - state - .keyboard_focused_window - .and_then(|focused_window| state.windows.get(&focused_window)) - .map(|window| window.window.x_window as u64) - .map(|x_window| std::future::ready(Some(WindowIdentifier::from_xid(x_window)))) - .unwrap_or(std::future::ready(None)) - } + fn window_identifier(&self) -> impl Future> + Send + 'static { + let state = self.0.borrow(); + state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|window| window.window.x_window as u64) + .map(|x_window| std::future::ready(Some(WindowIdentifier::from_xid(x_window)))) + .unwrap_or(std::future::ready(None)) + } } impl X11ClientState { - fn has_xim(&self) -> bool { - self.ximc.is_some() && self.xim_handler.is_some() - } + fn has_xim(&self) -> bool { + self.ximc.is_some() && self.xim_handler.is_some() + } - fn take_xim(&mut self) -> Option<(X11rbClient>, XimHandler)> { - let ximc = self - .ximc - .take() - .ok_or(anyhow!("bug: XIM connection not set")) - .log_err()?; - if let Some(xim_handler) = self.xim_handler.take() { - Some((ximc, xim_handler)) - } else { - self.ximc = Some(ximc); - log::error!("bug: XIM handler not set"); - None - } - } + fn take_xim(&mut self) -> Option<(X11rbClient>, XimHandler)> { + let ximc = self + .ximc + .take() + .ok_or(anyhow!("bug: XIM connection not set")) + .log_err()?; + if let Some(xim_handler) = self.xim_handler.take() { + Some((ximc, xim_handler)) + } else { + self.ximc = Some(ximc); + log::error!("bug: XIM handler not set"); + None + } + } - fn restore_xim(&mut self, ximc: X11rbClient>, xim_handler: XimHandler) { - self.ximc = Some(ximc); - self.xim_handler = Some(xim_handler); - } + fn restore_xim(&mut self, ximc: X11rbClient>, xim_handler: XimHandler) { + self.ximc = Some(ximc); + self.xim_handler = Some(xim_handler); + } - fn update_refresh_loop(&mut self, x_window: xproto::Window) { - let Some(window_ref) = self.windows.get_mut(&x_window) else { - return; - }; - let is_visible = window_ref.is_mapped - && !matches!(window_ref.last_visibility, Visibility::FULLY_OBSCURED); - match (is_visible, window_ref.refresh_state.take()) { - (false, refresh_state @ Some(RefreshState::Hidden { .. })) - | (false, refresh_state @ None) - | (true, refresh_state @ Some(RefreshState::PeriodicRefresh { .. })) => { - window_ref.refresh_state = refresh_state; - } - ( - false, - Some(RefreshState::PeriodicRefresh { - refresh_rate, - event_loop_token, - }), - ) => { - self.loop_handle.remove(event_loop_token); - window_ref.refresh_state = Some(RefreshState::Hidden { refresh_rate }); - } - (true, Some(RefreshState::Hidden { refresh_rate })) => { - let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); - let Some(window_ref) = self.windows.get_mut(&x_window) else { - return; - }; - window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { - refresh_rate, - event_loop_token, - }); - } - (true, None) => { - let Some(screen_resources) = get_reply( - || "Failed to get screen resources", - self.xcb_connection - .randr_get_screen_resources_current(x_window), - ) - .log_err() else { - return; - }; + fn update_refresh_loop(&mut self, x_window: xproto::Window) { + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + let is_visible = window_ref.is_mapped + && !matches!(window_ref.last_visibility, Visibility::FULLY_OBSCURED); + match (is_visible, window_ref.refresh_state.take()) { + (false, refresh_state @ Some(RefreshState::Hidden { .. })) + | (false, refresh_state @ None) + | (true, refresh_state @ Some(RefreshState::PeriodicRefresh { .. })) => { + window_ref.refresh_state = refresh_state; + } + ( + false, + Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }), + ) => { + self.loop_handle.remove(event_loop_token); + window_ref.refresh_state = Some(RefreshState::Hidden { refresh_rate }); + } + (true, Some(RefreshState::Hidden { refresh_rate })) => { + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + (true, None) => { + let Some(screen_resources) = get_reply( + || "Failed to get screen resources", + self.xcb_connection + .randr_get_screen_resources_current(x_window), + ) + .log_err() else { + return; + }; - // Ideally this would be re-queried when the window changes screens, but there - // doesn't seem to be an efficient / straightforward way to do this. Should also be - // updated when screen configurations change. - let mode_info = screen_resources.crtcs.iter().find_map(|crtc| { - let crtc_info = self - .xcb_connection - .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) - .ok()? - .reply() - .ok()?; + // Ideally this would be re-queried when the window changes screens, but there + // doesn't seem to be an efficient / straightforward way to do this. Should also be + // updated when screen configurations change. + let mode_info = screen_resources.crtcs.iter().find_map(|crtc| { + let crtc_info = self + .xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; - screen_resources - .modes - .iter() - .find(|m| m.id == crtc_info.mode) - }); - let refresh_rate = match mode_info { - Some(mode_info) => mode_refresh_rate(mode_info), - None => { - log::error!( - "Failed to get screen mode info from xrandr, \ + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }); + let refresh_rate = match mode_info { + Some(mode_info) => mode_refresh_rate(mode_info), + None => { + log::error!( + "Failed to get screen mode info from xrandr, \ defaulting to 60hz refresh rate." - ); - Duration::from_micros(1_000_000 / 60) - } - }; + ); + Duration::from_micros(1_000_000 / 60) + } + }; - let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); - let Some(window_ref) = self.windows.get_mut(&x_window) else { - return; - }; - window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { - refresh_rate, - event_loop_token, - }); - } - } - } + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + } + } - #[must_use] - fn start_refresh_loop( - &self, - x_window: xproto::Window, - refresh_rate: Duration, - ) -> RegistrationToken { - self.loop_handle - .insert_source(calloop::timer::Timer::immediate(), { - move |mut instant, (), client| { - let xcb_connection = { - let mut state = client.0.borrow_mut(); - let xcb_connection = state.xcb_connection.clone(); - if let Some(window) = state.windows.get_mut(&x_window) { - let expose_event_received = window.expose_event_received; - window.expose_event_received = false; - let force_render = std::mem::take( - &mut window.window.state.borrow_mut().force_render_after_recovery, - ); - let window = window.window.clone(); - drop(state); - window.refresh(RequestFrameOptions { - require_presentation: expose_event_received, - force_render, - }); - } - xcb_connection - }; - client.process_x11_events(&xcb_connection).log_err(); + #[must_use] + fn start_refresh_loop( + &self, + x_window: xproto::Window, + refresh_rate: Duration, + ) -> RegistrationToken { + self.loop_handle + .insert_source(calloop::timer::Timer::immediate(), { + move |mut instant, (), client| { + let xcb_connection = { + let mut state = client.0.borrow_mut(); + let xcb_connection = state.xcb_connection.clone(); + if let Some(window) = state.windows.get_mut(&x_window) { + let expose_event_received = window.expose_event_received; + window.expose_event_received = false; + let force_render = std::mem::take( + &mut window.window.state.borrow_mut().force_render_after_recovery, + ); + let window = window.window.clone(); + drop(state); + window.refresh(RequestFrameOptions { + require_presentation: expose_event_received, + force_render, + }); + } + xcb_connection + }; + client.process_x11_events(&xcb_connection).log_err(); - // Take into account that some frames have been skipped - let now = Instant::now(); - while instant < now { - instant += refresh_rate; - } - calloop::timer::TimeoutAction::ToInstant(instant) - } - }) - .expect("Failed to initialize window refresh timer") - } + // Take into account that some frames have been skipped + let now = Instant::now(); + while instant < now { + instant += refresh_rate; + } + calloop::timer::TimeoutAction::ToInstant(instant) + } + }) + .expect("Failed to initialize window refresh timer") + } - fn get_cursor_icon(&mut self, style: CursorStyle) -> Option { - if let Some(cursor) = self.cursor_cache.get(&style) { - return *cursor; - } + fn get_cursor_icon(&mut self, style: CursorStyle) -> Option { + if let Some(cursor) = self.cursor_cache.get(&style) { + return *cursor; + } - let result = 'outer: { - let mut errors = String::new(); - let cursor_icon_names = cursor_style_to_icon_names(style); - for cursor_icon_name in cursor_icon_names { - match self - .cursor_handle - .load_cursor(&self.xcb_connection, cursor_icon_name) - { - Ok(loaded_cursor) => { - if loaded_cursor != x11rb::NONE { - break 'outer Ok(loaded_cursor); - } - } - Err(err) => { - errors.push_str(&err.to_string()); - errors.push('\n'); - } - } - } - if errors.is_empty() { - Err(anyhow!( - "errors while loading cursor icons {:?}:\n{}", - cursor_icon_names, - errors - )) - } else { - Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)) - } - }; + let result = 'outer: { + let mut errors = String::new(); + let cursor_icon_names = cursor_style_to_icon_names(style); + for cursor_icon_name in cursor_icon_names { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, cursor_icon_name) + { + Ok(loaded_cursor) => { + if loaded_cursor != x11rb::NONE { + break 'outer Ok(loaded_cursor); + } + } + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } + } + } + if errors.is_empty() { + Err(anyhow!( + "errors while loading cursor icons {:?}:\n{}", + cursor_icon_names, + errors + )) + } else { + Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)) + } + }; - let cursor = match result { - Ok(cursor) => Some(cursor), - Err(err) => { - match self - .cursor_handle - .load_cursor(&self.xcb_connection, DEFAULT_CURSOR_ICON_NAME) - { - Ok(default) => { - log_cursor_icon_warning(err.context(format!( - "X11: error loading cursor icon, falling back on default icon '{}'", - DEFAULT_CURSOR_ICON_NAME - ))); - Some(default) - } - Err(default_err) => { - log_cursor_icon_warning(err.context(default_err).context(format!( - "X11: error loading default cursor fallback '{}'", - DEFAULT_CURSOR_ICON_NAME - ))); - None - } - } - } - }; + let cursor = match result { + Ok(cursor) => Some(cursor), + Err(err) => { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, DEFAULT_CURSOR_ICON_NAME) + { + Ok(default) => { + log_cursor_icon_warning(err.context(format!( + "X11: error loading cursor icon, falling back on default icon '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + Some(default) + } + Err(default_err) => { + log_cursor_icon_warning(err.context(default_err).context(format!( + "X11: error loading default cursor fallback '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + None + } + } + } + }; - self.cursor_cache.insert(style, cursor); - cursor - } + self.cursor_cache.insert(style, cursor); + cursor + } - fn get_or_create_invisible_cursor(&mut self) -> Option { - if let Some(cursor) = self.invisible_cursor_cache { - return Some(cursor); - } - let cursor = create_invisible_cursor(&self.xcb_connection) - .context("X11: error while creating invisible cursor") - .log_err()?; - self.invisible_cursor_cache = Some(cursor); - Some(cursor) - } + fn get_or_create_invisible_cursor(&mut self) -> Option { + if let Some(cursor) = self.invisible_cursor_cache { + return Some(cursor); + } + let cursor = create_invisible_cursor(&self.xcb_connection) + .context("X11: error while creating invisible cursor") + .log_err()?; + self.invisible_cursor_cache = Some(cursor); + Some(cursor) + } - fn hide_cursor_until_mouse_moves(&mut self) { - if self.cursor_hidden_window.is_some() { - return; - } - let Some(focused_window) = self.mouse_focused_window else { - // No window to apply the per-window invisible cursor to. - return; - }; - let Some(invisible_cursor) = self.get_or_create_invisible_cursor() else { - return; - }; - check_reply( - || "Failed to hide cursor", - self.xcb_connection.change_window_attributes( - focused_window, - &ChangeWindowAttributesAux { - cursor: Some(invisible_cursor), - ..Default::default() - }, - ), - ) - .log_err(); - self.xcb_connection.flush().log_err(); - self.cursor_hidden_window = Some(focused_window); - } + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window else { + // No window to apply the per-window invisible cursor to. + return; + }; + let Some(invisible_cursor) = self.get_or_create_invisible_cursor() else { + return; + }; + check_reply( + || "Failed to hide cursor", + self.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(invisible_cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + self.cursor_hidden_window = Some(focused_window); + } - fn restore_cursor_after_hide(&mut self) { - let Some(hidden_window) = self.cursor_hidden_window.take() else { - return; - }; - let style = self - .cursor_styles - .get(&hidden_window) - .copied() - .unwrap_or(CursorStyle::Arrow); - let Some(cursor) = self.get_cursor_icon(style) else { - log::warn!( - "X11: no cursor icon available to restore {:?} after hide; cursor may stay invisible", - style - ); - return; - }; - check_reply( - || "Failed to restore cursor style after hide", - self.xcb_connection.change_window_attributes( - hidden_window, - &ChangeWindowAttributesAux { - cursor: Some(cursor), - ..Default::default() - }, - ), - ) - .log_err(); - self.xcb_connection.flush().log_err(); - } + fn restore_cursor_after_hide(&mut self) { + let Some(hidden_window) = self.cursor_hidden_window.take() else { + return; + }; + let style = self + .cursor_styles + .get(&hidden_window) + .copied() + .unwrap_or(CursorStyle::Arrow); + let Some(cursor) = self.get_cursor_icon(style) else { + log::warn!( + "X11: no cursor icon available to restore {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + check_reply( + || "Failed to restore cursor style after hide", + self.xcb_connection.change_window_attributes( + hidden_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + } } // Adapted from: // https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { - if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { - return Duration::from_millis(16); - } + if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { + return Duration::from_millis(16); + } - let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); - let micros = 1_000_000_000 / millihertz; - log::info!("Refreshing every {}ms", micros / 1_000); - Duration::from_micros(micros) + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing every {}ms", micros / 1_000); + Duration::from_micros(micros) } fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { - value.integral as f32 + value.frac as f32 / u32::MAX as f32 + value.integral as f32 + value.frac as f32 / u32::MAX as f32 } fn detect_compositor_gpu( - xcb_connection: &XCBConnection, - screen: &xproto::Screen, + xcb_connection: &XCBConnection, + screen: &xproto::Screen, ) -> Option { - use std::os::fd::AsRawFd; - use std::os::unix::fs::MetadataExt; + use std::os::fd::AsRawFd; + use std::os::unix::fs::MetadataExt; - xcb_connection - .extension_information(dri3::X11_EXTENSION_NAME) - .ok()??; + xcb_connection + .extension_information(dri3::X11_EXTENSION_NAME) + .ok()??; - let reply = dri3::open(xcb_connection, screen.root, 0) - .ok()? - .reply() - .ok()?; - let fd = reply.device_fd; + let reply = dri3::open(xcb_connection, screen.root, 0) + .ok()? + .reply() + .ok()?; + let fd = reply.device_fd; - let path = format!("/proc/self/fd/{}", fd.as_raw_fd()); - let metadata = std::fs::metadata(&path).ok()?; + let path = format!("/proc/self/fd/{}", fd.as_raw_fd()); + let metadata = std::fs::metadata(&path).ok()?; - crate::linux::compositor_gpu_hint_from_dev_t(metadata.rdev()) + crate::linux::compositor_gpu_hint_from_dev_t(metadata.rdev()) } fn check_compositor_present(xcb_connection: &XCBConnection, root: xproto::Window) -> bool { - // Method 1: Check for _NET_WM_CM_S{root} - let atom_name = format!("_NET_WM_CM_S{}", root); - let atom1 = get_reply( - || format!("Failed to intern {atom_name}"), - xcb_connection.intern_atom(false, atom_name.as_bytes()), - ); - let method1 = match atom1.log_with_level(Level::Debug) { - Some(reply) if reply.atom != x11rb::NONE => { - let atom = reply.atom; - get_reply( - || format!("Failed to get {atom_name} owner"), - xcb_connection.get_selection_owner(atom), - ) - .map(|reply| reply.owner != 0) - .log_with_level(Level::Debug) - .unwrap_or(false) - } - _ => false, - }; + // Method 1: Check for _NET_WM_CM_S{root} + let atom_name = format!("_NET_WM_CM_S{}", root); + let atom1 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method1 = match atom1.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name} owner"), + xcb_connection.get_selection_owner(atom), + ) + .map(|reply| reply.owner != 0) + .log_with_level(Level::Debug) + .unwrap_or(false) + } + _ => false, + }; - // Method 2: Check for _NET_WM_CM_OWNER - let atom_name = "_NET_WM_CM_OWNER"; - let atom2 = get_reply( - || format!("Failed to intern {atom_name}"), - xcb_connection.intern_atom(false, atom_name.as_bytes()), - ); - let method2 = match atom2.log_with_level(Level::Debug) { - Some(reply) if reply.atom != x11rb::NONE => { - let atom = reply.atom; - get_reply( - || format!("Failed to get {atom_name}"), - xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), - ) - .map(|reply| reply.value_len > 0) - .unwrap_or(false) - } - _ => return false, - }; + // Method 2: Check for _NET_WM_CM_OWNER + let atom_name = "_NET_WM_CM_OWNER"; + let atom2 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method2 = match atom2.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; - // Method 3: Check for _NET_SUPPORTING_WM_CHECK - let atom_name = "_NET_SUPPORTING_WM_CHECK"; - let atom3 = get_reply( - || format!("Failed to intern {atom_name}"), - xcb_connection.intern_atom(false, atom_name.as_bytes()), - ); - let method3 = match atom3.log_with_level(Level::Debug) { - Some(reply) if reply.atom != x11rb::NONE => { - let atom = reply.atom; - get_reply( - || format!("Failed to get {atom_name}"), - xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), - ) - .map(|reply| reply.value_len > 0) - .unwrap_or(false) - } - _ => return false, - }; + // Method 3: Check for _NET_SUPPORTING_WM_CHECK + let atom_name = "_NET_SUPPORTING_WM_CHECK"; + let atom3 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method3 = match atom3.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; - log::debug!( - "Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}", - method1, - method2, - method3 - ); + log::debug!( + "Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}", + method1, + method2, + method3 + ); - method1 || method2 || method3 + method1 || method2 || method3 } fn check_gtk_frame_extents_supported( - xcb_connection: &XCBConnection, - atoms: &XcbAtoms, - root: xproto::Window, + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + root: xproto::Window, ) -> bool { - let Some(supported_atoms) = get_reply( - || "Failed to get _NET_SUPPORTED", - xcb_connection.get_property( - false, - root, - atoms._NET_SUPPORTED, - xproto::AtomEnum::ATOM, - 0, - 1024, - ), - ) - .log_with_level(Level::Debug) else { - return false; - }; + let Some(supported_atoms) = get_reply( + || "Failed to get _NET_SUPPORTED", + xcb_connection.get_property( + false, + root, + atoms._NET_SUPPORTED, + xproto::AtomEnum::ATOM, + 0, + 1024, + ), + ) + .log_with_level(Level::Debug) else { + return false; + }; - let supported_atom_ids: Vec = supported_atoms - .value - .chunks_exact(4) - .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) - .collect(); + let supported_atom_ids: Vec = supported_atoms + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect(); - supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) + supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) } fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { - atom == atoms.TEXT - || atom == atoms.STRING - || atom == atoms.UTF8_STRING - || atom == atoms.TEXT_PLAIN - || atom == atoms.TEXT_PLAIN_UTF8 - || atom == atoms.TextUriList + atom == atoms.TEXT + || atom == atoms.STRING + || atom == atoms.UTF8_STRING + || atom == atoms.TEXT_PLAIN + || atom == atoms.TEXT_PLAIN_UTF8 + || atom == atoms.TextUriList } fn xdnd_get_supported_atom( - xcb_connection: &XCBConnection, - supported_atoms: &XcbAtoms, - target: xproto::Window, + xcb_connection: &XCBConnection, + supported_atoms: &XcbAtoms, + target: xproto::Window, ) -> u32 { - if let Some(reply) = get_reply( - || "Failed to get XDnD supported atoms", - xcb_connection.get_property( - false, - target, - supported_atoms.XdndTypeList, - AtomEnum::ANY, - 0, - 1024, - ), - ) - .log_with_level(Level::Warn) - && let Some(atoms) = reply.value32() - { - for atom in atoms { - if xdnd_is_atom_supported(atom, supported_atoms) { - return atom; - } - } - } - 0 + if let Some(reply) = get_reply( + || "Failed to get XDnD supported atoms", + xcb_connection.get_property( + false, + target, + supported_atoms.XdndTypeList, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_with_level(Level::Warn) + && let Some(atoms) = reply.value32() + { + for atom in atoms { + if xdnd_is_atom_supported(atom, supported_atoms) { + return atom; + } + } + } + 0 } fn xdnd_send_finished( - xcb_connection: &XCBConnection, - atoms: &XcbAtoms, - source: xproto::Window, - target: xproto::Window, + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, ) { - let message = ClientMessageEvent { - format: 32, - window: target, - type_: atoms.XdndFinished, - data: ClientMessageData::from([source, 1, atoms.XdndActionCopy, 0, 0]), - sequence: 0, - response_type: xproto::CLIENT_MESSAGE_EVENT, - }; - check_reply( - || "Failed to send XDnD finished event", - xcb_connection.send_event(false, target, EventMask::default(), message), - ) - .log_err(); - xcb_connection.flush().log_err(); + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndFinished, + data: ClientMessageData::from([source, 1, atoms.XdndActionCopy, 0, 0]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD finished event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); } fn xdnd_send_status( - xcb_connection: &XCBConnection, - atoms: &XcbAtoms, - source: xproto::Window, - target: xproto::Window, - action: u32, + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, + action: u32, ) { - let message = ClientMessageEvent { - format: 32, - window: target, - type_: atoms.XdndStatus, - data: ClientMessageData::from([source, 1, 0, 0, action]), - sequence: 0, - response_type: xproto::CLIENT_MESSAGE_EVENT, - }; - check_reply( - || "Failed to send XDnD status event", - xcb_connection.send_event(false, target, EventMask::default(), message), - ) - .log_err(); - xcb_connection.flush().log_err(); + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndStatus, + data: ClientMessageData::from([source, 1, 0, 0, action]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD status event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); } /// Recomputes `pointer_device_states` by querying all pointer devices. /// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used. fn current_pointer_device_states( - xcb_connection: &XCBConnection, - scroll_values_to_preserve: &BTreeMap, + xcb_connection: &XCBConnection, + scroll_values_to_preserve: &BTreeMap, ) -> Option> { - let devices_query_result = get_reply( - || "Failed to query XInput devices", - xcb_connection.xinput_xi_query_device(XINPUT_ALL_DEVICES), - ) - .log_err()?; + let devices_query_result = get_reply( + || "Failed to query XInput devices", + xcb_connection.xinput_xi_query_device(XINPUT_ALL_DEVICES), + ) + .log_err()?; - let mut pointer_device_states = BTreeMap::new(); - pointer_device_states.extend( - devices_query_result - .infos - .iter() - .filter(|info| is_pointer_device(info.type_)) - .filter_map(|info| { - let scroll_data = info - .classes - .iter() - .filter_map(|class| class.data.as_scroll()) - .copied() - .rev() - .collect::>(); - let old_state = scroll_values_to_preserve.get(&info.deviceid); - let old_horizontal = old_state.map(|state| &state.horizontal); - let old_vertical = old_state.map(|state| &state.vertical); - let horizontal = scroll_data - .iter() - .find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL) - .map(|data| scroll_data_to_axis_state(data, old_horizontal)); - let vertical = scroll_data - .iter() - .find(|data| data.scroll_type == xinput::ScrollType::VERTICAL) - .map(|data| scroll_data_to_axis_state(data, old_vertical)); - if horizontal.is_none() && vertical.is_none() { - None - } else { - Some(( - info.deviceid, - PointerDeviceState { - horizontal: horizontal.unwrap_or_else(Default::default), - vertical: vertical.unwrap_or_else(Default::default), - }, - )) - } - }), - ); - if pointer_device_states.is_empty() { - log::error!("Found no xinput mouse pointers."); - } - Some(pointer_device_states) + let mut pointer_device_states = BTreeMap::new(); + pointer_device_states.extend( + devices_query_result + .infos + .iter() + .filter(|info| is_pointer_device(info.type_)) + .filter_map(|info| { + let scroll_data = info + .classes + .iter() + .filter_map(|class| class.data.as_scroll()) + .copied() + .rev() + .collect::>(); + let old_state = scroll_values_to_preserve.get(&info.deviceid); + let old_horizontal = old_state.map(|state| &state.horizontal); + let old_vertical = old_state.map(|state| &state.vertical); + let horizontal = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL) + .map(|data| scroll_data_to_axis_state(data, old_horizontal)); + let vertical = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::VERTICAL) + .map(|data| scroll_data_to_axis_state(data, old_vertical)); + if horizontal.is_none() && vertical.is_none() { + None + } else { + Some(( + info.deviceid, + PointerDeviceState { + horizontal: horizontal.unwrap_or_else(Default::default), + vertical: vertical.unwrap_or_else(Default::default), + }, + )) + } + }), + ); + if pointer_device_states.is_empty() { + log::error!("Found no xinput mouse pointers."); + } + Some(pointer_device_states) } /// Returns true if the device is a pointer device. Does not include pointer device groups. fn is_pointer_device(type_: xinput::DeviceType) -> bool { - type_ == xinput::DeviceType::SLAVE_POINTER + type_ == xinput::DeviceType::SLAVE_POINTER } fn scroll_data_to_axis_state( - data: &xinput::DeviceClassDataScroll, - old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>, + data: &xinput::DeviceClassDataScroll, + old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>, ) -> ScrollAxisState { - ScrollAxisState { - valuator_number: Some(data.number), - multiplier: SCROLL_LINES / fp3232_to_f32(data.increment), - scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value), - } + ScrollAxisState { + valuator_number: Some(data.number), + multiplier: SCROLL_LINES / fp3232_to_f32(data.increment), + scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value), + } } fn reset_all_pointer_device_scroll_positions( - pointer_device_states: &mut BTreeMap, + pointer_device_states: &mut BTreeMap, ) { - pointer_device_states - .iter_mut() - .for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state)); + pointer_device_states + .iter_mut() + .for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state)); } fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) { - pointer.horizontal.scroll_value = None; - pointer.vertical.scroll_value = None; + pointer.horizontal.scroll_value = None; + pointer.vertical.scroll_value = None; } /// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present. fn get_scroll_delta_and_update_state( - pointer: &mut PointerDeviceState, - event: &xinput::MotionEvent, + pointer: &mut PointerDeviceState, + event: &xinput::MotionEvent, ) -> Option> { - let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal); - let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical); - if delta_x.is_some() || delta_y.is_some() { - Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0))) - } else { - None - } + let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal); + let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical); + if delta_x.is_some() || delta_y.is_some() { + Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0))) + } else { + None + } } fn get_axis_scroll_delta_and_update_state( - event: &xinput::MotionEvent, - axis: &mut ScrollAxisState, + event: &xinput::MotionEvent, + axis: &mut ScrollAxisState, ) -> Option { - let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?; - if let Some(axis_value) = event.axisvalues.get(axis_index) { - let new_scroll = fp3232_to_f32(*axis_value); - let delta_scroll = axis - .scroll_value - .map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier); - axis.scroll_value = Some(new_scroll); - delta_scroll - } else { - log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly."); - None - } + let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?; + if let Some(axis_value) = event.axisvalues.get(axis_index) { + let new_scroll = fp3232_to_f32(*axis_value); + let delta_scroll = axis + .scroll_value + .map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier); + axis.scroll_value = Some(new_scroll); + delta_scroll + } else { + log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly."); + None + } } fn make_scroll_wheel_event( - position: Point, - scroll_delta: Point, - modifiers: Modifiers, + position: Point, + scroll_delta: Point, + modifiers: Modifiers, ) -> gpui::ScrollWheelEvent { - // When shift is held down, vertical scrolling turns into horizontal scrolling. - let delta = if modifiers.shift { - Point { - x: scroll_delta.y, - y: 0.0, - } - } else { - scroll_delta - }; - gpui::ScrollWheelEvent { - position, - delta: ScrollDelta::Lines(delta), - modifiers, - touch_phase: TouchPhase::default(), - } + // When shift is held down, vertical scrolling turns into horizontal scrolling. + let delta = if modifiers.shift { + Point { + x: scroll_delta.y, + y: 0.0, + } + } else { + scroll_delta + }; + gpui::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(delta), + modifiers, + touch_phase: TouchPhase::default(), + } } fn create_invisible_cursor( - connection: &XCBConnection, + connection: &XCBConnection, ) -> anyhow::Result { - let empty_pixmap = connection.generate_id()?; - let root = connection.setup().roots[0].root; - connection.create_pixmap(1, empty_pixmap, root, 1, 1)?; + let empty_pixmap = connection.generate_id()?; + let root = connection.setup().roots[0].root; + connection.create_pixmap(1, empty_pixmap, root, 1, 1)?; - let cursor = connection.generate_id()?; - connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?; + let cursor = connection.generate_id()?; + connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?; - connection.free_pixmap(empty_pixmap)?; + connection.free_pixmap(empty_pixmap)?; - xcb_flush(connection); - Ok(cursor) + xcb_flush(connection); + Ok(cursor) } enum DpiMode { - Randr, - Scale(f32), - NotSet, + Randr, + Scale(f32), + NotSet, } fn get_scale_factor( - connection: &XCBConnection, - resource_database: &Database, - screen_index: usize, + connection: &XCBConnection, + resource_database: &Database, + screen_index: usize, ) -> f32 { - let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) - .ok() - .map(|var| { - if var.to_lowercase() == "randr" { - DpiMode::Randr - } else if let Ok(scale) = var.parse::() { - if valid_scale_factor(scale) { - DpiMode::Scale(scale) - } else { - panic!( - "`{}` must be a positive normal number or `randr`. Got `{}`", - GPUI_X11_SCALE_FACTOR_ENV, var - ); - } - } else if var.is_empty() { - DpiMode::NotSet - } else { - panic!( - "`{}` must be a positive number or `randr`. Got `{}`", - GPUI_X11_SCALE_FACTOR_ENV, var - ); - } - }) - .unwrap_or(DpiMode::NotSet); + let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) + .ok() + .map(|var| { + if var.to_lowercase() == "randr" { + DpiMode::Randr + } else if let Ok(scale) = var.parse::() { + if valid_scale_factor(scale) { + DpiMode::Scale(scale) + } else { + panic!( + "`{}` must be a positive normal number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + } else if var.is_empty() { + DpiMode::NotSet + } else { + panic!( + "`{}` must be a positive number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + }) + .unwrap_or(DpiMode::NotSet); - match env_dpi { - DpiMode::Scale(scale) => { - log::info!( - "Using scale factor from {}: {}", - GPUI_X11_SCALE_FACTOR_ENV, - scale - ); - return scale; - } - DpiMode::Randr => { - if let Some(scale) = get_randr_scale_factor(connection, screen_index) { - log::info!( - "Using RandR scale factor from {}=randr: {}", - GPUI_X11_SCALE_FACTOR_ENV, - scale - ); - return scale; - } - log::warn!("Failed to calculate RandR scale factor, falling back to default"); - return 1.0; - } - DpiMode::NotSet => {} - } + match env_dpi { + DpiMode::Scale(scale) => { + log::info!( + "Using scale factor from {}: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + DpiMode::Randr => { + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!( + "Using RandR scale factor from {}=randr: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + log::warn!("Failed to calculate RandR scale factor, falling back to default"); + return 1.0; + } + DpiMode::NotSet => {} + } - // TODO: Use scale factor from XSettings here + // TODO: Use scale factor from XSettings here - if let Some(dpi) = resource_database - .get_value::("Xft.dpi", "Xft.dpi") - .ok() - .flatten() - { - let scale = dpi / 96.0; // base dpi - log::info!("Using scale factor from Xft.dpi: {}", scale); - return scale; - } + if let Some(dpi) = resource_database + .get_value::("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + { + let scale = dpi / 96.0; // base dpi + log::info!("Using scale factor from Xft.dpi: {}", scale); + return scale; + } - if let Some(scale) = get_randr_scale_factor(connection, screen_index) { - log::info!("Using RandR scale factor: {}", scale); - return scale; - } + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!("Using RandR scale factor: {}", scale); + return scale; + } - log::info!("Using default scale factor: 1.0"); - 1.0 + log::info!("Using default scale factor: 1.0"); + 1.0 } fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option { - let root = connection.setup().roots.get(screen_index)?.root; + let root = connection.setup().roots.get(screen_index)?.root; - let version_cookie = connection.randr_query_version(1, 6).ok()?; - let version_reply = version_cookie.reply().ok()?; - if version_reply.major_version < 1 - || (version_reply.major_version == 1 && version_reply.minor_version < 5) - { - return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 - } + let version_cookie = connection.randr_query_version(1, 6).ok()?; + let version_reply = version_cookie.reply().ok()?; + if version_reply.major_version < 1 + || (version_reply.major_version == 1 && version_reply.minor_version < 5) + { + return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 + } - let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only - let monitors_reply = monitors_cookie.reply().ok()?; + let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only + let monitors_reply = monitors_cookie.reply().ok()?; - let mut fallback_scale: Option = None; - for monitor in monitors_reply.monitors { - if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { - continue; - } - let scale_factor = get_dpi_factor( - (monitor.width as u32, monitor.height as u32), - ( - monitor.width_in_millimeters as u64, - monitor.height_in_millimeters as u64, - ), - ); - if monitor.primary { - return Some(scale_factor); - } else if fallback_scale.is_none() { - fallback_scale = Some(scale_factor); - } - } + let mut fallback_scale: Option = None; + for monitor in monitors_reply.monitors { + if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { + continue; + } + let scale_factor = get_dpi_factor( + (monitor.width as u32, monitor.height as u32), + ( + monitor.width_in_millimeters as u64, + monitor.height_in_millimeters as u64, + ), + ); + if monitor.primary { + return Some(scale_factor); + } else if fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } - fallback_scale + fallback_scale } fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option { - let primary_cookie = connection.randr_get_output_primary(root).ok()?; - let primary_reply = primary_cookie.reply().ok()?; - let primary_output = primary_reply.output; + let primary_cookie = connection.randr_get_output_primary(root).ok()?; + let primary_reply = primary_cookie.reply().ok()?; + let primary_output = primary_reply.output; - let primary_output_cookie = connection - .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) - .ok()?; - let primary_output_info = primary_output_cookie.reply().ok()?; + let primary_output_cookie = connection + .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) + .ok()?; + let primary_output_info = primary_output_cookie.reply().ok()?; - // try primary - if primary_output_info.connection == randr::Connection::CONNECTED - && primary_output_info.mm_width > 0 - && primary_output_info.mm_height > 0 - && primary_output_info.crtc != 0 - { - let crtc_cookie = connection - .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) - .ok()?; - let crtc_info = crtc_cookie.reply().ok()?; + // try primary + if primary_output_info.connection == randr::Connection::CONNECTED + && primary_output_info.mm_width > 0 + && primary_output_info.mm_height > 0 + && primary_output_info.crtc != 0 + { + let crtc_cookie = connection + .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) + .ok()?; + let crtc_info = crtc_cookie.reply().ok()?; - if crtc_info.width > 0 && crtc_info.height > 0 { - let scale_factor = get_dpi_factor( - (crtc_info.width as u32, crtc_info.height as u32), - ( - primary_output_info.mm_width as u64, - primary_output_info.mm_height as u64, - ), - ); - return Some(scale_factor); - } - } + if crtc_info.width > 0 && crtc_info.height > 0 { + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + ( + primary_output_info.mm_width as u64, + primary_output_info.mm_height as u64, + ), + ); + return Some(scale_factor); + } + } - // fallback: full scan - let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; - let screen_resources = resources_cookie.reply().ok()?; + // fallback: full scan + let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; + let screen_resources = resources_cookie.reply().ok()?; - let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); - for &crtc in &screen_resources.crtcs { - if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { - crtc_cookies.push((crtc, cookie)); - } - } + let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); + for &crtc in &screen_resources.crtcs { + if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { + crtc_cookies.push((crtc, cookie)); + } + } - let mut crtc_infos: HashMap = HashMap::default(); - let mut valid_outputs: HashSet = HashSet::new(); - for (crtc, cookie) in crtc_cookies { - if let Ok(reply) = cookie.reply() - && reply.width > 0 - && reply.height > 0 - && !reply.outputs.is_empty() - { - crtc_infos.insert(crtc, reply.clone()); - valid_outputs.extend(&reply.outputs); - } - } + let mut crtc_infos: HashMap = HashMap::default(); + let mut valid_outputs: HashSet = HashSet::new(); + for (crtc, cookie) in crtc_cookies { + if let Ok(reply) = cookie.reply() + && reply.width > 0 + && reply.height > 0 + && !reply.outputs.is_empty() + { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); + } + } - if valid_outputs.is_empty() { - return None; - } + if valid_outputs.is_empty() { + return None; + } - let mut output_cookies = Vec::with_capacity(valid_outputs.len()); - for &output in &valid_outputs { - if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { - output_cookies.push((output, cookie)); - } - } - let mut output_infos: HashMap = HashMap::default(); - for (output, cookie) in output_cookies { - if let Ok(reply) = cookie.reply() { - output_infos.insert(output, reply); - } - } + let mut output_cookies = Vec::with_capacity(valid_outputs.len()); + for &output in &valid_outputs { + if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { + output_cookies.push((output, cookie)); + } + } + let mut output_infos: HashMap = HashMap::default(); + for (output, cookie) in output_cookies { + if let Ok(reply) = cookie.reply() { + output_infos.insert(output, reply); + } + } - let mut fallback_scale: Option = None; - for crtc_info in crtc_infos.values() { - for &output in &crtc_info.outputs { - if let Some(output_info) = output_infos.get(&output) { - if output_info.connection != randr::Connection::CONNECTED { - continue; - } + let mut fallback_scale: Option = None; + for crtc_info in crtc_infos.values() { + for &output in &crtc_info.outputs { + if let Some(output_info) = output_infos.get(&output) { + if output_info.connection != randr::Connection::CONNECTED { + continue; + } - if output_info.mm_width == 0 || output_info.mm_height == 0 { - continue; - } + if output_info.mm_width == 0 || output_info.mm_height == 0 { + continue; + } - let scale_factor = get_dpi_factor( - (crtc_info.width as u32, crtc_info.height as u32), - (output_info.mm_width as u64, output_info.mm_height as u64), - ); + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + (output_info.mm_width as u64, output_info.mm_height as u64), + ); - if output != primary_output && fallback_scale.is_none() { - fallback_scale = Some(scale_factor); - } - } - } - } + if output != primary_output && fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + } + } - fallback_scale + fallback_scale } fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 { - let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm - const MM_PER_INCH: f64 = 25.4; - const BASE_DPI: f64 = 96.0; - const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 - const MIN_SCALE: f64 = 1.0; - const MAX_SCALE: f64 = 20.0; + const MM_PER_INCH: f64 = 25.4; + const BASE_DPI: f64 = 96.0; + const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 + const MIN_SCALE: f64 = 1.0; + const MAX_SCALE: f64 = 20.0; - let dpi_factor = - ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); + let dpi_factor = + ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); - let validated_factor = if dpi_factor <= MAX_SCALE { - dpi_factor - } else { - MIN_SCALE - }; + let validated_factor = if dpi_factor <= MAX_SCALE { + dpi_factor + } else { + MIN_SCALE + }; - if valid_scale_factor(validated_factor as f32) { - validated_factor as f32 - } else { - log::warn!( - "Calculated DPI factor {} is invalid, using 1.0", - validated_factor - ); - 1.0 - } + if valid_scale_factor(validated_factor as f32) { + validated_factor as f32 + } else { + log::warn!( + "Calculated DPI factor {} is invalid, using 1.0", + validated_factor + ); + 1.0 + } } #[inline] fn valid_scale_factor(scale_factor: f32) -> bool { - scale_factor.is_sign_positive() && scale_factor.is_normal() + scale_factor.is_sign_positive() && scale_factor.is_normal() } #[inline] fn xkb_state_for_key_event(xkb: &xkbc::State, event_state: xproto::KeyButMask) -> xkbc::State { - let keymap = xkb.get_keymap(); - let mut key_event_state = xkbc::State::new(&keymap); + let keymap = xkb.get_keymap(); + let mut key_event_state = xkbc::State::new(&keymap); - let latched_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LATCHED); - let locked_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LOCKED); - let active_modifier_mask: xkbc::ModMask = u16::from( - event_state - & (xproto::KeyButMask::SHIFT - | xproto::KeyButMask::LOCK - | xproto::KeyButMask::CONTROL - | xproto::KeyButMask::MOD1 - | xproto::KeyButMask::MOD2 - | xproto::KeyButMask::MOD3 - | xproto::KeyButMask::MOD4 - | xproto::KeyButMask::MOD5), - ) - .into(); - let depressed_modifiers = active_modifier_mask & !(latched_modifiers | locked_modifiers); + let latched_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LATCHED); + let locked_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LOCKED); + let active_modifier_mask: xkbc::ModMask = u16::from( + event_state + & (xproto::KeyButMask::SHIFT + | xproto::KeyButMask::LOCK + | xproto::KeyButMask::CONTROL + | xproto::KeyButMask::MOD1 + | xproto::KeyButMask::MOD2 + | xproto::KeyButMask::MOD3 + | xproto::KeyButMask::MOD4 + | xproto::KeyButMask::MOD5), + ) + .into(); + let depressed_modifiers = active_modifier_mask & !(latched_modifiers | locked_modifiers); - key_event_state.update_mask( - depressed_modifiers, - latched_modifiers, - locked_modifiers, - xkb.serialize_layout(xkbc::STATE_LAYOUT_DEPRESSED), - xkb.serialize_layout(xkbc::STATE_LAYOUT_LATCHED), - xkb.serialize_layout(xkbc::STATE_LAYOUT_LOCKED), - ); + key_event_state.update_mask( + depressed_modifiers, + latched_modifiers, + locked_modifiers, + xkb.serialize_layout(xkbc::STATE_LAYOUT_DEPRESSED), + xkb.serialize_layout(xkbc::STATE_LAYOUT_LATCHED), + xkb.serialize_layout(xkbc::STATE_LAYOUT_LOCKED), + ); - key_event_state + key_event_state } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn test_keymap(layouts: &str) -> xkbc::Keymap { - test_keymap_with_variant(layouts, "") - } + fn test_keymap(layouts: &str) -> xkbc::Keymap { + test_keymap_with_variant(layouts, "") + } - fn test_keymap_with_variant(layouts: &str, variant: &str) -> xkbc::Keymap { - let context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); - xkbc::Keymap::new_from_names( - &context, - "", - "pc105", - layouts, - variant, - None, - xkbc::COMPILE_NO_FLAGS, - ) - .expect("test keymap should compile") - } + fn test_keymap_with_variant(layouts: &str, variant: &str) -> xkbc::Keymap { + let context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + xkbc::Keymap::new_from_names( + &context, + "", + "pc105", + layouts, + variant, + None, + xkbc::COMPILE_NO_FLAGS, + ) + .expect("test keymap should compile") + } - // Returns a state where the second layout is active via a temporary - // mechanism (holding a key down or one-shot), not a permanent toggle. - fn state_with_non_locked_layout(keymap: &xkbc::Keymap) -> xkbc::State { - let mut depressed_layout_state = xkbc::State::new(keymap); - depressed_layout_state.update_mask(0, 0, 0, 1, 0, 0); - if depressed_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { - return depressed_layout_state; - } + // Returns a state where the second layout is active via a temporary + // mechanism (holding a key down or one-shot), not a permanent toggle. + fn state_with_non_locked_layout(keymap: &xkbc::Keymap) -> xkbc::State { + let mut depressed_layout_state = xkbc::State::new(keymap); + depressed_layout_state.update_mask(0, 0, 0, 1, 0, 0); + if depressed_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { + return depressed_layout_state; + } - let mut latched_layout_state = xkbc::State::new(keymap); - latched_layout_state.update_mask(0, 0, 0, 0, 1, 0); - if latched_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { - return latched_layout_state; - } + let mut latched_layout_state = xkbc::State::new(keymap); + latched_layout_state.update_mask(0, 0, 0, 0, 1, 0); + if latched_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { + return latched_layout_state; + } - panic!("test keymap should support a non-locked secondary layout"); - } + panic!("test keymap should support a non-locked secondary layout"); + } - #[test] - fn key_event_state_uses_event_modifiers_without_mutating_server_state() { - let keymap = test_keymap("us"); - let server_state = xkbc::State::new(&keymap); - // The "9" key on a US keyboard. - let keycode = keymap - .key_by_name("AE09") - .expect("test key should exist in the keymap"); + #[test] + fn key_event_state_uses_event_modifiers_without_mutating_server_state() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "9" key on a US keyboard. + let keycode = keymap + .key_by_name("AE09") + .expect("test key should exist in the keymap"); - // Simulate pressing Shift+9 (which should produce "("). - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); - let keystroke = keystroke_from_xkb( - &key_event_state, - modifiers_from_state(xproto::KeyButMask::SHIFT), - keycode, - ); + // Simulate pressing Shift+9 (which should produce "("). + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + let keystroke = keystroke_from_xkb( + &key_event_state, + modifiers_from_state(xproto::KeyButMask::SHIFT), + keycode, + ); - // Assert Shift+9 produces "(" on US layout. - assert_eq!(keystroke.key, "("); - assert_eq!(keystroke.key_char.as_deref(), Some("(")); - // Assert the long-lived server state was not mutated by the key event. - assert_eq!(server_state.key_get_utf8(keycode), "9"); - } + // Assert Shift+9 produces "(" on US layout. + assert_eq!(keystroke.key, "("); + assert_eq!(keystroke.key_char.as_deref(), Some("(")); + // Assert the long-lived server state was not mutated by the key event. + assert_eq!(server_state.key_get_utf8(keycode), "9"); + } - #[test] - fn key_event_state_ignores_pointer_button_bits() { - let keymap = test_keymap("us"); - let server_state = xkbc::State::new(&keymap); - // The "9" key on a US keyboard. - let keycode = keymap - .key_by_name("AE09") - .expect("test key should exist in the keymap"); + #[test] + fn key_event_state_ignores_pointer_button_bits() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "9" key on a US keyboard. + let keycode = keymap + .key_by_name("AE09") + .expect("test key should exist in the keymap"); - // Simulate Shift held down. - let shifted_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); - // Simulate Shift held down while also clicking the left mouse button. - let shifted_with_button_state = xkb_state_for_key_event( - &server_state, - xproto::KeyButMask::SHIFT | xproto::KeyButMask::BUTTON1, - ); + // Simulate Shift held down. + let shifted_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + // Simulate Shift held down while also clicking the left mouse button. + let shifted_with_button_state = xkb_state_for_key_event( + &server_state, + xproto::KeyButMask::SHIFT | xproto::KeyButMask::BUTTON1, + ); - // Assert the mouse button has no effect on modifier state. - assert_eq!( - shifted_with_button_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), - shifted_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE) - ); - // Assert both cases produce the same character. - assert_eq!( - shifted_with_button_state.key_get_utf8(keycode), - shifted_state.key_get_utf8(keycode) - ); - } + // Assert the mouse button has no effect on modifier state. + assert_eq!( + shifted_with_button_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), + shifted_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE) + ); + // Assert both cases produce the same character. + assert_eq!( + shifted_with_button_state.key_get_utf8(keycode), + shifted_state.key_get_utf8(keycode) + ); + } - #[test] - fn key_event_state_preserves_non_locked_layout_components() { - // US + Russian dual-layout keyboard. - let keymap = test_keymap("us,ru"); - // Simulate the Russian layout being active via a temporary layout - // switch (holding a key), not a permanent toggle. - let server_state = state_with_non_locked_layout(&keymap); - // The "Q" key position, which produces a Cyrillic character in Russian layout. - let keycode = keymap - .key_by_name("AD01") - .expect("test key should exist in the keymap"); + #[test] + fn key_event_state_preserves_non_locked_layout_components() { + // US + Russian dual-layout keyboard. + let keymap = test_keymap("us,ru"); + // Simulate the Russian layout being active via a temporary layout + // switch (holding a key), not a permanent toggle. + let server_state = state_with_non_locked_layout(&keymap); + // The "Q" key position, which produces a Cyrillic character in Russian layout. + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); - let expected_text = server_state.key_get_utf8(keycode); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + let expected_text = server_state.key_get_utf8(keycode); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); - // Assert the temporary layout switch is preserved. - assert_eq!( - key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), - server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) - ); - // Assert the key produces the same character as expected from the - // Russian layout. - assert_eq!(key_event_state.key_get_utf8(keycode), expected_text); - } + // Assert the temporary layout switch is preserved. + assert_eq!( + key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), + server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) + ); + // Assert the key produces the same character as expected from the + // Russian layout. + assert_eq!(key_event_state.key_get_utf8(keycode), expected_text); + } - // https://github.com/zed-industries/zed/issues/14282 - #[test] - fn capslock_toggle_produces_uppercase() { - let keymap = test_keymap("us"); - let mut server_state = xkbc::State::new(&keymap); - // The "A" key position on a US keyboard. - let keycode = keymap - .key_by_name("AC01") - .expect("'a' key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn capslock_toggle_produces_uppercase() { + let keymap = test_keymap("us"); + let mut server_state = xkbc::State::new(&keymap); + // The "A" key position on a US keyboard. + let keycode = keymap + .key_by_name("AC01") + .expect("'a' key should exist in the keymap"); - // Simulate the user having toggled CapsLock on (it's now permanently - // active until pressed again). - let lock_mod = u16::from(xproto::KeyButMask::LOCK) as xkbc::ModMask; - server_state.update_mask(0, 0, lock_mod, 0, 0, 0); + // Simulate the user having toggled CapsLock on (it's now permanently + // active until pressed again). + let lock_mod = u16::from(xproto::KeyButMask::LOCK) as xkbc::ModMask; + server_state.update_mask(0, 0, lock_mod, 0, 0, 0); - // Simulate pressing the "a" key while CapsLock is on. - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::LOCK); + // Simulate pressing the "a" key while CapsLock is on. + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::LOCK); - // Assert CapsLock is treated as a toggle (locked), not as a held key - // (depressed). This distinction matters because XKB only applies - // capitalization when CapsLock is in the "locked" state. - assert_eq!( - key_event_state.serialize_mods(xkbc::STATE_MODS_LOCKED) & lock_mod, - lock_mod, - ); - // Assert typing "a" with CapsLock on produces "A". - assert_eq!(key_event_state.key_get_utf8(keycode), "A"); - } + // Assert CapsLock is treated as a toggle (locked), not as a held key + // (depressed). This distinction matters because XKB only applies + // capitalization when CapsLock is in the "locked" state. + assert_eq!( + key_event_state.serialize_mods(xkbc::STATE_MODS_LOCKED) & lock_mod, + lock_mod, + ); + // Assert typing "a" with CapsLock on produces "A". + assert_eq!(key_event_state.key_get_utf8(keycode), "A"); + } - // https://github.com/zed-industries/zed/issues/14282 - #[test] - fn neo2_level3_via_capslock_produces_ellipsis() { - // Neo 2 is a German keyboard layout that repurposes CapsLock as a - // "level 3" modifier key for accessing additional characters. - let keymap = test_keymap_with_variant("de", "neo"); - let server_state = xkbc::State::new(&keymap); - // The key in the "Q" position, which produces "x" on Neo 2 base layer. - let keycode = keymap - .key_by_name("AD01") - .expect("test key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn neo2_level3_via_capslock_produces_ellipsis() { + // Neo 2 is a German keyboard layout that repurposes CapsLock as a + // "level 3" modifier key for accessing additional characters. + let keymap = test_keymap_with_variant("de", "neo"); + let server_state = xkbc::State::new(&keymap); + // The key in the "Q" position, which produces "x" on Neo 2 base layer. + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); - // Simulate holding CapsLock, which in Neo 2 activates the "level 3" - // layer (mapped to the Mod5 modifier internally). - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); + // Simulate holding CapsLock, which in Neo 2 activates the "level 3" + // layer (mapped to the Mod5 modifier internally). + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); - // Assert holding CapsLock + pressing the "x" key produces "..." - // (ellipsis), which is the level 3 character on that key in Neo 2. - assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); - } + // Assert holding CapsLock + pressing the "x" key produces "..." + // (ellipsis), which is the level 3 character on that key in Neo 2. + assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); + } - // https://github.com/zed-industries/zed/issues/14282 - #[test] - fn neo2_latched_mod5_preserved() { - // Neo 2 also supports "latching" the level 3 modifier (via Caps+Tab), - // which activates it for only the next keypress and then deactivates. - let keymap = test_keymap_with_variant("de", "neo"); - let mut server_state = xkbc::State::new(&keymap); - let keycode = keymap - .key_by_name("AD01") - .expect("test key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn neo2_latched_mod5_preserved() { + // Neo 2 also supports "latching" the level 3 modifier (via Caps+Tab), + // which activates it for only the next keypress and then deactivates. + let keymap = test_keymap_with_variant("de", "neo"); + let mut server_state = xkbc::State::new(&keymap); + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); - // Simulate the level 3 modifier being latched (one-shot active). - let mod5 = u16::from(xproto::KeyButMask::MOD5) as xkbc::ModMask; - server_state.update_mask(0, mod5, 0, 0, 0, 0); + // Simulate the level 3 modifier being latched (one-shot active). + let mod5 = u16::from(xproto::KeyButMask::MOD5) as xkbc::ModMask; + server_state.update_mask(0, mod5, 0, 0, 0, 0); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); - // Assert the modifier stays classified as "latched" (one-shot) rather - // than being reclassified as "depressed" (held down). This matters - // because latched modifiers auto-deactivate after one keypress. - assert_eq!( - key_event_state.serialize_mods(xkbc::STATE_MODS_LATCHED) & mod5, - mod5, - ); - // Assert the latched level 3 still produces the ellipsis character. - assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); - } + // Assert the modifier stays classified as "latched" (one-shot) rather + // than being reclassified as "depressed" (held down). This matters + // because latched modifiers auto-deactivate after one keypress. + assert_eq!( + key_event_state.serialize_mods(xkbc::STATE_MODS_LATCHED) & mod5, + mod5, + ); + // Assert the latched level 3 still produces the ellipsis character. + assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); + } - // https://github.com/zed-industries/zed/pull/31193 - #[test] - fn german_layout_correct_key_resolution() { - // Standard German keyboard layout. - let keymap = test_keymap("de"); - let server_state = xkbc::State::new(&keymap); - // The "7" key on the number row. - let keycode = keymap - .key_by_name("AE07") - .expect("'7' key should exist in the keymap"); + // https://github.com/zed-industries/zed/pull/31193 + #[test] + fn german_layout_correct_key_resolution() { + // Standard German keyboard layout. + let keymap = test_keymap("de"); + let server_state = xkbc::State::new(&keymap); + // The "7" key on the number row. + let keycode = keymap + .key_by_name("AE07") + .expect("'7' key should exist in the keymap"); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); - // Assert pressing the "7" key on a German layout produces "7". - assert_eq!(key_event_state.key_get_utf8(keycode), "7"); - } + // Assert pressing the "7" key on a German layout produces "7". + assert_eq!(key_event_state.key_get_utf8(keycode), "7"); + } - // https://github.com/zed-industries/zed/issues/26468 - // https://github.com/zed-industries/zed/issues/16667 - #[test] - fn space_works_with_cyrillic_layout_active() { - // US + Russian dual-layout keyboard. - let keymap = test_keymap("us,ru"); - let mut server_state = xkbc::State::new(&keymap); - let space = keymap - .key_by_name("SPCE") - .expect("space key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/26468 + // https://github.com/zed-industries/zed/issues/16667 + #[test] + fn space_works_with_cyrillic_layout_active() { + // US + Russian dual-layout keyboard. + let keymap = test_keymap("us,ru"); + let mut server_state = xkbc::State::new(&keymap); + let space = keymap + .key_by_name("SPCE") + .expect("space key should exist in the keymap"); - // Simulate the user having switched to the Russian layout - // (e.g. via a keyboard shortcut like Super+Space). - server_state.update_mask(0, 0, 0, 0, 0, 1); + // Simulate the user having switched to the Russian layout + // (e.g. via a keyboard shortcut like Super+Space). + server_state.update_mask(0, 0, 0, 0, 0, 1); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); - // Assert the Russian layout is still active after constructing the - // key event state (not accidentally reset to US). - assert_eq!(key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 1); - // Assert pressing space while on the Russian layout still types a space. - assert_eq!(key_event_state.key_get_utf8(space), " "); - } + // Assert the Russian layout is still active after constructing the + // key event state (not accidentally reset to US). + assert_eq!(key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 1); + // Assert pressing space while on the Russian layout still types a space. + assert_eq!(key_event_state.key_get_utf8(space), " "); + } - // https://github.com/zed-industries/zed/issues/40678 - #[test] - fn macro_shift_bracket_produces_brace() { - let keymap = test_keymap("us"); - let server_state = xkbc::State::new(&keymap); - // The "]" key on a US keyboard. - let bracket = keymap - .key_by_name("AD12") - .expect("']' key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/40678 + #[test] + fn macro_shift_bracket_produces_brace() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "]" key on a US keyboard. + let bracket = keymap + .key_by_name("AD12") + .expect("']' key should exist in the keymap"); - // Simulate a keyboard macro (e.g. from a ZMK/QMK firmware keyboard) - // that sends Shift + "]" very rapidly. The modifier state notification - // for Shift hasn't reached us yet, so the server state has no - // modifiers. But the key event itself carries the correct Shift state. - assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + // Simulate a keyboard macro (e.g. from a ZMK/QMK firmware keyboard) + // that sends Shift + "]" very rapidly. The modifier state notification + // for Shift hasn't reached us yet, so the server state has no + // modifiers. But the key event itself carries the correct Shift state. + assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); - // Assert Shift+"]" produces "}" even when the Shift notification - // arrived late. - assert_eq!(key_event_state.key_get_utf8(bracket), "}"); - } + // Assert Shift+"]" produces "}" even when the Shift notification + // arrived late. + assert_eq!(key_event_state.key_get_utf8(bracket), "}"); + } - // https://github.com/zed-industries/zed/issues/49329 - #[test] - fn sequential_key_events_do_not_corrupt_state() { - let keymap = test_keymap("us"); - let server_state = xkbc::State::new(&keymap); + // https://github.com/zed-industries/zed/issues/49329 + #[test] + fn sequential_key_events_do_not_corrupt_state() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); - // Simulate typing "a s d" with spaces in between, all without any - // modifier keys held. - let keys: &[(&str, &str)] = &[ - ("AC01", "a"), - ("SPCE", " "), - ("AC02", "s"), - ("SPCE", " "), - ("AC03", "d"), - ]; + // Simulate typing "a s d" with spaces in between, all without any + // modifier keys held. + let keys: &[(&str, &str)] = &[ + ("AC01", "a"), + ("SPCE", " "), + ("AC02", "s"), + ("SPCE", " "), + ("AC03", "d"), + ]; - for &(key_name, expected_utf8) in keys { - let keycode = keymap - .key_by_name(key_name) - .expect("test key should exist in the keymap"); + for &(key_name, expected_utf8) in keys { + let keycode = keymap + .key_by_name(key_name) + .expect("test key should exist in the keymap"); - let key_event_state = - xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + let key_event_state = + xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); - // Assert each key in the sequence produces the expected character - // (no dropped or garbled input from state corruption). - assert_eq!( - key_event_state.key_get_utf8(keycode), - expected_utf8, - "key {key_name} should produce {expected_utf8:?}", - ); - } + // Assert each key in the sequence produces the expected character + // (no dropped or garbled input from state corruption). + assert_eq!( + key_event_state.key_get_utf8(keycode), + expected_utf8, + "key {key_name} should produce {expected_utf8:?}", + ); + } - // Assert the server state is completely untouched after processing - // all key events. - assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); - assert_eq!(server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 0); - } + // Assert the server state is completely untouched after processing + // all key events. + assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); + assert_eq!(server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 0); + } - // https://github.com/zed-industries/zed/issues/26468 - #[test] - fn space_works_with_czech_layout_active() { - // US + Czech dual-layout keyboard. - let keymap = test_keymap("us,cz"); - let mut server_state = xkbc::State::new(&keymap); - let space = keymap - .key_by_name("SPCE") - .expect("space key should exist in the keymap"); + // https://github.com/zed-industries/zed/issues/26468 + #[test] + fn space_works_with_czech_layout_active() { + // US + Czech dual-layout keyboard. + let keymap = test_keymap("us,cz"); + let mut server_state = xkbc::State::new(&keymap); + let space = keymap + .key_by_name("SPCE") + .expect("space key should exist in the keymap"); - // Simulate the user having switched to the Czech layout. - server_state.update_mask(0, 0, 0, 0, 0, 1); + // Simulate the user having switched to the Czech layout. + server_state.update_mask(0, 0, 0, 0, 0, 1); - let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); - // Assert pressing space while on the Czech layout still types a space. - assert_eq!(key_event_state.key_get_utf8(space), " "); - } + // Assert pressing space while on the Czech layout still types a space. + assert_eq!(key_event_state.key_get_utf8(space), " "); + } } diff --git a/crates/gpui_linux/src/linux/x11/clipboard.rs b/crates/gpui_linux/src/linux/x11/clipboard.rs index fb6cecd904..8c7f908ffb 100644 --- a/crates/gpui_linux/src/linux/x11/clipboard.rs +++ b/crates/gpui_linux/src/linux/x11/clipboard.rs @@ -19,32 +19,32 @@ // https://freedesktop.org/wiki/ClipboardManager/ use std::{ - borrow::Cow, - cell::RefCell, - collections::{HashMap, hash_map::Entry}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread::JoinHandle, - thread_local, - time::{Duration, Instant}, + borrow::Cow, + cell::RefCell, + collections::{HashMap, hash_map::Entry}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::JoinHandle, + thread_local, + time::{Duration, Instant}, }; use parking_lot::{Condvar, Mutex, MutexGuard, RwLock}; use x11rb::{ - COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, - connection::Connection, - protocol::{ - Event, - xproto::{ - Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, - PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, - SelectionRequestEvent, Time, WindowClass, - }, - }, - rust_connection::RustConnection, - wrapper::ConnectionExt as _, + COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, + connection::Connection, + protocol::{ + Event, + xproto::{ + Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, + PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, + SelectionRequestEvent, Time, WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, }; use gpui::{ClipboardItem, Image, ImageFormat, hash}; @@ -55,48 +55,48 @@ type Result = std::result::Result; static CLIPBOARD: Mutex> = parking_lot::const_mutex(None); x11rb::atom_manager! { - pub Atoms: AtomCookies { - CLIPBOARD, - PRIMARY, - SECONDARY, + pub Atoms: AtomCookies { + CLIPBOARD, + PRIMARY, + SECONDARY, - CLIPBOARD_MANAGER, - SAVE_TARGETS, - TARGETS, - ATOM, - INCR, + CLIPBOARD_MANAGER, + SAVE_TARGETS, + TARGETS, + ATOM, + INCR, - UTF8_STRING, - UTF8_MIME_0: b"text/plain;charset=utf-8", - UTF8_MIME_1: b"text/plain;charset=UTF-8", - // Text in ISO Latin-1 encoding - // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 - STRING, - // Text in unknown encoding - // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 - TEXT, - TEXT_MIME_UNKNOWN: b"text/plain", + UTF8_STRING, + UTF8_MIME_0: b"text/plain;charset=utf-8", + UTF8_MIME_1: b"text/plain;charset=UTF-8", + // Text in ISO Latin-1 encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + STRING, + // Text in unknown encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + TEXT, + TEXT_MIME_UNKNOWN: b"text/plain", - // HTML: b"text/html", - // URI_LIST: b"text/uri-list", + // HTML: b"text/html", + // URI_LIST: b"text/uri-list", - PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), - JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), - WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), - GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), - SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), - BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), - TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), - ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), - PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(), - // This is just some random name for the property on our window, into which - // the clipboard owner writes the data we requested. - ARBOARD_CLIPBOARD, - } + PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), + JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), + WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), + GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), + SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), + BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), + TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), + ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), + PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(), + // This is just some random name for the property on our window, into which + // the clipboard owner writes the data we requested. + ARBOARD_CLIPBOARD, + } } thread_local! { - static ATOM_NAME_CACHE: RefCell> = Default::default(); + static ATOM_NAME_CACHE: RefCell> = Default::default(); } // Some clipboard items, like images, may take a very long time to produce a @@ -106,1036 +106,1036 @@ const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); #[derive(Debug, PartialEq, Eq)] enum ManagerHandoverState { - Idle, - InProgress, - Finished, + Idle, + InProgress, + Finished, } struct GlobalClipboard { - inner: Arc, + inner: Arc, - /// Join handle to the thread which serves selection requests. - server_handle: JoinHandle<()>, + /// Join handle to the thread which serves selection requests. + server_handle: JoinHandle<()>, } struct XContext { - conn: RustConnection, - win_id: u32, + conn: RustConnection, + win_id: u32, } struct Inner { - /// The context for the thread which serves clipboard read - /// requests coming to us. - server: XContext, - atoms: Atoms, + /// The context for the thread which serves clipboard read + /// requests coming to us. + server: XContext, + atoms: Atoms, - clipboard: Selection, - primary: Selection, - secondary: Selection, + clipboard: Selection, + primary: Selection, + secondary: Selection, - handover_state: Mutex, - handover_cv: Condvar, + handover_state: Mutex, + handover_cv: Condvar, - serve_stopped: AtomicBool, + serve_stopped: AtomicBool, } impl XContext { - fn new() -> Result { - // create a new connection to an X11 server - let (conn, screen_num): (RustConnection, _) = - RustConnection::connect(None).map_err(|_| { - Error::unknown("X11 server connection timed out because it was unreachable") - })?; - let screen = conn - .setup() - .roots - .get(screen_num) - .ok_or(Error::unknown("no screen found"))?; - let win_id = conn.generate_id().map_err(into_unknown)?; + fn new() -> Result { + // create a new connection to an X11 server + let (conn, screen_num): (RustConnection, _) = + RustConnection::connect(None).map_err(|_| { + Error::unknown("X11 server connection timed out because it was unreachable") + })?; + let screen = conn + .setup() + .roots + .get(screen_num) + .ok_or(Error::unknown("no screen found"))?; + let win_id = conn.generate_id().map_err(into_unknown)?; - let event_mask = + let event_mask = // Just in case that some program reports SelectionNotify events // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. EventMask::PROPERTY_CHANGE | // To receive DestroyNotify event and stop the message loop. EventMask::STRUCTURE_NOTIFY; - // create the window - conn.create_window( - // copy as much as possible from the parent, because no other specific input is needed - COPY_DEPTH_FROM_PARENT, - win_id, - screen.root, - 0, - 0, - 1, - 1, - 0, - WindowClass::COPY_FROM_PARENT, - COPY_FROM_PARENT, - // don't subscribe to any special events because we are requesting everything we need ourselves - &CreateWindowAux::new().event_mask(event_mask), - ) - .map_err(into_unknown)?; - conn.flush().map_err(into_unknown)?; + // create the window + conn.create_window( + // copy as much as possible from the parent, because no other specific input is needed + COPY_DEPTH_FROM_PARENT, + win_id, + screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::COPY_FROM_PARENT, + COPY_FROM_PARENT, + // don't subscribe to any special events because we are requesting everything we need ourselves + &CreateWindowAux::new().event_mask(event_mask), + ) + .map_err(into_unknown)?; + conn.flush().map_err(into_unknown)?; - Ok(Self { conn, win_id }) - } + Ok(Self { conn, win_id }) + } } #[derive(Default)] struct Selection { - data: RwLock>>, - /// Mutex around nothing to use with the below condvar. - mutex: Mutex<()>, - /// A condvar that is notified when the contents of this clipboard are changed. - /// - /// This is associated with `Self::mutex`. - data_changed: Condvar, + data: RwLock>>, + /// Mutex around nothing to use with the below condvar. + mutex: Mutex<()>, + /// A condvar that is notified when the contents of this clipboard are changed. + /// + /// This is associated with `Self::mutex`. + data_changed: Condvar, } #[derive(Debug, Clone)] struct ClipboardData { - bytes: Vec, + bytes: Vec, - /// The atom representing the format in which the data is encoded. - format: Atom, + /// The atom representing the format in which the data is encoded. + format: Atom, } enum ReadSelNotifyResult { - GotData(ClipboardData), - IncrStarted, - EventNotRecognized, + GotData(ClipboardData), + IncrStarted, + EventNotRecognized, } impl Inner { - fn new() -> Result { - let server = XContext::new()?; - let atoms = Atoms::new(&server.conn) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)?; + fn new() -> Result { + let server = XContext::new()?; + let atoms = Atoms::new(&server.conn) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; - Ok(Self { - server, - atoms, - clipboard: Selection::default(), - primary: Selection::default(), - secondary: Selection::default(), - handover_state: Mutex::new(ManagerHandoverState::Idle), - handover_cv: Condvar::new(), - serve_stopped: AtomicBool::new(false), - }) - } + Ok(Self { + server, + atoms, + clipboard: Selection::default(), + primary: Selection::default(), + secondary: Selection::default(), + handover_state: Mutex::new(ManagerHandoverState::Idle), + handover_cv: Condvar::new(), + serve_stopped: AtomicBool::new(false), + }) + } - fn write( - &self, - data: Vec, - selection: ClipboardKind, - wait: WaitConfig, - ) -> Result<()> { - if self.serve_stopped.load(Ordering::Relaxed) { - return Err(Error::unknown( - "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", - )); - } + fn write( + &self, + data: Vec, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + if self.serve_stopped.load(Ordering::Relaxed) { + return Err(Error::unknown( + "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", + )); + } - let server_win = self.server.win_id; + let server_win = self.server.win_id; - // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data - // changes. - self.server - .conn - .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) - .map_err(|_| Error::ClipboardOccupied)?; + // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data + // changes. + self.server + .conn + .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) + .map_err(|_| Error::ClipboardOccupied)?; - self.server.conn.flush().map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; - // Just setting the data, and the `serve_requests` will take care of the rest. - let selection = self.selection_of(selection); - let mut data_guard = selection.data.write(); - *data_guard = Some(data); + // Just setting the data, and the `serve_requests` will take care of the rest. + let selection = self.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = Some(data); - // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between - // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other - // threads in that position. - let mut guard = selection.mutex.lock(); + // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between + // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other + // threads in that position. + let mut guard = selection.mutex.lock(); - // Notify any existing waiting threads that we have changed the data in the selection. - // It is important that the mutex is locked to prevent this notification getting lost. - selection.data_changed.notify_all(); + // Notify any existing waiting threads that we have changed the data in the selection. + // It is important that the mutex is locked to prevent this notification getting lost. + selection.data_changed.notify_all(); - match wait { - WaitConfig::None => {} - WaitConfig::Forever => { - drop(data_guard); - selection.data_changed.wait(&mut guard); - } - WaitConfig::Until(deadline) => { - drop(data_guard); - selection.data_changed.wait_until(&mut guard, deadline); - } - } + match wait { + WaitConfig::None => {} + WaitConfig::Forever => { + drop(data_guard); + selection.data_changed.wait(&mut guard); + } + WaitConfig::Until(deadline) => { + drop(data_guard); + selection.data_changed.wait_until(&mut guard, deadline); + } + } - Ok(()) - } + Ok(()) + } - /// `formats` must be a slice of atoms, where each atom represents a target format. - /// The first format from `formats`, which the clipboard owner supports will be the - /// format of the return value. - fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result { - // if we are the current owner, we can get the current clipboard ourselves - if self.is_owner(selection)? { - let data = self.selection_of(selection).data.read(); - if let Some(data_list) = &*data { - for data in data_list { - for format in formats { - if *format == data.format { - return Ok(data.clone()); - } - } - } - } - return Err(Error::ContentNotAvailable); - } - let reader = XContext::new()?; + /// `formats` must be a slice of atoms, where each atom represents a target format. + /// The first format from `formats`, which the clipboard owner supports will be the + /// format of the return value. + fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result { + // if we are the current owner, we can get the current clipboard ourselves + if self.is_owner(selection)? { + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + for format in formats { + if *format == data.format { + return Ok(data.clone()); + } + } + } + } + return Err(Error::ContentNotAvailable); + } + let reader = XContext::new()?; - let highest_precedence_format = - match self.read_single(&reader, selection, self.atoms.TARGETS) { - Err(err) => { - log::trace!("Clipboard TARGETS query failed with {err:?}"); - None - } - Ok(ClipboardData { bytes, format }) => { - if format == self.atoms.ATOM { - let available_formats = Self::parse_formats(&bytes); - formats - .iter() - .find(|format| available_formats.contains(format)) - } else { - log::trace!( - "Unexpected clipboard TARGETS format {}", - self.atom_name(format) - ); - None - } - } - }; + let highest_precedence_format = + match self.read_single(&reader, selection, self.atoms.TARGETS) { + Err(err) => { + log::trace!("Clipboard TARGETS query failed with {err:?}"); + None + } + Ok(ClipboardData { bytes, format }) => { + if format == self.atoms.ATOM { + let available_formats = Self::parse_formats(&bytes); + formats + .iter() + .find(|format| available_formats.contains(format)) + } else { + log::trace!( + "Unexpected clipboard TARGETS format {}", + self.atom_name(format) + ); + None + } + } + }; - if let Some(&format) = highest_precedence_format { - let data = self.read_single(&reader, selection, format)?; - if !formats.contains(&data.format) { - // This shouldn't happen since the format is from the TARGETS list. - log::trace!( - "Conversion to {} responded with {} which is not supported", - self.atom_name(format), - self.atom_name(data.format), - ); - return Err(Error::ConversionFailure); - } - return Ok(data); - } + if let Some(&format) = highest_precedence_format { + let data = self.read_single(&reader, selection, format)?; + if !formats.contains(&data.format) { + // This shouldn't happen since the format is from the TARGETS list. + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(format), + self.atom_name(data.format), + ); + return Err(Error::ConversionFailure); + } + return Ok(data); + } - log::trace!("Falling back on attempting to convert clipboard to each format."); - for format in formats { - match self.read_single(&reader, selection, *format) { - Ok(data) => { - if formats.contains(&data.format) { - return Ok(data); - } else { - log::trace!( - "Conversion to {} responded with {} which is not supported", - self.atom_name(*format), - self.atom_name(data.format), - ); - continue; - } - } - Err(Error::ContentNotAvailable) => { - continue; - } - Err(e) => { - log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); - return Err(e); - } - } - } - log::trace!("All conversions to supported formats failed."); - Err(Error::ContentNotAvailable) - } + log::trace!("Falling back on attempting to convert clipboard to each format."); + for format in formats { + match self.read_single(&reader, selection, *format) { + Ok(data) => { + if formats.contains(&data.format) { + return Ok(data); + } else { + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(*format), + self.atom_name(data.format), + ); + continue; + } + } + Err(Error::ContentNotAvailable) => { + continue; + } + Err(e) => { + log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); + return Err(e); + } + } + } + log::trace!("All conversions to supported formats failed."); + Err(Error::ContentNotAvailable) + } - fn parse_formats(bytes: &[u8]) -> Vec { - bytes - .chunks_exact(4) - .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect() - } + fn parse_formats(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect() + } - fn read_single( - &self, - reader: &XContext, - selection: ClipboardKind, - target_format: Atom, - ) -> Result { - // Delete the property so that we can detect (using property notify) - // when the selection owner receives our request. - reader - .conn - .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) - .map_err(into_unknown)?; + fn read_single( + &self, + reader: &XContext, + selection: ClipboardKind, + target_format: Atom, + ) -> Result { + // Delete the property so that we can detect (using property notify) + // when the selection owner receives our request. + reader + .conn + .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) + .map_err(into_unknown)?; - // request to convert the clipboard selection to our data type(s) - reader - .conn - .convert_selection( - reader.win_id, - self.atom_of(selection), - target_format, - self.atoms.ARBOARD_CLIPBOARD, - Time::CURRENT_TIME, - ) - .map_err(into_unknown)?; - reader.conn.sync().map_err(into_unknown)?; + // request to convert the clipboard selection to our data type(s) + reader + .conn + .convert_selection( + reader.win_id, + self.atom_of(selection), + target_format, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + reader.conn.sync().map_err(into_unknown)?; - log::trace!("Finished `convert_selection`"); + log::trace!("Finished `convert_selection`"); - let mut incr_data: Vec = Vec::new(); - let mut using_incr = false; + let mut incr_data: Vec = Vec::new(); + let mut using_incr = false; - let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; + let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; - while Instant::now() < timeout_end { - let event = reader.conn.poll_for_event().map_err(into_unknown)?; - let event = match event { - Some(e) => e, - None => { - std::thread::sleep(Duration::from_millis(1)); - continue; - } - }; - match event { - // The first response after requesting a selection. - Event::SelectionNotify(event) => { - log::trace!("Read SelectionNotify"); - let result = self.handle_read_selection_notify( - reader, - target_format, - &mut using_incr, - &mut incr_data, - event, - )?; - match result { - ReadSelNotifyResult::GotData(data) => return Ok(data), - ReadSelNotifyResult::IncrStarted => { - // This means we received an indication that an the - // data is going to be sent INCRementally. Let's - // reset our timeout. - timeout_end += SHORT_TIMEOUT_DUR; - } - ReadSelNotifyResult::EventNotRecognized => (), - } - } - // If the previous SelectionNotify event specified that the data - // will be sent in INCR segments, each segment is transferred in - // a PropertyNotify event. - Event::PropertyNotify(event) => { - let result = self.handle_read_property_notify( - reader, - target_format, - using_incr, - &mut incr_data, - &mut timeout_end, - event, - )?; - if result { - return Ok(ClipboardData { - bytes: incr_data, - format: target_format, - }); - } - } - _ => log::trace!( - "An unexpected event arrived while reading the clipboard: {:?}", - event - ), - } - } - log::info!("Time-out hit while reading the clipboard."); - Err(Error::ContentNotAvailable) - } + while Instant::now() < timeout_end { + let event = reader.conn.poll_for_event().map_err(into_unknown)?; + let event = match event { + Some(e) => e, + None => { + std::thread::sleep(Duration::from_millis(1)); + continue; + } + }; + match event { + // The first response after requesting a selection. + Event::SelectionNotify(event) => { + log::trace!("Read SelectionNotify"); + let result = self.handle_read_selection_notify( + reader, + target_format, + &mut using_incr, + &mut incr_data, + event, + )?; + match result { + ReadSelNotifyResult::GotData(data) => return Ok(data), + ReadSelNotifyResult::IncrStarted => { + // This means we received an indication that an the + // data is going to be sent INCRementally. Let's + // reset our timeout. + timeout_end += SHORT_TIMEOUT_DUR; + } + ReadSelNotifyResult::EventNotRecognized => (), + } + } + // If the previous SelectionNotify event specified that the data + // will be sent in INCR segments, each segment is transferred in + // a PropertyNotify event. + Event::PropertyNotify(event) => { + let result = self.handle_read_property_notify( + reader, + target_format, + using_incr, + &mut incr_data, + &mut timeout_end, + event, + )?; + if result { + return Ok(ClipboardData { + bytes: incr_data, + format: target_format, + }); + } + } + _ => log::trace!( + "An unexpected event arrived while reading the clipboard: {:?}", + event + ), + } + } + log::info!("Time-out hit while reading the clipboard."); + Err(Error::ContentNotAvailable) + } - fn atom_of(&self, selection: ClipboardKind) -> Atom { - match selection { - ClipboardKind::Clipboard => self.atoms.CLIPBOARD, - ClipboardKind::Primary => self.atoms.PRIMARY, - ClipboardKind::Secondary => self.atoms.SECONDARY, - } - } + fn atom_of(&self, selection: ClipboardKind) -> Atom { + match selection { + ClipboardKind::Clipboard => self.atoms.CLIPBOARD, + ClipboardKind::Primary => self.atoms.PRIMARY, + ClipboardKind::Secondary => self.atoms.SECONDARY, + } + } - fn selection_of(&self, selection: ClipboardKind) -> &Selection { - match selection { - ClipboardKind::Clipboard => &self.clipboard, - ClipboardKind::Primary => &self.primary, - ClipboardKind::Secondary => &self.secondary, - } - } + fn selection_of(&self, selection: ClipboardKind) -> &Selection { + match selection { + ClipboardKind::Clipboard => &self.clipboard, + ClipboardKind::Primary => &self.primary, + ClipboardKind::Secondary => &self.secondary, + } + } - fn kind_of(&self, atom: Atom) -> Option { - match atom { - a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), - a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), - a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), - _ => None, - } - } + fn kind_of(&self, atom: Atom) -> Option { + match atom { + a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), + a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), + a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), + _ => None, + } + } - fn is_owner(&self, selection: ClipboardKind) -> Result { - let current = self - .server - .conn - .get_selection_owner(self.atom_of(selection)) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)? - .owner; + fn is_owner(&self, selection: ClipboardKind) -> Result { + let current = self + .server + .conn + .get_selection_owner(self.atom_of(selection)) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .owner; - Ok(current == self.server.win_id) - } + Ok(current == self.server.win_id) + } - fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { - String::from_utf8( - self.server - .conn - .get_atom_name(atom) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)? - .name, - ) - .map_err(into_unknown) - } + fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { + String::from_utf8( + self.server + .conn + .get_atom_name(atom) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .name, + ) + .map_err(into_unknown) + } - fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { - ATOM_NAME_CACHE.with(|cache| { - let mut cache = cache.borrow_mut(); - match cache.entry(atom) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let s = self - .query_atom_name(atom) - .map(|s| Box::leak(s.into_boxed_str()) as &str) - .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); - entry.insert(s); - s - } - } - }) - } + fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { + ATOM_NAME_CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + match cache.entry(atom) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let s = self + .query_atom_name(atom) + .map(|s| Box::leak(s.into_boxed_str()) as &str) + .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); + entry.insert(s); + s + } + } + }) + } - fn handle_read_selection_notify( - &self, - reader: &XContext, - target_format: u32, - using_incr: &mut bool, - incr_data: &mut Vec, - event: SelectionNotifyEvent, - ) -> Result { - // The property being set to NONE means that the `convert_selection` - // failed. + fn handle_read_selection_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: &mut bool, + incr_data: &mut Vec, + event: SelectionNotifyEvent, + ) -> Result { + // The property being set to NONE means that the `convert_selection` + // failed. - // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 - // the target must be set to the same as what we requested. - if event.property == NONE || event.target != target_format { - return Err(Error::ContentNotAvailable); - } - if self.kind_of(event.selection).is_none() { - log::info!( - "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." - ); - return Ok(ReadSelNotifyResult::EventNotRecognized); - } - if *using_incr { - log::warn!("Received a SelectionNotify while already expecting INCR segments."); - return Ok(ReadSelNotifyResult::EventNotRecognized); - } - // Accept any property type. The property type will typically match the format type except - // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case - // where the clipboard is not convertible to the requested format. In this case - // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY` - // is provided. - let property_type = AtomEnum::ANY; - // request the selection - let mut reply = reader - .conn - .get_property( - true, - event.requestor, - event.property, - property_type, - 0, - u32::MAX / 4, - ) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)?; + // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 + // the target must be set to the same as what we requested. + if event.property == NONE || event.target != target_format { + return Err(Error::ContentNotAvailable); + } + if self.kind_of(event.selection).is_none() { + log::info!( + "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + if *using_incr { + log::warn!("Received a SelectionNotify while already expecting INCR segments."); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + // Accept any property type. The property type will typically match the format type except + // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case + // where the clipboard is not convertible to the requested format. In this case + // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY` + // is provided. + let property_type = AtomEnum::ANY; + // request the selection + let mut reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + property_type, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; - // we found something - if reply.type_ == self.atoms.INCR { - // Note that we call the get_property again because we are - // indicating that we are ready to receive the data by deleting the - // property, however deleting only works if the type matches the - // property type. But the type didn't match in the previous call. - reply = reader - .conn - .get_property( - true, - event.requestor, - event.property, - self.atoms.INCR, - 0, - u32::MAX / 4, - ) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)?; - log::trace!("Receiving INCR segments"); - *using_incr = true; - if reply.value_len == 4 { - let min_data_len = reply - .value32() - .and_then(|mut vals| vals.next()) - .unwrap_or(0); - incr_data.reserve(min_data_len as usize); - } - Ok(ReadSelNotifyResult::IncrStarted) - } else { - Ok(ReadSelNotifyResult::GotData(ClipboardData { - bytes: reply.value, - format: reply.type_, - })) - } - } + // we found something + if reply.type_ == self.atoms.INCR { + // Note that we call the get_property again because we are + // indicating that we are ready to receive the data by deleting the + // property, however deleting only works if the type matches the + // property type. But the type didn't match in the previous call. + reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + self.atoms.INCR, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + log::trace!("Receiving INCR segments"); + *using_incr = true; + if reply.value_len == 4 { + let min_data_len = reply + .value32() + .and_then(|mut vals| vals.next()) + .unwrap_or(0); + incr_data.reserve(min_data_len as usize); + } + Ok(ReadSelNotifyResult::IncrStarted) + } else { + Ok(ReadSelNotifyResult::GotData(ClipboardData { + bytes: reply.value, + format: reply.type_, + })) + } + } - /// Returns Ok(true) when the incr_data is ready - fn handle_read_property_notify( - &self, - reader: &XContext, - target_format: u32, - using_incr: bool, - incr_data: &mut Vec, - timeout_end: &mut Instant, - event: PropertyNotifyEvent, - ) -> Result { - if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE { - return Ok(false); - } - if !using_incr { - // This must mean the selection owner received our request, and is - // now preparing the data - return Ok(false); - } - let reply = reader - .conn - .get_property( - true, - event.window, - event.atom, - if target_format == self.atoms.TARGETS { - self.atoms.ATOM - } else { - target_format - }, - 0, - u32::MAX / 4, - ) - .map_err(into_unknown)? - .reply() - .map_err(into_unknown)?; + /// Returns Ok(true) when the incr_data is ready + fn handle_read_property_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: bool, + incr_data: &mut Vec, + timeout_end: &mut Instant, + event: PropertyNotifyEvent, + ) -> Result { + if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE { + return Ok(false); + } + if !using_incr { + // This must mean the selection owner received our request, and is + // now preparing the data + return Ok(false); + } + let reply = reader + .conn + .get_property( + true, + event.window, + event.atom, + if target_format == self.atoms.TARGETS { + self.atoms.ATOM + } else { + target_format + }, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; - // log::trace!("Received segment. value_len {}", reply.value_len,); - if reply.value_len == 0 { - // This indicates that all the data has been sent. - return Ok(true); - } - incr_data.extend(reply.value); + // log::trace!("Received segment. value_len {}", reply.value_len,); + if reply.value_len == 0 { + // This indicates that all the data has been sent. + return Ok(true); + } + incr_data.extend(reply.value); - // Let's reset our timeout, since we received a valid chunk. - *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR; + // Let's reset our timeout, since we received a valid chunk. + *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR; - // Not yet complete - Ok(false) - } + // Not yet complete + Ok(false) + } - fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { - let selection = match self.kind_of(event.selection) { - Some(kind) => kind, - None => { - log::warn!( - "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." - ); - return Ok(()); - } - }; + fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { + let selection = match self.kind_of(event.selection) { + Some(kind) => kind, + None => { + log::warn!( + "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(()); + } + }; - let success; - // we are asked for a list of supported conversion targets - if event.target == self.atoms.TARGETS { - log::trace!( - "Handling TARGETS, dst property is {}", - self.atom_name(event.property) - ); - let mut targets = Vec::with_capacity(10); - targets.push(self.atoms.TARGETS); - targets.push(self.atoms.SAVE_TARGETS); - let data = self.selection_of(selection).data.read(); - if let Some(data_list) = &*data { - for data in data_list { - targets.push(data.format); - if data.format == self.atoms.UTF8_STRING { - // When we are storing a UTF8 string, - // add all equivalent formats to the supported targets - targets.push(self.atoms.UTF8_MIME_0); - targets.push(self.atoms.UTF8_MIME_1); - } - } - } - self.server - .conn - .change_property32( - PropMode::REPLACE, - event.requestor, - event.property, - // TODO: change to `AtomEnum::ATOM` - self.atoms.ATOM, - &targets, - ) - .map_err(into_unknown)?; - self.server.conn.flush().map_err(into_unknown)?; - success = true; - } else { - log::trace!("Handling request for (probably) the clipboard contents."); - let data = self.selection_of(selection).data.read(); - if let Some(data_list) = &*data { - success = match data_list.iter().find(|d| d.format == event.target) { - Some(data) => { - self.server - .conn - .change_property8( - PropMode::REPLACE, - event.requestor, - event.property, - event.target, - &data.bytes, - ) - .map_err(into_unknown)?; - self.server.conn.flush().map_err(into_unknown)?; - true - } - None => false, - }; - } else { - // This must mean that we lost ownership of the data - // since the other side requested the selection. - // Let's respond with the property set to none. - success = false; - } - } - // on failure we notify the requester of it - let property = if success { - event.property - } else { - AtomEnum::NONE.into() - }; - // tell the requestor that we finished sending data - self.server - .conn - .send_event( - false, - event.requestor, - EventMask::NO_EVENT, - SelectionNotifyEvent { - response_type: SELECTION_NOTIFY_EVENT, - sequence: event.sequence, - time: event.time, - requestor: event.requestor, - selection: event.selection, - target: event.target, - property, - }, - ) - .map_err(into_unknown)?; + let success; + // we are asked for a list of supported conversion targets + if event.target == self.atoms.TARGETS { + log::trace!( + "Handling TARGETS, dst property is {}", + self.atom_name(event.property) + ); + let mut targets = Vec::with_capacity(10); + targets.push(self.atoms.TARGETS); + targets.push(self.atoms.SAVE_TARGETS); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + targets.push(data.format); + if data.format == self.atoms.UTF8_STRING { + // When we are storing a UTF8 string, + // add all equivalent formats to the supported targets + targets.push(self.atoms.UTF8_MIME_0); + targets.push(self.atoms.UTF8_MIME_1); + } + } + } + self.server + .conn + .change_property32( + PropMode::REPLACE, + event.requestor, + event.property, + // TODO: change to `AtomEnum::ATOM` + self.atoms.ATOM, + &targets, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + success = true; + } else { + log::trace!("Handling request for (probably) the clipboard contents."); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + success = match data_list.iter().find(|d| d.format == event.target) { + Some(data) => { + self.server + .conn + .change_property8( + PropMode::REPLACE, + event.requestor, + event.property, + event.target, + &data.bytes, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + true + } + None => false, + }; + } else { + // This must mean that we lost ownership of the data + // since the other side requested the selection. + // Let's respond with the property set to none. + success = false; + } + } + // on failure we notify the requester of it + let property = if success { + event.property + } else { + AtomEnum::NONE.into() + }; + // tell the requestor that we finished sending data + self.server + .conn + .send_event( + false, + event.requestor, + EventMask::NO_EVENT, + SelectionNotifyEvent { + response_type: SELECTION_NOTIFY_EVENT, + sequence: event.sequence, + time: event.time, + requestor: event.requestor, + selection: event.selection, + target: event.target, + property, + }, + ) + .map_err(into_unknown)?; - self.server.conn.flush().map_err(into_unknown) - } + self.server.conn.flush().map_err(into_unknown) + } - fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> { - if self.server.win_id == 0 { - // This shouldn't really ever happen but let's just check. - log::error!("The server's window id was 0. This is unexpected"); - return Ok(()); - } + fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> { + if self.server.win_id == 0 { + // This shouldn't really ever happen but let's just check. + log::error!("The server's window id was 0. This is unexpected"); + return Ok(()); + } - if !self.is_owner(ClipboardKind::Clipboard)? { - // We are not owning the clipboard, nothing to do. - return Ok(()); - } - if self - .selection_of(ClipboardKind::Clipboard) - .data - .read() - .is_none() - { - // If we don't have any data, there's nothing to do. - return Ok(()); - } + if !self.is_owner(ClipboardKind::Clipboard)? { + // We are not owning the clipboard, nothing to do. + return Ok(()); + } + if self + .selection_of(ClipboardKind::Clipboard) + .data + .read() + .is_none() + { + // If we don't have any data, there's nothing to do. + return Ok(()); + } - // It's important that we lock the state before sending the request - // because we don't want the request server thread to lock the state - // after the request but before we can lock it here. - let mut handover_state = self.handover_state.lock(); + // It's important that we lock the state before sending the request + // because we don't want the request server thread to lock the state + // after the request but before we can lock it here. + let mut handover_state = self.handover_state.lock(); - log::trace!("Sending the data to the clipboard manager"); - self.server - .conn - .convert_selection( - self.server.win_id, - self.atoms.CLIPBOARD_MANAGER, - self.atoms.SAVE_TARGETS, - self.atoms.ARBOARD_CLIPBOARD, - Time::CURRENT_TIME, - ) - .map_err(into_unknown)?; - self.server.conn.flush().map_err(into_unknown)?; + log::trace!("Sending the data to the clipboard manager"); + self.server + .conn + .convert_selection( + self.server.win_id, + self.atoms.CLIPBOARD_MANAGER, + self.atoms.SAVE_TARGETS, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; - *handover_state = ManagerHandoverState::InProgress; - let max_handover_duration = Duration::from_millis(100); + *handover_state = ManagerHandoverState::InProgress; + let max_handover_duration = Duration::from_millis(100); - // Note that we are using a parking_lot condvar here, which doesn't wake up - // spuriously - let result = self - .handover_cv - .wait_for(&mut handover_state, max_handover_duration); + // Note that we are using a parking_lot condvar here, which doesn't wake up + // spuriously + let result = self + .handover_cv + .wait_for(&mut handover_state, max_handover_duration); - if *handover_state == ManagerHandoverState::Finished { - return Ok(()); - } - if result.timed_out() { - log::warn!( - "Could not hand the clipboard contents over to the clipboard manager. The request timed out." - ); - return Ok(()); - } + if *handover_state == ManagerHandoverState::Finished { + return Ok(()); + } + if result.timed_out() { + log::warn!( + "Could not hand the clipboard contents over to the clipboard manager. The request timed out." + ); + return Ok(()); + } - Err(Error::unknown( - "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.", - )) - } + Err(Error::unknown( + "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.", + )) + } } fn serve_requests(context: Arc) -> Result<(), Box> { - fn handover_finished(clip: &Arc, mut handover_state: MutexGuard) { - log::trace!("Finishing clipboard manager handover."); - *handover_state = ManagerHandoverState::Finished; + fn handover_finished(clip: &Arc, mut handover_state: MutexGuard) { + log::trace!("Finishing clipboard manager handover."); + *handover_state = ManagerHandoverState::Finished; - // Not sure if unlocking the mutex is necessary here but better safe than sorry. - drop(handover_state); + // Not sure if unlocking the mutex is necessary here but better safe than sorry. + drop(handover_state); - clip.handover_cv.notify_all(); - } + clip.handover_cv.notify_all(); + } - log::trace!("Started serve requests thread."); + log::trace!("Started serve requests thread."); - let _guard = util::defer(|| { - context.serve_stopped.store(true, Ordering::Relaxed); - }); + let _guard = util::defer(|| { + context.serve_stopped.store(true, Ordering::Relaxed); + }); - let mut written = false; - let mut notified = false; + let mut written = false; + let mut notified = false; - loop { - match context.server.conn.wait_for_event().map_err(into_unknown)? { - Event::DestroyNotify(_) => { - // This window is being destroyed. - log::trace!("Clipboard server window is being destroyed x_x"); - return Ok(()); - } - Event::SelectionClear(event) => { - // TODO: check if this works - // Someone else has new content in the clipboard, so it is - // notifying us that we should delete our data now. - log::trace!("Somebody else owns the clipboard now"); + loop { + match context.server.conn.wait_for_event().map_err(into_unknown)? { + Event::DestroyNotify(_) => { + // This window is being destroyed. + log::trace!("Clipboard server window is being destroyed x_x"); + return Ok(()); + } + Event::SelectionClear(event) => { + // TODO: check if this works + // Someone else has new content in the clipboard, so it is + // notifying us that we should delete our data now. + log::trace!("Somebody else owns the clipboard now"); - if let Some(selection) = context.kind_of(event.selection) { - let selection = context.selection_of(selection); - let mut data_guard = selection.data.write(); - *data_guard = None; + if let Some(selection) = context.kind_of(event.selection) { + let selection = context.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = None; - // It is important that this mutex is locked at the time of calling - // `notify_all` to prevent notifications getting lost in case the sleeping - // thread has unlocked its `data_guard` and is just about to sleep. - // It is also important that the RwLock is kept write-locked for the same - // reason. - let _guard = selection.mutex.lock(); - selection.data_changed.notify_all(); - } - } - Event::SelectionRequest(event) => { - log::trace!( - "SelectionRequest - selection is: {}, target is {}", - context.atom_name(event.selection), - context.atom_name(event.target), - ); - // Someone is requesting the clipboard content from us. - context - .handle_selection_request(event) - .map_err(into_unknown)?; + // It is important that this mutex is locked at the time of calling + // `notify_all` to prevent notifications getting lost in case the sleeping + // thread has unlocked its `data_guard` and is just about to sleep. + // It is also important that the RwLock is kept write-locked for the same + // reason. + let _guard = selection.mutex.lock(); + selection.data_changed.notify_all(); + } + } + Event::SelectionRequest(event) => { + log::trace!( + "SelectionRequest - selection is: {}, target is {}", + context.atom_name(event.selection), + context.atom_name(event.target), + ); + // Someone is requesting the clipboard content from us. + context + .handle_selection_request(event) + .map_err(into_unknown)?; - // if we are in the progress of saving to the clipboard manager - // make sure we save that we have finished writing - let handover_state = context.handover_state.lock(); - if *handover_state == ManagerHandoverState::InProgress { - // Only set written, when the actual contents were written, - // not just a response to what TARGETS we have. - if event.target != context.atoms.TARGETS { - log::trace!("The contents were written to the clipboard manager."); - written = true; - // if we have written and notified, make sure to notify that we are done - if notified { - handover_finished(&context, handover_state); - } - } - } - } - Event::SelectionNotify(event) => { - // We've requested the clipboard content and this is the answer. - // Considering that this thread is not responsible for reading - // clipboard contents, this must come from the clipboard manager - // signaling that the data was handed over successfully. - if event.selection != context.atoms.CLIPBOARD_MANAGER { - log::error!( - "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread." - ); - continue; - } - let handover_state = context.handover_state.lock(); - if *handover_state == ManagerHandoverState::InProgress { - // Note that some clipboard managers send a selection notify - // before even sending a request for the actual contents. - // (That's why we use the "notified" & "written" flags) - log::trace!( - "The clipboard manager indicated that it's done requesting the contents from us." - ); - notified = true; + // if we are in the progress of saving to the clipboard manager + // make sure we save that we have finished writing + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Only set written, when the actual contents were written, + // not just a response to what TARGETS we have. + if event.target != context.atoms.TARGETS { + log::trace!("The contents were written to the clipboard manager."); + written = true; + // if we have written and notified, make sure to notify that we are done + if notified { + handover_finished(&context, handover_state); + } + } + } + } + Event::SelectionNotify(event) => { + // We've requested the clipboard content and this is the answer. + // Considering that this thread is not responsible for reading + // clipboard contents, this must come from the clipboard manager + // signaling that the data was handed over successfully. + if event.selection != context.atoms.CLIPBOARD_MANAGER { + log::error!( + "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread." + ); + continue; + } + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Note that some clipboard managers send a selection notify + // before even sending a request for the actual contents. + // (That's why we use the "notified" & "written" flags) + log::trace!( + "The clipboard manager indicated that it's done requesting the contents from us." + ); + notified = true; - // One would think that we could also finish if the property - // here is set 0, because that indicates failure. However - // this is not the case; for example on KDE plasma 5.18, we - // immediately get a SelectionNotify with property set to 0, - // but following that, we also get a valid SelectionRequest - // from the clipboard manager. - if written { - handover_finished(&context, handover_state); - } - } - } - _event => { - // May be useful for debugging but nothing else really. - //log::trace!("Received unwanted event: {:?}", event); - } - } - } + // One would think that we could also finish if the property + // here is set 0, because that indicates failure. However + // this is not the case; for example on KDE plasma 5.18, we + // immediately get a SelectionNotify with property set to 0, + // but following that, we also get a valid SelectionRequest + // from the clipboard manager. + if written { + handover_finished(&context, handover_state); + } + } + } + _event => { + // May be useful for debugging but nothing else really. + //log::trace!("Received unwanted event: {:?}", event); + } + } + } } pub(crate) struct Clipboard { - inner: Arc, + inner: Arc, } impl Clipboard { - pub(crate) fn new() -> Result { - let mut global_cb = CLIPBOARD.lock(); - if let Some(global_cb) = &*global_cb { - return Ok(Self { - inner: Arc::clone(&global_cb.inner), - }); - } - // At this point we know that the clipboard does not exist. - let ctx = Arc::new(Inner::new()?); - let join_handle = std::thread::Builder::new() - .name("Clipboard".to_owned()) - .spawn({ - let ctx = Arc::clone(&ctx); - move || { - if let Err(error) = serve_requests(ctx) { - log::error!("Worker thread errored with: {}", error); - } - } - }) - .unwrap(); - *global_cb = Some(GlobalClipboard { - inner: Arc::clone(&ctx), - server_handle: join_handle, - }); - Ok(Self { inner: ctx }) - } + pub(crate) fn new() -> Result { + let mut global_cb = CLIPBOARD.lock(); + if let Some(global_cb) = &*global_cb { + return Ok(Self { + inner: Arc::clone(&global_cb.inner), + }); + } + // At this point we know that the clipboard does not exist. + let ctx = Arc::new(Inner::new()?); + let join_handle = std::thread::Builder::new() + .name("Clipboard".to_owned()) + .spawn({ + let ctx = Arc::clone(&ctx); + move || { + if let Err(error) = serve_requests(ctx) { + log::error!("Worker thread errored with: {}", error); + } + } + }) + .unwrap(); + *global_cb = Some(GlobalClipboard { + inner: Arc::clone(&ctx), + server_handle: join_handle, + }); + Ok(Self { inner: ctx }) + } - pub(crate) fn set_text( - &self, - message: Cow<'_, str>, - selection: ClipboardKind, - wait: WaitConfig, - ) -> Result<()> { - let data = vec![ClipboardData { - bytes: message.into_owned().into_bytes(), - format: self.inner.atoms.UTF8_STRING, - }]; - self.inner.write(data, selection, wait) - } + pub(crate) fn set_text( + &self, + message: Cow<'_, str>, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let data = vec![ClipboardData { + bytes: message.into_owned().into_bytes(), + format: self.inner.atoms.UTF8_STRING, + }]; + self.inner.write(data, selection, wait) + } - fn image_format_atom(&self, format: ImageFormat) -> Atom { - match format { - ImageFormat::Png => self.inner.atoms.PNG__MIME, - ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME, - ImageFormat::Webp => self.inner.atoms.WEBP_MIME, - ImageFormat::Gif => self.inner.atoms.GIF__MIME, - ImageFormat::Svg => self.inner.atoms.SVG__MIME, - ImageFormat::Bmp => self.inner.atoms.BMP__MIME, - ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, - ImageFormat::Ico => self.inner.atoms.ICO__MIME, - ImageFormat::Pnm => self.inner.atoms.PNM__MIME, - } - } + fn image_format_atom(&self, format: ImageFormat) -> Atom { + match format { + ImageFormat::Png => self.inner.atoms.PNG__MIME, + ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME, + ImageFormat::Webp => self.inner.atoms.WEBP_MIME, + ImageFormat::Gif => self.inner.atoms.GIF__MIME, + ImageFormat::Svg => self.inner.atoms.SVG__MIME, + ImageFormat::Bmp => self.inner.atoms.BMP__MIME, + ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, + ImageFormat::Ico => self.inner.atoms.ICO__MIME, + ImageFormat::Pnm => self.inner.atoms.PNM__MIME, + } + } - #[allow(unused)] - pub(crate) fn set_image( - &self, - image: Image, - selection: ClipboardKind, - wait: WaitConfig, - ) -> Result<()> { - let format = self.image_format_atom(image.format); - let data = vec![ClipboardData { - bytes: image.bytes, - format: self.inner.atoms.PNG__MIME, - }]; - self.inner.write(data, selection, wait) - } + #[allow(unused)] + pub(crate) fn set_image( + &self, + image: Image, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let format = self.image_format_atom(image.format); + let data = vec![ClipboardData { + bytes: image.bytes, + format: self.inner.atoms.PNG__MIME, + }]; + self.inner.write(data, selection, wait) + } - pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result { - let image_entries = ImageFormat::iter() - .map(|format| (self.image_format_atom(format), format)) - .collect::>(); + pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result { + let image_entries = ImageFormat::iter() + .map(|format| (self.image_format_atom(format), format)) + .collect::>(); - let text_format_atoms: &[Atom] = &[ - self.inner.atoms.UTF8_STRING, - self.inner.atoms.UTF8_MIME_0, - self.inner.atoms.UTF8_MIME_1, - self.inner.atoms.STRING, - self.inner.atoms.TEXT, - self.inner.atoms.TEXT_MIME_UNKNOWN, - ]; + let text_format_atoms: &[Atom] = &[ + self.inner.atoms.UTF8_STRING, + self.inner.atoms.UTF8_MIME_0, + self.inner.atoms.UTF8_MIME_1, + self.inner.atoms.STRING, + self.inner.atoms.TEXT, + self.inner.atoms.TEXT_MIME_UNKNOWN, + ]; - // image formats first, as they are more specific, and read will return the first - // format that the contents can be converted to - let mut format_atoms = Vec::with_capacity(image_entries.len() + text_format_atoms.len()); - format_atoms.extend(image_entries.iter().map(|(atom, _)| *atom)); - format_atoms.extend_from_slice(text_format_atoms); + // image formats first, as they are more specific, and read will return the first + // format that the contents can be converted to + let mut format_atoms = Vec::with_capacity(image_entries.len() + text_format_atoms.len()); + format_atoms.extend(image_entries.iter().map(|(atom, _)| *atom)); + format_atoms.extend_from_slice(text_format_atoms); - let result = self.inner.read(&format_atoms, selection)?; + let result = self.inner.read(&format_atoms, selection)?; - log::trace!( - "read clipboard as format {:?}", - self.inner.atom_name(result.format) - ); + log::trace!( + "read clipboard as format {:?}", + self.inner.atom_name(result.format) + ); - for (format_atom, image_format) in image_entries { - if result.format == format_atom { - let bytes = result.bytes; - let id = hash(&bytes); - return Ok(ClipboardItem::new_image(&Image { - id, - format: image_format, - bytes, - })); - } - } + for (format_atom, image_format) in image_entries { + if result.format == format_atom { + let bytes = result.bytes; + let id = hash(&bytes); + return Ok(ClipboardItem::new_image(&Image { + id, + format: image_format, + bytes, + })); + } + } - let text = if result.format == self.inner.atoms.STRING { - // ISO Latin-1 - // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8 - result.bytes.into_iter().map(|c| c as char).collect() - } else { - String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? - }; - Ok(ClipboardItem::new_string(text)) - } + let text = if result.format == self.inner.atoms.STRING { + // ISO Latin-1 + // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8 + result.bytes.into_iter().map(|c| c as char).collect() + } else { + String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? + }; + Ok(ClipboardItem::new_string(text)) + } - pub fn is_owner(&self, selection: ClipboardKind) -> bool { - self.inner.is_owner(selection).unwrap_or(false) - } + pub fn is_owner(&self, selection: ClipboardKind) -> bool { + self.inner.is_owner(selection).unwrap_or(false) + } } impl Drop for Clipboard { - fn drop(&mut self) { - // There are always at least 3 owners: - // the global, the server thread, and one `Clipboard::inner` - const MIN_OWNERS: usize = 3; + fn drop(&mut self) { + // There are always at least 3 owners: + // the global, the server thread, and one `Clipboard::inner` + const MIN_OWNERS: usize = 3; - // We start with locking the global guard to prevent race - // conditions below. - let mut global_cb = CLIPBOARD.lock(); - if Arc::strong_count(&self.inner) == MIN_OWNERS { - // If the are the only owners of the clipboard are ourselves and - // the global object, then we should destroy the global object, - // and send the data to the clipboard manager + // We start with locking the global guard to prevent race + // conditions below. + let mut global_cb = CLIPBOARD.lock(); + if Arc::strong_count(&self.inner) == MIN_OWNERS { + // If the are the only owners of the clipboard are ourselves and + // the global object, then we should destroy the global object, + // and send the data to the clipboard manager - if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() { - log::error!( - "Could not hand the clipboard data over to the clipboard manager: {}", - e - ); - } - let global_cb = global_cb.take(); - if let Err(e) = self - .inner - .server - .conn - .destroy_window(self.inner.server.win_id) - { - log::error!("Failed to destroy the clipboard window. Error: {}", e); - return; - } - if let Err(e) = self.inner.server.conn.flush() { - log::error!("Failed to flush the clipboard window. Error: {}", e); - return; - } - if let Some(global_cb) = global_cb - && let Err(e) = global_cb.server_handle.join() - { - // Let's try extracting the error message - let message; - if let Some(msg) = e.downcast_ref::<&'static str>() { - message = Some((*msg).to_string()); - } else if let Some(msg) = e.downcast_ref::() { - message = Some(msg.clone()); - } else { - message = None; - } - if let Some(message) = message { - log::error!( - "The clipboard server thread panicked. Panic message: '{}'", - message, - ); - } else { - log::error!("The clipboard server thread panicked."); - } - } - } - } + if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() { + log::error!( + "Could not hand the clipboard data over to the clipboard manager: {}", + e + ); + } + let global_cb = global_cb.take(); + if let Err(e) = self + .inner + .server + .conn + .destroy_window(self.inner.server.win_id) + { + log::error!("Failed to destroy the clipboard window. Error: {}", e); + return; + } + if let Err(e) = self.inner.server.conn.flush() { + log::error!("Failed to flush the clipboard window. Error: {}", e); + return; + } + if let Some(global_cb) = global_cb + && let Err(e) = global_cb.server_handle.join() + { + // Let's try extracting the error message + let message; + if let Some(msg) = e.downcast_ref::<&'static str>() { + message = Some((*msg).to_string()); + } else if let Some(msg) = e.downcast_ref::() { + message = Some(msg.clone()); + } else { + message = None; + } + if let Some(message) = message { + log::error!( + "The clipboard server thread panicked. Panic message: '{}'", + message, + ); + } else { + log::error!("The clipboard server thread panicked."); + } + } + } + } } fn into_unknown(error: E) -> Error { - Error::Unknown { - description: error.to_string(), - } + Error::Unknown { + description: error.to_string(), + } } /// Clipboard selection @@ -1147,87 +1147,87 @@ fn into_unknown(error: E) -> Error { /// description of the different clipboards. #[derive(Copy, Clone, Debug)] pub enum ClipboardKind { - /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like - /// clipboard behavior) - Clipboard, + /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like + /// clipboard behavior) + Clipboard, - /// Typically used for mouse selections and/or currently selected text. Accessible via middle - /// mouse click. - Primary, + /// Typically used for mouse selections and/or currently selected text. Accessible via middle + /// mouse click. + Primary, - /// The secondary clipboard is rarely used but theoretically available on X11. - Secondary, + /// The secondary clipboard is rarely used but theoretically available on X11. + Secondary, } /// Configuration on how long to wait for a new X11 copy event is emitted. #[derive(Default)] pub(crate) enum WaitConfig { - /// Waits until the given [`Instant`] has reached. - #[allow( - unused, - reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" - )] - Until(Instant), + /// Waits until the given [`Instant`] has reached. + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Until(Instant), - /// Waits forever until a new event is reached. - #[allow(unused)] - #[allow( - unused, - reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" - )] - Forever, + /// Waits forever until a new event is reached. + #[allow(unused)] + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Forever, - /// It shouldn't wait. - #[default] - None, + /// It shouldn't wait. + #[default] + None, } #[non_exhaustive] pub enum Error { - /// The clipboard contents were not available in the requested format. - /// This could either be due to the clipboard being empty or the clipboard contents having - /// an incompatible format to the requested one (eg when calling `get_image` on text) - ContentNotAvailable, + /// The clipboard contents were not available in the requested format. + /// This could either be due to the clipboard being empty or the clipboard contents having + /// an incompatible format to the requested one (eg when calling `get_image` on text) + ContentNotAvailable, - /// The native clipboard is not accessible due to being held by an other party. - /// - /// This "other party" could be a different process or it could be within - /// the same program. So for example you may get this error when trying - /// to interact with the clipboard from multiple threads at once. - /// - /// Note that it's OK to have multiple `Clipboard` instances. The underlying - /// implementation will make sure that the native clipboard is only - /// opened for transferring data and then closed as soon as possible. - ClipboardOccupied, + /// The native clipboard is not accessible due to being held by an other party. + /// + /// This "other party" could be a different process or it could be within + /// the same program. So for example you may get this error when trying + /// to interact with the clipboard from multiple threads at once. + /// + /// Note that it's OK to have multiple `Clipboard` instances. The underlying + /// implementation will make sure that the native clipboard is only + /// opened for transferring data and then closed as soon as possible. + ClipboardOccupied, - /// The image or the text that was about the be transferred to/from the clipboard could not be - /// converted to the appropriate format. - ConversionFailure, + /// The image or the text that was about the be transferred to/from the clipboard could not be + /// converted to the appropriate format. + ConversionFailure, - /// Any error that doesn't fit the other error types. - /// - /// The `description` field is only meant to help the developer and should not be relied on as a - /// means to identify an error case during runtime. - Unknown { description: String }, + /// Any error that doesn't fit the other error types. + /// + /// The `description` field is only meant to help the developer and should not be relied on as a + /// means to identify an error case during runtime. + Unknown { description: String }, } impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."), Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."), Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), } - } + } } impl std::error::Error for Error {} impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use Error::*; - macro_rules! kind_to_str { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; + macro_rules! kind_to_str { ($( $e: pat ),*) => { match self { $( @@ -1236,20 +1236,20 @@ impl std::fmt::Debug for Error { } } } - let name = kind_to_str!( - ContentNotAvailable, - ClipboardOccupied, - ConversionFailure, - Unknown { .. } - ); - f.write_fmt(format_args!("{name} - \"{self}\"")) - } + let name = kind_to_str!( + ContentNotAvailable, + ClipboardOccupied, + ConversionFailure, + Unknown { .. } + ); + f.write_fmt(format_args!("{name} - \"{self}\"")) + } } impl Error { - pub(crate) fn unknown>(message: M) -> Self { - Error::Unknown { - description: message.into(), - } - } + pub(crate) fn unknown>(message: M) -> Self { + Error::Unknown { + description: message.into(), + } + } } diff --git a/crates/gpui_linux/src/linux/x11/display.rs b/crates/gpui_linux/src/linux/x11/display.rs index 582d76f7f6..4a1eb4c643 100644 --- a/crates/gpui_linux/src/linux/x11/display.rs +++ b/crates/gpui_linux/src/linux/x11/display.rs @@ -6,46 +6,46 @@ use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px}; #[derive(Debug)] pub(crate) struct X11Display { - x_screen_index: usize, - bounds: Bounds, - uuid: Uuid, + x_screen_index: usize, + bounds: Bounds, + uuid: Uuid, } impl X11Display { - pub(crate) fn new( - xcb: &XCBConnection, - scale_factor: f32, - x_screen_index: usize, - ) -> anyhow::Result { - let screen = xcb - .setup() - .roots - .get(x_screen_index) - .with_context(|| format!("No screen found with index {x_screen_index}"))?; - Ok(Self { - x_screen_index, - bounds: Bounds { - origin: Default::default(), - size: Size { - width: px(screen.width_in_pixels as f32 / scale_factor), - height: px(screen.height_in_pixels as f32 / scale_factor), - }, - }, - uuid: Uuid::from_bytes([0; 16]), - }) - } + pub(crate) fn new( + xcb: &XCBConnection, + scale_factor: f32, + x_screen_index: usize, + ) -> anyhow::Result { + let screen = xcb + .setup() + .roots + .get(x_screen_index) + .with_context(|| format!("No screen found with index {x_screen_index}"))?; + Ok(Self { + x_screen_index, + bounds: Bounds { + origin: Default::default(), + size: Size { + width: px(screen.width_in_pixels as f32 / scale_factor), + height: px(screen.height_in_pixels as f32 / scale_factor), + }, + }, + uuid: Uuid::from_bytes([0; 16]), + }) + } } impl PlatformDisplay for X11Display { - fn id(&self) -> DisplayId { - DisplayId::new(self.x_screen_index as u64) - } + fn id(&self) -> DisplayId { + DisplayId::new(self.x_screen_index as u64) + } - fn uuid(&self) -> anyhow::Result { - Ok(self.uuid) - } + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } - fn bounds(&self) -> Bounds { - self.bounds - } + fn bounds(&self) -> Bounds { + self.bounds + } } diff --git a/crates/gpui_linux/src/linux/x11/event.rs b/crates/gpui_linux/src/linux/x11/event.rs index 3fb916425b..47b7a7ed84 100644 --- a/crates/gpui_linux/src/linux/x11/event.rs +++ b/crates/gpui_linux/src/linux/x11/event.rs @@ -1,154 +1,154 @@ use x11rb::protocol::{ - xinput, - xproto::{self, ModMask}, + xinput, + xproto::{self, ModMask}, }; use gpui::{Modifiers, MouseButton, NavigationDirection}; pub(crate) enum ButtonOrScroll { - Button(MouseButton), - Scroll(ScrollDirection), + Button(MouseButton), + Scroll(ScrollDirection), } pub(crate) enum ScrollDirection { - Up, - Down, - Left, - Right, + Up, + Down, + Left, + Right, } pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option { - Some(match detail { - 1 => ButtonOrScroll::Button(MouseButton::Left), - 2 => ButtonOrScroll::Button(MouseButton::Middle), - 3 => ButtonOrScroll::Button(MouseButton::Right), - 4 => ButtonOrScroll::Scroll(ScrollDirection::Up), - 5 => ButtonOrScroll::Scroll(ScrollDirection::Down), - 6 => ButtonOrScroll::Scroll(ScrollDirection::Left), - 7 => ButtonOrScroll::Scroll(ScrollDirection::Right), - 8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)), - 9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)), - _ => return None, - }) + Some(match detail { + 1 => ButtonOrScroll::Button(MouseButton::Left), + 2 => ButtonOrScroll::Button(MouseButton::Middle), + 3 => ButtonOrScroll::Button(MouseButton::Right), + 4 => ButtonOrScroll::Scroll(ScrollDirection::Up), + 5 => ButtonOrScroll::Scroll(ScrollDirection::Down), + 6 => ButtonOrScroll::Scroll(ScrollDirection::Left), + 7 => ButtonOrScroll::Scroll(ScrollDirection::Right), + 8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)), + 9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)), + _ => return None, + }) } pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers { - Modifiers { - control: state.contains(xproto::KeyButMask::CONTROL), - alt: state.contains(xproto::KeyButMask::MOD1), - shift: state.contains(xproto::KeyButMask::SHIFT), - platform: state.contains(xproto::KeyButMask::MOD4), - function: false, - } + Modifiers { + control: state.contains(xproto::KeyButMask::CONTROL), + alt: state.contains(xproto::KeyButMask::MOD1), + shift: state.contains(xproto::KeyButMask::SHIFT), + platform: state.contains(xproto::KeyButMask::MOD4), + function: false, + } } pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers { - Modifiers { - control: modifier_info.effective as u16 & ModMask::CONTROL.bits() - == ModMask::CONTROL.bits(), - alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(), - shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(), - platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(), - function: false, - } + Modifiers { + control: modifier_info.effective as u16 & ModMask::CONTROL.bits() + == ModMask::CONTROL.bits(), + alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(), + shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(), + platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(), + function: false, + } } pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option { - Some(if button_mask & 2 == 2 { - MouseButton::Left - } else if button_mask & 4 == 4 { - MouseButton::Middle - } else if button_mask & 8 == 8 { - MouseButton::Right - } else { - return None; - }) + Some(if button_mask & 2 == 2 { + MouseButton::Left + } else if button_mask & 4 == 4 { + MouseButton::Middle + } else if button_mask & 8 == 8 { + MouseButton::Right + } else { + return None; + }) } pub(crate) fn get_valuator_axis_index( - valuator_mask: &Vec, - valuator_number: u16, + valuator_mask: &Vec, + valuator_number: u16, ) -> Option { - // XInput valuator masks have a 1 at the bit indexes corresponding to each - // valuator present in this event's axisvalues. Axisvalues is ordered from - // lowest valuator number to highest, so counting bits before the 1 bit for - // this valuator yields the index in axisvalues. - if bit_is_set_in_vec(valuator_mask, valuator_number) { - Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) - } else { - None - } + // XInput valuator masks have a 1 at the bit indexes corresponding to each + // valuator present in this event's axisvalues. Axisvalues is ordered from + // lowest valuator number to highest, so counting bits before the 1 bit for + // this valuator yields the index in axisvalues. + if bit_is_set_in_vec(valuator_mask, valuator_number) { + Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) + } else { + None + } } /// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`. fn popcount_upto_bit_index(bit_vec: &Vec, bit_index: u16) -> u32 { - let array_index = bit_index as usize / 32; - let popcount: u32 = bit_vec - .get(array_index) - .map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones()); - if array_index == 0 { - popcount - } else { - // Valuator numbers over 32 probably never occur for scroll position, but may as well - // support it. - let leading_popcount: u32 = bit_vec - .iter() - .take(array_index) - .map(|bits| bits.count_ones()) - .sum(); - popcount + leading_popcount - } + let array_index = bit_index as usize / 32; + let popcount: u32 = bit_vec + .get(array_index) + .map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones()); + if array_index == 0 { + popcount + } else { + // Valuator numbers over 32 probably never occur for scroll position, but may as well + // support it. + let leading_popcount: u32 = bit_vec + .iter() + .take(array_index) + .map(|bits| bits.count_ones()) + .sum(); + popcount + leading_popcount + } } fn bit_is_set_in_vec(bit_vec: &Vec, bit_index: u16) -> bool { - let array_index = bit_index as usize / 32; - bit_vec - .get(array_index) - .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) + let array_index = bit_index as usize / 32; + bit_vec + .get(array_index) + .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) } fn bit_is_set(bits: u32, bit_index: u16) -> bool { - bits & (1 << bit_index) != 0 + bits & (1 << bit_index) != 0 } /// Sets every bit with `i >= bit_index` to 0. fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 { - if bit_index == 0 { - 0 - } else if bit_index >= 32 { - u32::MAX - } else { - bits & ((1 << bit_index) - 1) - } + if bit_index == 0 { + 0 + } else if bit_index >= 32 { + u32::MAX + } else { + bits & ((1 << bit_index) - 1) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_get_valuator_axis_index() { - assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0)); - assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1)); - assert!(get_valuator_axis_index(&vec![0b11], 2) == None); + #[test] + fn test_get_valuator_axis_index() { + assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b11], 2) == None); - assert!(get_valuator_axis_index(&vec![0b100], 0) == None); - assert!(get_valuator_axis_index(&vec![0b100], 1) == None); - assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0)); - assert!(get_valuator_axis_index(&vec![0b100], 3) == None); + assert!(get_valuator_axis_index(&vec![0b100], 0) == None); + assert!(get_valuator_axis_index(&vec![0b100], 1) == None); + assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b100], 3) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0)); - assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1)); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0)); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1)); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2)); - assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None); - assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3)); - } + assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3)); + } } diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index e56a47f54d..7203406a41 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -3,11 +3,11 @@ use x11rb::connection::RequestConnection; use crate::linux::X11ClientStatePtr; use gpui::{ - AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, - Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, - Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, - WindowDecorations, WindowKind, WindowParams, px, + AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, + Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, + WindowDecorations, WindowKind, WindowParams, px, }; use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig}; @@ -15,223 +15,223 @@ use collections::FxHashSet; use raw_window_handle as rwh; use util::{ResultExt, maybe}; use x11rb::{ - connection::Connection, - cookie::{Cookie, VoidCookie}, - errors::ConnectionError, - properties::WmSizeHints, - protocol::{ - sync, - xinput::{self, ConnectionExt as _}, - xproto::{self, ClientMessageEvent, ConnectionExt, TranslateCoordinatesReply}, - }, - wrapper::ConnectionExt as _, - xcb_ffi::XCBConnection, + connection::Connection, + cookie::{Cookie, VoidCookie}, + errors::ConnectionError, + properties::WmSizeHints, + protocol::{ + sync, + xinput::{self, ConnectionExt as _}, + xproto::{self, ClientMessageEvent, ConnectionExt, TranslateCoordinatesReply}, + }, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, }; use std::{ - cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ptr::NonNull, rc::Rc, sync::Arc, + cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ptr::NonNull, rc::Rc, sync::Arc, }; use super::{X11Display, XINPUT_ALL_DEVICE_GROUPS, XINPUT_ALL_DEVICES}; x11rb::atom_manager! { - pub XcbAtoms: AtomsCookie { - XA_ATOM, - XdndAware, - XdndStatus, - XdndEnter, - XdndLeave, - XdndPosition, - XdndSelection, - XdndDrop, - XdndFinished, - XdndTypeList, - XdndActionCopy, - TextUriList: b"text/uri-list", - UTF8_STRING, - TEXT, - STRING, - TEXT_PLAIN_UTF8: b"text/plain;charset=utf-8", - TEXT_PLAIN: b"text/plain", - XDND_DATA, - WM_PROTOCOLS, - WM_DELETE_WINDOW, - WM_CHANGE_STATE, - WM_TRANSIENT_FOR, - _NET_WM_PID, - _NET_WM_NAME, - _NET_WM_ICON, - _NET_WM_STATE, - _NET_WM_STATE_MAXIMIZED_VERT, - _NET_WM_STATE_MAXIMIZED_HORZ, - _NET_WM_STATE_FULLSCREEN, - _NET_WM_STATE_HIDDEN, - _NET_WM_STATE_FOCUSED, - _NET_ACTIVE_WINDOW, - _NET_WM_SYNC_REQUEST, - _NET_WM_SYNC_REQUEST_COUNTER, - _NET_WM_BYPASS_COMPOSITOR, - _NET_WM_MOVERESIZE, - _NET_WM_WINDOW_TYPE, - _NET_WM_WINDOW_TYPE_NOTIFICATION, - _NET_WM_WINDOW_TYPE_DIALOG, - _NET_WM_STATE_MODAL, - _NET_WM_SYNC, - _NET_SUPPORTED, - _MOTIF_WM_HINTS, - _GTK_SHOW_WINDOW_MENU, - _GTK_FRAME_EXTENTS, - _GTK_EDGE_CONSTRAINTS, - _NET_CLIENT_LIST_STACKING, - } + pub XcbAtoms: AtomsCookie { + XA_ATOM, + XdndAware, + XdndStatus, + XdndEnter, + XdndLeave, + XdndPosition, + XdndSelection, + XdndDrop, + XdndFinished, + XdndTypeList, + XdndActionCopy, + TextUriList: b"text/uri-list", + UTF8_STRING, + TEXT, + STRING, + TEXT_PLAIN_UTF8: b"text/plain;charset=utf-8", + TEXT_PLAIN: b"text/plain", + XDND_DATA, + WM_PROTOCOLS, + WM_DELETE_WINDOW, + WM_CHANGE_STATE, + WM_TRANSIENT_FOR, + _NET_WM_PID, + _NET_WM_NAME, + _NET_WM_ICON, + _NET_WM_STATE, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_FOCUSED, + _NET_ACTIVE_WINDOW, + _NET_WM_SYNC_REQUEST, + _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_BYPASS_COMPOSITOR, + _NET_WM_MOVERESIZE, + _NET_WM_WINDOW_TYPE, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_STATE_MODAL, + _NET_WM_SYNC, + _NET_SUPPORTED, + _MOTIF_WM_HINTS, + _GTK_SHOW_WINDOW_MENU, + _GTK_FRAME_EXTENTS, + _GTK_EDGE_CONSTRAINTS, + _NET_CLIENT_LIST_STACKING, + } } fn query_render_extent( - xcb: &Rc, - x_window: xproto::Window, + xcb: &Rc, + x_window: xproto::Window, ) -> anyhow::Result> { - let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; - Ok(Size { - width: DevicePixels(reply.width as i32), - height: DevicePixels(reply.height as i32), - }) + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + Ok(Size { + width: DevicePixels(reply.width as i32), + height: DevicePixels(reply.height as i32), + }) } fn resize_edge_to_moveresize(edge: ResizeEdge) -> u32 { - match edge { - ResizeEdge::TopLeft => 0, - ResizeEdge::Top => 1, - ResizeEdge::TopRight => 2, - ResizeEdge::Right => 3, - ResizeEdge::BottomRight => 4, - ResizeEdge::Bottom => 5, - ResizeEdge::BottomLeft => 6, - ResizeEdge::Left => 7, - } + match edge { + ResizeEdge::TopLeft => 0, + ResizeEdge::Top => 1, + ResizeEdge::TopRight => 2, + ResizeEdge::Right => 3, + ResizeEdge::BottomRight => 4, + ResizeEdge::Bottom => 5, + ResizeEdge::BottomLeft => 6, + ResizeEdge::Left => 7, + } } #[derive(Debug)] struct EdgeConstraints { - top_tiled: bool, - #[allow(dead_code)] - top_resizable: bool, + top_tiled: bool, + #[allow(dead_code)] + top_resizable: bool, - right_tiled: bool, - #[allow(dead_code)] - right_resizable: bool, + right_tiled: bool, + #[allow(dead_code)] + right_resizable: bool, - bottom_tiled: bool, - #[allow(dead_code)] - bottom_resizable: bool, + bottom_tiled: bool, + #[allow(dead_code)] + bottom_resizable: bool, - left_tiled: bool, - #[allow(dead_code)] - left_resizable: bool, + left_tiled: bool, + #[allow(dead_code)] + left_resizable: bool, } impl EdgeConstraints { - fn from_atom(atom: u32) -> Self { - EdgeConstraints { - top_tiled: (atom & (1 << 0)) != 0, - top_resizable: (atom & (1 << 1)) != 0, - right_tiled: (atom & (1 << 2)) != 0, - right_resizable: (atom & (1 << 3)) != 0, - bottom_tiled: (atom & (1 << 4)) != 0, - bottom_resizable: (atom & (1 << 5)) != 0, - left_tiled: (atom & (1 << 6)) != 0, - left_resizable: (atom & (1 << 7)) != 0, - } - } + fn from_atom(atom: u32) -> Self { + EdgeConstraints { + top_tiled: (atom & (1 << 0)) != 0, + top_resizable: (atom & (1 << 1)) != 0, + right_tiled: (atom & (1 << 2)) != 0, + right_resizable: (atom & (1 << 3)) != 0, + bottom_tiled: (atom & (1 << 4)) != 0, + bottom_resizable: (atom & (1 << 5)) != 0, + left_tiled: (atom & (1 << 6)) != 0, + left_resizable: (atom & (1 << 7)) != 0, + } + } - fn to_tiling(&self) -> Tiling { - Tiling { - top: self.top_tiled, - right: self.right_tiled, - bottom: self.bottom_tiled, - left: self.left_tiled, - } - } + fn to_tiling(&self) -> Tiling { + Tiling { + top: self.top_tiled, + right: self.right_tiled, + bottom: self.bottom_tiled, + left: self.left_tiled, + } + } } #[derive(Copy, Clone, Debug)] struct Visual { - id: xproto::Visualid, - colormap: u32, - depth: u8, + id: xproto::Visualid, + colormap: u32, + depth: u8, } struct VisualSet { - inherit: Visual, - opaque: Option, - transparent: Option, - root: u32, - black_pixel: u32, + inherit: Visual, + opaque: Option, + transparent: Option, + root: u32, + black_pixel: u32, } fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { - let screen = &xcb.setup().roots[screen_index]; - let mut set = VisualSet { - inherit: Visual { - id: screen.root_visual, - colormap: screen.default_colormap, - depth: screen.root_depth, - }, - opaque: None, - transparent: None, - root: screen.root, - black_pixel: screen.black_pixel, - }; + let screen = &xcb.setup().roots[screen_index]; + let mut set = VisualSet { + inherit: Visual { + id: screen.root_visual, + colormap: screen.default_colormap, + depth: screen.root_depth, + }, + opaque: None, + transparent: None, + root: screen.root, + black_pixel: screen.black_pixel, + }; - for depth_info in screen.allowed_depths.iter() { - for visual_type in depth_info.visuals.iter() { - let visual = Visual { - id: visual_type.visual_id, - colormap: 0, - depth: depth_info.depth, - }; - log::debug!( - "Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}", - visual_type.visual_id, - visual_type.class, - depth_info.depth, - visual_type.bits_per_rgb_value, - visual_type.red_mask, - visual_type.green_mask, - visual_type.blue_mask, - ); + for depth_info in screen.allowed_depths.iter() { + for visual_type in depth_info.visuals.iter() { + let visual = Visual { + id: visual_type.visual_id, + colormap: 0, + depth: depth_info.depth, + }; + log::debug!( + "Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}", + visual_type.visual_id, + visual_type.class, + depth_info.depth, + visual_type.bits_per_rgb_value, + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ); - if ( - visual_type.red_mask, - visual_type.green_mask, - visual_type.blue_mask, - ) != (0xFF0000, 0xFF00, 0xFF) - { - continue; - } - let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask; - let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1); + if ( + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ) != (0xFF0000, 0xFF00, 0xFF) + { + continue; + } + let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask; + let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1); - if alpha_mask == 0 { - if set.opaque.is_none() { - set.opaque = Some(visual); - } - } else { - if set.transparent.is_none() { - set.transparent = Some(visual); - } - } - } - } + if alpha_mask == 0 { + if set.opaque.is_none() { + set.opaque = Some(visual); + } + } else { + if set.transparent.is_none() { + set.transparent = Some(visual); + } + } + } + } - set + set } #[derive(Debug, Clone, Copy)] struct RawWindow { - connection: *mut c_void, - screen_id: usize, - window_id: u32, - visual_id: u32, + connection: *mut c_void, + screen_id: usize, + window_id: u32, + visual_id: u32, } // Safety: The raw pointers in RawWindow point to X11 connection @@ -242,1753 +242,1753 @@ unsafe impl Sync for RawWindow {} #[derive(Default)] pub struct Callbacks { - request_frame: Option>, - input: Option gpui::DispatchEventResult>>, - active_status_change: Option>, - hovered_status_change: Option>, - resize: Option, f32)>>, - moved: Option>, - should_close: Option bool>>, - close: Option>, - appearance_changed: Option>, - button_layout_changed: Option>, + request_frame: Option>, + input: Option gpui::DispatchEventResult>>, + active_status_change: Option>, + hovered_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, + button_layout_changed: Option>, } pub struct X11WindowState { - pub destroyed: bool, - parent: Option, - children: FxHashSet, - client: X11ClientStatePtr, - executor: ForegroundExecutor, - atoms: XcbAtoms, - x_root_window: xproto::Window, - x_screen_index: usize, - visual_id: u32, - pub(crate) counter_id: sync::Counter, - pub(crate) last_sync_counter: Option, - bounds: Bounds, - scale_factor: f32, - renderer: WgpuRenderer, - display: Rc, - input_handler: Option, - appearance: WindowAppearance, - background_appearance: WindowBackgroundAppearance, - maximized_vertical: bool, - maximized_horizontal: bool, - hidden: bool, - active: bool, - hovered: bool, - pub(crate) force_render_after_recovery: bool, - fullscreen: bool, - client_side_decorations_supported: bool, - decorations: WindowDecorations, - edge_constraints: Option, - pub handle: AnyWindowHandle, - last_insets: [u32; 4], - accesskit_adapter: Option, + pub destroyed: bool, + parent: Option, + children: FxHashSet, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + atoms: XcbAtoms, + x_root_window: xproto::Window, + x_screen_index: usize, + visual_id: u32, + pub(crate) counter_id: sync::Counter, + pub(crate) last_sync_counter: Option, + bounds: Bounds, + scale_factor: f32, + renderer: WgpuRenderer, + display: Rc, + input_handler: Option, + appearance: WindowAppearance, + background_appearance: WindowBackgroundAppearance, + maximized_vertical: bool, + maximized_horizontal: bool, + hidden: bool, + active: bool, + hovered: bool, + pub(crate) force_render_after_recovery: bool, + fullscreen: bool, + client_side_decorations_supported: bool, + decorations: WindowDecorations, + edge_constraints: Option, + pub handle: AnyWindowHandle, + last_insets: [u32; 4], + accesskit_adapter: Option, } impl X11WindowState { - fn is_transparent(&self) -> bool { - self.background_appearance != WindowBackgroundAppearance::Opaque - } + fn is_transparent(&self) -> bool { + self.background_appearance != WindowBackgroundAppearance::Opaque + } } #[derive(Clone)] pub(crate) struct X11WindowStatePtr { - pub state: Rc>, - pub(crate) callbacks: Rc>, - xcb: Rc, - pub(crate) x_window: xproto::Window, + pub state: Rc>, + pub(crate) callbacks: Rc>, + xcb: Rc, + pub(crate) x_window: xproto::Window, } impl rwh::HasWindowHandle for RawWindow { - fn window_handle(&self) -> Result, rwh::HandleError> { - let Some(non_zero) = NonZeroU32::new(self.window_id) else { - log::error!("RawWindow.window_id zero when getting window handle."); - return Err(rwh::HandleError::Unavailable); - }; - let mut handle = rwh::XcbWindowHandle::new(non_zero); - handle.visual_id = NonZeroU32::new(self.visual_id); - Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) - } + fn window_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonZeroU32::new(self.window_id) else { + log::error!("RawWindow.window_id zero when getting window handle."); + return Err(rwh::HandleError::Unavailable); + }; + let mut handle = rwh::XcbWindowHandle::new(non_zero); + handle.visual_id = NonZeroU32::new(self.visual_id); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } } impl rwh::HasDisplayHandle for RawWindow { - fn display_handle(&self) -> Result, rwh::HandleError> { - let Some(non_zero) = NonNull::new(self.connection) else { - log::error!("Null RawWindow.connection when getting display handle."); - return Err(rwh::HandleError::Unavailable); - }; - let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); - Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) - } + fn display_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonNull::new(self.connection) else { + log::error!("Null RawWindow.connection when getting display handle."); + return Err(rwh::HandleError::Unavailable); + }; + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } } impl rwh::HasWindowHandle for X11Window { - fn window_handle(&self) -> Result, rwh::HandleError> { - let Some(non_zero) = NonZeroU32::new(self.0.x_window) else { - return Err(rwh::HandleError::Unavailable); - }; - let handle = rwh::XcbWindowHandle::new(non_zero); - Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) - } + fn window_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonZeroU32::new(self.0.x_window) else { + return Err(rwh::HandleError::Unavailable); + }; + let handle = rwh::XcbWindowHandle::new(non_zero); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } } impl rwh::HasDisplayHandle for X11Window { - fn display_handle(&self) -> Result, rwh::HandleError> { - let connection = - as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(&*self.0.xcb) - as *mut _; - let Some(non_zero) = NonNull::new(connection) else { - return Err(rwh::HandleError::Unavailable); - }; - let screen_id = { - let state = self.0.state.borrow(); - u64::from(state.display.id()) as i32 - }; - let handle = rwh::XcbDisplayHandle::new(Some(non_zero), screen_id); - Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) - } + fn display_handle(&self) -> Result, rwh::HandleError> { + let connection = + as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(&*self.0.xcb) + as *mut _; + let Some(non_zero) = NonNull::new(connection) else { + return Err(rwh::HandleError::Unavailable); + }; + let screen_id = { + let state = self.0.state.borrow(); + u64::from(state.display.id()) as i32 + }; + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), screen_id); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } } pub(crate) fn xcb_flush(xcb: &XCBConnection) { - xcb.flush() - .map_err(handle_connection_error) - .context("X11 flush failed") - .log_err(); + xcb.flush() + .map_err(handle_connection_error) + .context("X11 flush failed") + .log_err(); } pub(crate) fn check_reply( - failure_context: F, - result: Result, ConnectionError>, + failure_context: F, + result: Result, ConnectionError>, ) -> anyhow::Result<()> where - E: Display + Send + Sync + 'static, - F: FnOnce() -> E, - C: RequestConnection, + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, { - result - .map_err(handle_connection_error) - .and_then(|response| response.check().map_err(|reply_error| anyhow!(reply_error))) - .with_context(failure_context) + result + .map_err(handle_connection_error) + .and_then(|response| response.check().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) } pub(crate) fn get_reply( - failure_context: F, - result: Result, ConnectionError>, + failure_context: F, + result: Result, ConnectionError>, ) -> anyhow::Result where - E: Display + Send + Sync + 'static, - F: FnOnce() -> E, - C: RequestConnection, - O: x11rb::x11_utils::TryParse, + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, + O: x11rb::x11_utils::TryParse, { - result - .map_err(handle_connection_error) - .and_then(|response| response.reply().map_err(|reply_error| anyhow!(reply_error))) - .with_context(failure_context) + result + .map_err(handle_connection_error) + .and_then(|response| response.reply().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) } /// Convert X11 connection errors to `anyhow::Error` and panic for unrecoverable errors. pub(crate) fn handle_connection_error(err: ConnectionError) -> anyhow::Error { - match err { - ConnectionError::UnknownError => anyhow!("X11 connection: Unknown error"), - ConnectionError::UnsupportedExtension => anyhow!("X11 connection: Unsupported extension"), - ConnectionError::MaximumRequestLengthExceeded => { - anyhow!("X11 connection: Maximum request length exceeded") - } - ConnectionError::FdPassingFailed => { - panic!("X11 connection: File descriptor passing failed") - } - ConnectionError::ParseError(parse_error) => { - anyhow!(parse_error).context("Parse error in X11 response") - } - ConnectionError::InsufficientMemory => panic!("X11 connection: Insufficient memory"), - ConnectionError::IoError(err) => anyhow!(err).context("X11 connection: IOError"), - _ => anyhow!(err), - } + match err { + ConnectionError::UnknownError => anyhow!("X11 connection: Unknown error"), + ConnectionError::UnsupportedExtension => anyhow!("X11 connection: Unsupported extension"), + ConnectionError::MaximumRequestLengthExceeded => { + anyhow!("X11 connection: Maximum request length exceeded") + } + ConnectionError::FdPassingFailed => { + panic!("X11 connection: File descriptor passing failed") + } + ConnectionError::ParseError(parse_error) => { + anyhow!(parse_error).context("Parse error in X11 response") + } + ConnectionError::InsufficientMemory => panic!("X11 connection: Insufficient memory"), + ConnectionError::IoError(err) => anyhow!(err).context("X11 connection: IOError"), + _ => anyhow!(err), + } } impl X11WindowState { - pub fn new( - handle: AnyWindowHandle, - client: X11ClientStatePtr, - executor: ForegroundExecutor, - gpu_context: gpui_wgpu::GpuContext, - compositor_gpu: Option, - gpu_requirements: Option, - params: WindowParams, - xcb: &Rc, - client_side_decorations_supported: bool, - x_main_screen_index: usize, - x_window: xproto::Window, - atoms: &XcbAtoms, - scale_factor: f32, - appearance: WindowAppearance, - parent_window: Option, - supports_xinput_gestures: bool, - is_bgr: bool, - ) -> anyhow::Result { - let x_screen_index = params - .display_id - .map_or(x_main_screen_index, |did| u64::from(did) as usize); + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + gpu_requirements: Option, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + supports_xinput_gestures: bool, + is_bgr: bool, + ) -> anyhow::Result { + let x_screen_index = params + .display_id + .map_or(x_main_screen_index, |did| u64::from(did) as usize); - let visual_set = find_visuals(xcb, x_screen_index); + let visual_set = find_visuals(xcb, x_screen_index); - let visual = match visual_set.transparent { - Some(visual) => visual, - None => { - log::warn!("Unable to find a transparent visual",); - visual_set.inherit - } - }; - log::info!("Using {:?}", visual); + let visual = match visual_set.transparent { + Some(visual) => visual, + None => { + log::warn!("Unable to find a transparent visual",); + visual_set.inherit + } + }; + log::info!("Using {:?}", visual); - let colormap = if visual.colormap != 0 { - visual.colormap - } else { - let id = xcb.generate_id()?; - log::info!("Creating colormap {}", id); - check_reply( - || format!("X11 CreateColormap failed. id: {}", id), - xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), - )?; - id - }; + let colormap = if visual.colormap != 0 { + visual.colormap + } else { + let id = xcb.generate_id()?; + log::info!("Creating colormap {}", id); + check_reply( + || format!("X11 CreateColormap failed. id: {}", id), + xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), + )?; + id + }; - let win_aux = xproto::CreateWindowAux::new() - // https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh - .border_pixel(visual_set.black_pixel) - .colormap(colormap) - .override_redirect((params.kind == WindowKind::PopUp) as u32) - .event_mask( - xproto::EventMask::EXPOSURE - | xproto::EventMask::STRUCTURE_NOTIFY - | xproto::EventMask::FOCUS_CHANGE - | xproto::EventMask::KEY_PRESS - | xproto::EventMask::KEY_RELEASE - | xproto::EventMask::PROPERTY_CHANGE - | xproto::EventMask::VISIBILITY_CHANGE, - ); + let win_aux = xproto::CreateWindowAux::new() + // https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh + .border_pixel(visual_set.black_pixel) + .colormap(colormap) + .override_redirect((params.kind == WindowKind::PopUp) as u32) + .event_mask( + xproto::EventMask::EXPOSURE + | xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::FOCUS_CHANGE + | xproto::EventMask::KEY_PRESS + | xproto::EventMask::KEY_RELEASE + | xproto::EventMask::PROPERTY_CHANGE + | xproto::EventMask::VISIBILITY_CHANGE, + ); - let mut bounds = params.bounds.to_device_pixels(scale_factor); - if bounds.size.width.0 == 0 || bounds.size.height.0 == 0 { - log::warn!( - "Window bounds contain a zero value. height={}, width={}. Falling back to defaults.", - bounds.size.height.0, - bounds.size.width.0 - ); - bounds.size.width = 800.into(); - bounds.size.height = 600.into(); - } + let mut bounds = params.bounds.to_device_pixels(scale_factor); + if bounds.size.width.0 == 0 || bounds.size.height.0 == 0 { + log::warn!( + "Window bounds contain a zero value. height={}, width={}. Falling back to defaults.", + bounds.size.height.0, + bounds.size.width.0 + ); + bounds.size.width = 800.into(); + bounds.size.height = 600.into(); + } - check_reply( - || { - format!( - "X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", - visual.depth, - x_window, - visual_set.root, - bounds.origin.x.0 + 2, - bounds.origin.y.0, - bounds.size.width.0, - bounds.size.height.0 - ) - }, - xcb.create_window( - visual.depth, - x_window, - visual_set.root, - (bounds.origin.x.0 + 2) as i16, - bounds.origin.y.0 as i16, - bounds.size.width.0 as u16, - bounds.size.height.0 as u16, - 0, - xproto::WindowClass::INPUT_OUTPUT, - visual.id, - &win_aux, - ), - )?; + check_reply( + || { + format!( + "X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", + visual.depth, + x_window, + visual_set.root, + bounds.origin.x.0 + 2, + bounds.origin.y.0, + bounds.size.width.0, + bounds.size.height.0 + ) + }, + xcb.create_window( + visual.depth, + x_window, + visual_set.root, + (bounds.origin.x.0 + 2) as i16, + bounds.origin.y.0 as i16, + bounds.size.width.0 as u16, + bounds.size.height.0 as u16, + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual.id, + &win_aux, + ), + )?; - // Collect errors during setup, so that window can be destroyed on failure. - let setup_result = maybe!({ - let pid = std::process::id(); - check_reply( - || "X11 ChangeProperty for _NET_WM_PID failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_PID, - xproto::AtomEnum::CARDINAL, - &[pid], - ), - )?; + // Collect errors during setup, so that window can be destroyed on failure. + let setup_result = maybe!({ + let pid = std::process::id(); + check_reply( + || "X11 ChangeProperty for _NET_WM_PID failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_PID, + xproto::AtomEnum::CARDINAL, + &[pid], + ), + )?; - let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; - if reply.x == 0 && reply.y == 0 { - bounds.origin.x.0 += 2; - // Work around a bug where our rendered content appears - // outside the window bounds when opened at the default position - // (14px, 49px on X + Gnome + Ubuntu 22). - let x = bounds.origin.x.0; - let y = bounds.origin.y.0; - check_reply( - || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), - xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), - )?; - } - if let Some(titlebar) = params.titlebar - && let Some(title) = titlebar.title - { - check_reply( - || "X11 ChangeProperty8 on WM_NAME failed.", - xcb.change_property8( - xproto::PropMode::REPLACE, - x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ), - )?; - check_reply( - || "X11 ChangeProperty8 on _NET_WM_NAME failed.", - xcb.change_property8( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_NAME, - atoms.UTF8_STRING, - title.as_bytes(), - ), - )?; - } + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + if reply.x == 0 && reply.y == 0 { + bounds.origin.x.0 += 2; + // Work around a bug where our rendered content appears + // outside the window bounds when opened at the default position + // (14px, 49px on X + Gnome + Ubuntu 22). + let x = bounds.origin.x.0; + let y = bounds.origin.y.0; + check_reply( + || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), + xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), + )?; + } + if let Some(titlebar) = params.titlebar + && let Some(title) = titlebar.title + { + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_NAME, + atoms.UTF8_STRING, + title.as_bytes(), + ), + )?; + } - if params.kind == WindowKind::PopUp { - check_reply( - || "X11 ChangeProperty32 setting window type for pop-up failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_WINDOW_TYPE, - xproto::AtomEnum::ATOM, - &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], - ), - )?; - } + if params.kind == WindowKind::PopUp { + check_reply( + || "X11 ChangeProperty32 setting window type for pop-up failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], + ), + )?; + } - if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { - if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) { - // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set - // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to - // place the floating window in relation to the main window. - // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html - check_reply( - || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms.WM_TRANSIENT_FOR, - xproto::AtomEnum::WINDOW, - &[parent_window], - ), - )?; - } - } + if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { + if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) { + // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set + // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to + // place the floating window in relation to the main window. + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_TRANSIENT_FOR, + xproto::AtomEnum::WINDOW, + &[parent_window], + ), + )?; + } + } - let parent = if params.kind == WindowKind::Dialog - && let Some(parent) = parent_window - { - parent.add_child(x_window); + let parent = if params.kind == WindowKind::Dialog + && let Some(parent) = parent_window + { + parent.add_child(x_window); - Some(parent) - } else { - None - }; + Some(parent) + } else { + None + }; - if params.kind == WindowKind::Dialog { - // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window - // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html - check_reply( - || "X11 ChangeProperty32 setting window type for dialog window failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_WINDOW_TYPE, - xproto::AtomEnum::ATOM, - &[atoms._NET_WM_WINDOW_TYPE_DIALOG], - ), - )?; + if params.kind == WindowKind::Dialog { + // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting window type for dialog window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_DIALOG], + ), + )?; - // We set the modal state for dialog windows, so that the window manager - // can handle it appropriately (e.g., prevent interaction with the parent window - // while the dialog is open). - check_reply( - || "X11 ChangeProperty32 setting modal state for dialog window failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_STATE, - xproto::AtomEnum::ATOM, - &[atoms._NET_WM_STATE_MODAL], - ), - )?; - } + // We set the modal state for dialog windows, so that the window manager + // can handle it appropriately (e.g., prevent interaction with the parent window + // while the dialog is open). + check_reply( + || "X11 ChangeProperty32 setting modal state for dialog window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_STATE_MODAL], + ), + )?; + } - check_reply( - || "X11 ChangeProperty32 setting protocols failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms.WM_PROTOCOLS, - xproto::AtomEnum::ATOM, - &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], - ), - )?; + check_reply( + || "X11 ChangeProperty32 setting protocols failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_PROTOCOLS, + xproto::AtomEnum::ATOM, + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ), + )?; - get_reply( - || "X11 sync protocol initialize failed.", - sync::initialize(xcb, 3, 1), - )?; - let sync_request_counter = xcb.generate_id()?; - check_reply( - || "X11 sync CreateCounter failed.", - sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), - )?; + get_reply( + || "X11 sync protocol initialize failed.", + sync::initialize(xcb, 3, 1), + )?; + let sync_request_counter = xcb.generate_id()?; + check_reply( + || "X11 sync CreateCounter failed.", + sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), + )?; - check_reply( - || "X11 ChangeProperty32 setting sync request counter failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_SYNC_REQUEST_COUNTER, - xproto::AtomEnum::CARDINAL, - &[sync_request_counter], - ), - )?; + check_reply( + || "X11 ChangeProperty32 setting sync request counter failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], + ), + )?; - let mut xi_event_mask = xinput::XIEventMask::MOTION - | xinput::XIEventMask::BUTTON_PRESS - | xinput::XIEventMask::BUTTON_RELEASE - | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE; - if supports_xinput_gestures { - // x11rb 0.13 doesn't define XIEventMask constants for gesture - // events, so we construct them from the event opcodes (each - // XInput event type N maps to mask bit N). - xi_event_mask |= - xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT) - | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT) - | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT); - } - check_reply( - || "X11 XiSelectEvents failed.", - xcb.xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICE_GROUPS, - mask: vec![xi_event_mask], - }], - ), - )?; + let mut xi_event_mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE; + if supports_xinput_gestures { + // x11rb 0.13 doesn't define XIEventMask constants for gesture + // events, so we construct them from the event opcodes (each + // XInput event type N maps to mask bit N). + xi_event_mask |= + xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT); + } + check_reply( + || "X11 XiSelectEvents failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICE_GROUPS, + mask: vec![xi_event_mask], + }], + ), + )?; - check_reply( - || "X11 XiSelectEvents for device changes failed.", - xcb.xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICES, - mask: vec![ - xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, - ], - }], - ), - )?; + check_reply( + || "X11 XiSelectEvents for device changes failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICES, + mask: vec![ + xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, + ], + }], + ), + )?; - xcb_flush(xcb); + xcb_flush(xcb); - let mut renderer = { - let raw_window = RawWindow { - connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( - xcb, - ) as *mut _, - screen_id: x_screen_index, - window_id: x_window, - visual_id: visual.id, - }; - let config = WgpuSurfaceConfig { - // Note: this has to be done after the GPU init, or otherwise - // the sizes are immediately invalidated. - size: query_render_extent(xcb, x_window)?, - // We set it to transparent by default, even if we have client-side - // decorations, since those seem to work on X11 even without `true` here. - // If the window appearance changes, then the renderer will get updated - // too - transparent: false, - preferred_present_mode: None, - }; - WgpuRenderer::new( - gpu_context, - &raw_window, - config, - compositor_gpu, - gpu_requirements, - )? - }; + let mut renderer = { + let raw_window = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + xcb, + ) as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let config = WgpuSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb, x_window)?, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, + preferred_present_mode: None, + }; + WgpuRenderer::new( + gpu_context, + &raw_window, + config, + compositor_gpu, + gpu_requirements, + )? + }; - renderer.set_subpixel_layout(is_bgr); + renderer.set_subpixel_layout(is_bgr); - // Set max window size hints based on the GPU's maximum texture dimension. - // This prevents the window from being resized larger than what the GPU can render. - let max_texture_size = renderer.max_texture_size(); - let mut size_hints = WmSizeHints::new(); - if let Some(size) = params.window_min_size { - size_hints.min_size = - Some((f32::from(size.width) as i32, f32::from(size.height) as i32)); - } - size_hints.max_size = Some((max_texture_size as i32, max_texture_size as i32)); - check_reply( - || { - format!( - "X11 change of WM_SIZE_HINTS failed. max_size: {:?}", - max_texture_size - ) - }, - size_hints.set_normal_hints(xcb, x_window), - )?; + // Set max window size hints based on the GPU's maximum texture dimension. + // This prevents the window from being resized larger than what the GPU can render. + let max_texture_size = renderer.max_texture_size(); + let mut size_hints = WmSizeHints::new(); + if let Some(size) = params.window_min_size { + size_hints.min_size = + Some((f32::from(size.width) as i32, f32::from(size.height) as i32)); + } + size_hints.max_size = Some((max_texture_size as i32, max_texture_size as i32)); + check_reply( + || { + format!( + "X11 change of WM_SIZE_HINTS failed. max_size: {:?}", + max_texture_size + ) + }, + size_hints.set_normal_hints(xcb, x_window), + )?; - if let Some(image) = params.icon { - // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#id-1.6.13 - let property_size = 2 + (image.width() * image.height()) as usize; - let mut property_data: Vec = Vec::with_capacity(property_size); - property_data.push(image.width()); - property_data.push(image.height()); - property_data.extend(image.pixels().map(|px| { - let [r, g, b, a]: [u8; 4] = px.0; - u32::from_le_bytes([b, g, r, a]) - })); + if let Some(image) = params.icon { + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#id-1.6.13 + let property_size = 2 + (image.width() * image.height()) as usize; + let mut property_data: Vec = Vec::with_capacity(property_size); + property_data.push(image.width()); + property_data.push(image.height()); + property_data.extend(image.pixels().map(|px| { + let [r, g, b, a]: [u8; 4] = px.0; + u32::from_le_bytes([b, g, r, a]) + })); - check_reply( - || "X11 ChangeProperty32 for _NET_ICON_NAME failed.", - xcb.change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_ICON, - xproto::AtomEnum::CARDINAL, - &property_data, - ), - )?; - } + check_reply( + || "X11 ChangeProperty32 for _NET_ICON_NAME failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_ICON, + xproto::AtomEnum::CARDINAL, + &property_data, + ), + )?; + } - let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); - Ok(Self { - parent, - children: FxHashSet::default(), - client, - executor, - display, - x_root_window: visual_set.root, - x_screen_index, - visual_id: visual.id, - bounds: bounds.to_pixels(scale_factor), - scale_factor, - renderer, - atoms: *atoms, - input_handler: None, - active: false, - hovered: false, - force_render_after_recovery: false, - fullscreen: false, - maximized_vertical: false, - maximized_horizontal: false, - hidden: false, - appearance, - handle, - background_appearance: WindowBackgroundAppearance::Opaque, - destroyed: false, - client_side_decorations_supported, - decorations: WindowDecorations::Server, - last_insets: [0, 0, 0, 0], - edge_constraints: None, - accesskit_adapter: None, - counter_id: sync_request_counter, - last_sync_counter: None, - }) - }); + Ok(Self { + parent, + children: FxHashSet::default(), + client, + executor, + display, + x_root_window: visual_set.root, + x_screen_index, + visual_id: visual.id, + bounds: bounds.to_pixels(scale_factor), + scale_factor, + renderer, + atoms: *atoms, + input_handler: None, + active: false, + hovered: false, + force_render_after_recovery: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, + appearance, + handle, + background_appearance: WindowBackgroundAppearance::Opaque, + destroyed: false, + client_side_decorations_supported, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + edge_constraints: None, + accesskit_adapter: None, + counter_id: sync_request_counter, + last_sync_counter: None, + }) + }); - if setup_result.is_err() { - check_reply( - || "X11 DestroyWindow failed while cleaning it up after setup failure.", - xcb.destroy_window(x_window), - )?; - xcb_flush(xcb); - } + if setup_result.is_err() { + check_reply( + || "X11 DestroyWindow failed while cleaning it up after setup failure.", + xcb.destroy_window(x_window), + )?; + xcb_flush(xcb); + } - setup_result - } + setup_result + } - fn content_size(&self) -> Size { - self.bounds.size - } + fn content_size(&self) -> Size { + self.bounds.size + } } pub(crate) struct X11Window(pub X11WindowStatePtr); impl Drop for X11Window { - fn drop(&mut self) { - let mut state = self.0.state.borrow_mut(); + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); - if let Some(parent) = state.parent.as_ref() { - parent.state.borrow_mut().children.remove(&self.0.x_window); - } + if let Some(parent) = state.parent.as_ref() { + parent.state.borrow_mut().children.remove(&self.0.x_window); + } - state.renderer.destroy(); + state.renderer.destroy(); - let destroy_x_window = maybe!({ - check_reply( - || "X11 DestroyWindow failure.", - self.0.xcb.destroy_window(self.0.x_window), - )?; - xcb_flush(&self.0.xcb); + let destroy_x_window = maybe!({ + check_reply( + || "X11 DestroyWindow failure.", + self.0.xcb.destroy_window(self.0.x_window), + )?; + xcb_flush(&self.0.xcb); - anyhow::Ok(()) - }) - .log_err(); + anyhow::Ok(()) + }) + .log_err(); - if destroy_x_window.is_some() { - state.destroyed = true; + if destroy_x_window.is_some() { + state.destroyed = true; - let this_ptr = self.0.clone(); - let client_ptr = state.client.clone(); - state - .executor - .spawn(async move { - this_ptr.close(); - client_ptr.drop_window(this_ptr.x_window); - }) - .detach(); - } + let this_ptr = self.0.clone(); + let client_ptr = state.client.clone(); + state + .executor + .spawn(async move { + this_ptr.close(); + client_ptr.drop_window(this_ptr.x_window); + }) + .detach(); + } - drop(state); - } + drop(state); + } } enum WmHintPropertyState { - // Remove = 0, - // Add = 1, - Toggle = 2, + // Remove = 0, + // Add = 1, + Toggle = 2, } impl X11Window { - pub fn new( - handle: AnyWindowHandle, - client: X11ClientStatePtr, - executor: ForegroundExecutor, - gpu_context: gpui_wgpu::GpuContext, - compositor_gpu: Option, - gpu_requirements: Option, - params: WindowParams, - xcb: &Rc, - client_side_decorations_supported: bool, - x_main_screen_index: usize, - x_window: xproto::Window, - atoms: &XcbAtoms, - scale_factor: f32, - appearance: WindowAppearance, - parent_window: Option, - supports_xinput_gestures: bool, - is_bgr: bool, - ) -> anyhow::Result { - let ptr = X11WindowStatePtr { - state: Rc::new(RefCell::new(X11WindowState::new( - handle, - client, - executor, - gpu_context, - compositor_gpu, - gpu_requirements, - params, - xcb, - client_side_decorations_supported, - x_main_screen_index, - x_window, - atoms, - scale_factor, - appearance, - parent_window, - supports_xinput_gestures, - is_bgr, - )?)), - callbacks: Rc::new(RefCell::new(Callbacks::default())), - xcb: xcb.clone(), - x_window, - }; + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + gpu_requirements: Option, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + supports_xinput_gestures: bool, + is_bgr: bool, + ) -> anyhow::Result { + let ptr = X11WindowStatePtr { + state: Rc::new(RefCell::new(X11WindowState::new( + handle, + client, + executor, + gpu_context, + compositor_gpu, + gpu_requirements, + params, + xcb, + client_side_decorations_supported, + x_main_screen_index, + x_window, + atoms, + scale_factor, + appearance, + parent_window, + supports_xinput_gestures, + is_bgr, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + xcb: xcb.clone(), + x_window, + }; - let state = ptr.state.borrow_mut(); - ptr.set_wm_properties(state)?; + let state = ptr.state.borrow_mut(); + ptr.set_wm_properties(state)?; - Ok(Self(ptr)) - } + Ok(Self(ptr)) + } - fn set_wm_hints C>( - &self, - failure_context: F, - wm_hint_property_state: WmHintPropertyState, - prop1: u32, - prop2: u32, - ) -> anyhow::Result<()> { - let state = self.0.state.borrow(); - let message = ClientMessageEvent::new( - 32, - self.0.x_window, - state.atoms._NET_WM_STATE, - [wm_hint_property_state as u32, prop1, prop2, 1, 0], - ); - check_reply( - failure_context, - self.0.xcb.send_event( - false, - state.x_root_window, - xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, - message, - ), - )?; - xcb_flush(&self.0.xcb); - Ok(()) - } + fn set_wm_hints C>( + &self, + failure_context: F, + wm_hint_property_state: WmHintPropertyState, + prop1: u32, + prop2: u32, + ) -> anyhow::Result<()> { + let state = self.0.state.borrow(); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_STATE, + [wm_hint_property_state as u32, prop1, prop2, 1, 0], + ); + check_reply( + failure_context, + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; + xcb_flush(&self.0.xcb); + Ok(()) + } - fn get_root_position( - &self, - position: Point, - ) -> anyhow::Result { - let state = self.0.state.borrow(); - get_reply( - || "X11 TranslateCoordinates failed.", - self.0.xcb.translate_coordinates( - self.0.x_window, - state.x_root_window, - (f32::from(position.x) * state.scale_factor) as i16, - (f32::from(position.y) * state.scale_factor) as i16, - ), - ) - } + fn get_root_position( + &self, + position: Point, + ) -> anyhow::Result { + let state = self.0.state.borrow(); + get_reply( + || "X11 TranslateCoordinates failed.", + self.0.xcb.translate_coordinates( + self.0.x_window, + state.x_root_window, + (f32::from(position.x) * state.scale_factor) as i16, + (f32::from(position.y) * state.scale_factor) as i16, + ), + ) + } - fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { - let state = self.0.state.borrow(); + fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { + let state = self.0.state.borrow(); - check_reply( - || "X11 UngrabPointer before move/resize of window failed.", - self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), - )?; + check_reply( + || "X11 UngrabPointer before move/resize of window failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + )?; - let pointer = get_reply( - || "X11 QueryPointer before move/resize of window failed.", - self.0.xcb.query_pointer(self.0.x_window), - )?; - let message = ClientMessageEvent::new( - 32, - self.0.x_window, - state.atoms._NET_WM_MOVERESIZE, - [ - pointer.root_x as u32, - pointer.root_y as u32, - flag, - 0, // Left mouse button - 0, - ], - ); - check_reply( - || "X11 SendEvent to move/resize window failed.", - self.0.xcb.send_event( - false, - state.x_root_window, - xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, - message, - ), - )?; + let pointer = get_reply( + || "X11 QueryPointer before move/resize of window failed.", + self.0.xcb.query_pointer(self.0.x_window), + )?; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_MOVERESIZE, + [ + pointer.root_x as u32, + pointer.root_y as u32, + flag, + 0, // Left mouse button + 0, + ], + ); + check_reply( + || "X11 SendEvent to move/resize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; - xcb_flush(&self.0.xcb); - Ok(()) - } + xcb_flush(&self.0.xcb); + Ok(()) + } } impl X11WindowStatePtr { - pub fn should_close(&self) -> bool { - let mut cb = self.callbacks.borrow_mut(); - if let Some(mut should_close) = cb.should_close.take() { - let result = (should_close)(); - cb.should_close = Some(should_close); - result - } else { - true - } - } + pub fn should_close(&self) -> bool { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + true + } + } - pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { - let state = self.state.borrow_mut(); - if event.atom == state.atoms._NET_WM_STATE { - self.set_wm_properties(state)?; - } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { - self.set_edge_constraints(state)?; - } - Ok(()) - } + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { + let state = self.state.borrow_mut(); + if event.atom == state.atoms._NET_WM_STATE { + self.set_wm_properties(state)?; + } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { + self.set_edge_constraints(state)?; + } + Ok(()) + } - fn set_edge_constraints( - &self, - mut state: std::cell::RefMut, - ) -> anyhow::Result<()> { - let reply = get_reply( - || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", - self.xcb.get_property( - false, - self.x_window, - state.atoms._GTK_EDGE_CONSTRAINTS, - xproto::AtomEnum::CARDINAL, - 0, - 4, - ), - )?; + fn set_edge_constraints( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._GTK_EDGE_CONSTRAINTS, + xproto::AtomEnum::CARDINAL, + 0, + 4, + ), + )?; - if reply.value_len != 0 { - if let Ok(bytes) = reply.value[0..4].try_into() { - let atom = u32::from_ne_bytes(bytes); - let edge_constraints = EdgeConstraints::from_atom(atom); - state.edge_constraints.replace(edge_constraints); - } else { - log::error!("Failed to parse GTK_EDGE_CONSTRAINTS"); - } - } + if reply.value_len != 0 { + if let Ok(bytes) = reply.value[0..4].try_into() { + let atom = u32::from_ne_bytes(bytes); + let edge_constraints = EdgeConstraints::from_atom(atom); + state.edge_constraints.replace(edge_constraints); + } else { + log::error!("Failed to parse GTK_EDGE_CONSTRAINTS"); + } + } - Ok(()) - } + Ok(()) + } - fn set_wm_properties( - &self, - mut state: std::cell::RefMut, - ) -> anyhow::Result<()> { - let reply = get_reply( - || "X11 GetProperty for _NET_WM_STATE failed.", - self.xcb.get_property( - false, - self.x_window, - state.atoms._NET_WM_STATE, - xproto::AtomEnum::ATOM, - 0, - u32::MAX, - ), - )?; + fn set_wm_properties( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _NET_WM_STATE failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + 0, + u32::MAX, + ), + )?; - let atoms = reply - .value - .chunks_exact(4) - .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + let atoms = reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); - state.active = false; - state.fullscreen = false; - state.maximized_vertical = false; - state.maximized_horizontal = false; - state.hidden = false; + state.active = false; + state.fullscreen = false; + state.maximized_vertical = false; + state.maximized_horizontal = false; + state.hidden = false; - for atom in atoms { - if atom == state.atoms._NET_WM_STATE_FOCUSED { - state.active = true; - } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN { - state.fullscreen = true; - } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT { - state.maximized_vertical = true; - } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ { - state.maximized_horizontal = true; - } else if atom == state.atoms._NET_WM_STATE_HIDDEN { - state.hidden = true; - } - } + for atom in atoms { + if atom == state.atoms._NET_WM_STATE_FOCUSED { + state.active = true; + } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN { + state.fullscreen = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT { + state.maximized_vertical = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ { + state.maximized_horizontal = true; + } else if atom == state.atoms._NET_WM_STATE_HIDDEN { + state.hidden = true; + } + } - Ok(()) - } + Ok(()) + } - pub fn add_child(&self, child: xproto::Window) { - let mut state = self.state.borrow_mut(); - state.children.insert(child); - } + pub fn add_child(&self, child: xproto::Window) { + let mut state = self.state.borrow_mut(); + state.children.insert(child); + } - pub fn is_blocked(&self) -> bool { - let state = self.state.borrow(); - !state.children.is_empty() - } + pub fn is_blocked(&self) -> bool { + let state = self.state.borrow(); + !state.children.is_empty() + } - pub fn close(&self) { - let state = self.state.borrow(); - let client = state.client.clone(); - #[allow(clippy::mutable_key_type)] - let children = state.children.clone(); - drop(state); + pub fn close(&self) { + let state = self.state.borrow(); + let client = state.client.clone(); + #[allow(clippy::mutable_key_type)] + let children = state.children.clone(); + drop(state); - if let Some(client) = client.get_client() { - for child in children { - if let Some(child_window) = client.get_window(child) { - child_window.close(); - } - } - } + if let Some(client) = client.get_client() { + for child in children { + if let Some(child_window) = client.get_window(child) { + child_window.close(); + } + } + } - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(fun) = callbacks.close.take() { - fun() - } - } + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } - pub fn refresh(&self, request_frame_options: RequestFrameOptions) { - let callback = self.callbacks.borrow_mut().request_frame.take(); - if let Some(mut fun) = callback { - fun(request_frame_options); - self.callbacks.borrow_mut().request_frame = Some(fun); - } - } + pub fn refresh(&self, request_frame_options: RequestFrameOptions) { + let callback = self.callbacks.borrow_mut().request_frame.take(); + if let Some(mut fun) = callback { + fun(request_frame_options); + self.callbacks.borrow_mut().request_frame = Some(fun); + } + } - pub fn handle_input(&self, input: PlatformInput) { - if self.is_blocked() { - return; - } - let callback = self.callbacks.borrow_mut().input.take(); - if let Some(mut fun) = callback { - let result = fun(input.clone()); - self.callbacks.borrow_mut().input = Some(fun); - if !result.propagate { - return; - } - } - if let PlatformInput::KeyDown(event) = input { - // only allow shift modifier when inserting text - if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - if let Some(key_char) = &event.keystroke.key_char { - drop(state); - input_handler.replace_text_in_range(None, key_char); - state = self.state.borrow_mut(); - } - state.input_handler = Some(input_handler); - } - } - } - } + pub fn handle_input(&self, input: PlatformInput) { + if self.is_blocked() { + return; + } + let callback = self.callbacks.borrow_mut().input.take(); + if let Some(mut fun) = callback { + let result = fun(input.clone()); + self.callbacks.borrow_mut().input = Some(fun); + if !result.propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + // only allow shift modifier when inserting text + if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + if let Some(key_char) = &event.keystroke.key_char { + drop(state); + input_handler.replace_text_in_range(None, key_char); + state = self.state.borrow_mut(); + } + state.input_handler = Some(input_handler); + } + } + } + } - pub fn handle_ime_commit(&self, text: String) { - if self.is_blocked() { - return; - } - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.replace_text_in_range(None, &text); - let mut state = self.state.borrow_mut(); - state.input_handler = Some(input_handler); - } - } + pub fn handle_ime_commit(&self, text: String) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, &text); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } - pub fn handle_ime_preedit(&self, text: String) { - if self.is_blocked() { - return; - } - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.replace_and_mark_text_in_range(None, &text, None); - let mut state = self.state.borrow_mut(); - state.input_handler = Some(input_handler); - } - } + pub fn handle_ime_preedit(&self, text: String) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_and_mark_text_in_range(None, &text, None); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } - pub fn handle_ime_unmark(&self) { - if self.is_blocked() { - return; - } - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.unmark_text(); - let mut state = self.state.borrow_mut(); - state.input_handler = Some(input_handler); - } - } + pub fn handle_ime_unmark(&self) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.unmark_text(); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } - pub fn handle_ime_delete(&self) { - if self.is_blocked() { - return; - } - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - if let Some(marked) = input_handler.marked_text_range() { - input_handler.replace_text_in_range(Some(marked), ""); - } - let mut state = self.state.borrow_mut(); - state.input_handler = Some(input_handler); - } - } + pub fn handle_ime_delete(&self) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } - pub fn get_ime_area(&self) -> Option> { - let mut state = self.state.borrow_mut(); - let scale_factor = state.scale_factor; - let mut bounds: Option> = None; - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - if let Some(selection) = input_handler.selected_text_range(true) { - bounds = input_handler.bounds_for_range(selection.range); - } - let mut state = self.state.borrow_mut(); - state.input_handler = Some(input_handler); - }; - bounds.map(|b| b.scale(scale_factor)) - } + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let scale_factor = state.scale_factor; + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(selection) = input_handler.selected_text_range(true) { + bounds = input_handler.bounds_for_range(selection.range); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + }; + bounds.map(|b| b.scale(scale_factor)) + } - pub fn set_bounds(&self, bounds: Bounds) -> anyhow::Result<()> { - let (is_resize, content_size, scale_factor) = { - let mut state = self.state.borrow_mut(); - let bounds = bounds.map(|f| px(f as f32 / state.scale_factor)); + pub fn set_bounds(&self, bounds: Bounds) -> anyhow::Result<()> { + let (is_resize, content_size, scale_factor) = { + let mut state = self.state.borrow_mut(); + let bounds = bounds.map(|f| px(f as f32 / state.scale_factor)); - let is_resize = bounds.size.width != state.bounds.size.width - || bounds.size.height != state.bounds.size.height; + let is_resize = bounds.size.width != state.bounds.size.width + || bounds.size.height != state.bounds.size.height; - // If it's a resize event (only width/height changed), we ignore `bounds.origin` - // because it contains wrong values. - if is_resize { - state.bounds.size = bounds.size; - } else { - state.bounds = bounds; - } + // If it's a resize event (only width/height changed), we ignore `bounds.origin` + // because it contains wrong values. + if is_resize { + state.bounds.size = bounds.size; + } else { + state.bounds = bounds; + } - let gpu_size = query_render_extent(&self.xcb, self.x_window)?; - state.renderer.update_drawable_size(gpu_size); - let result = (is_resize, state.content_size(), state.scale_factor); - if let Some(value) = state.last_sync_counter.take() { - check_reply( - || "X11 sync SetCounter failed.", - sync::set_counter(&self.xcb, state.counter_id, value), - )?; - } - result - }; + let gpu_size = query_render_extent(&self.xcb, self.x_window)?; + state.renderer.update_drawable_size(gpu_size); + let result = (is_resize, state.content_size(), state.scale_factor); + if let Some(value) = state.last_sync_counter.take() { + check_reply( + || "X11 sync SetCounter failed.", + sync::set_counter(&self.xcb, state.counter_id, value), + )?; + } + result + }; - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(ref mut fun) = callbacks.resize { - fun(content_size, scale_factor) - } + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.resize { + fun(content_size, scale_factor) + } - if !is_resize && let Some(ref mut fun) = callbacks.moved { - fun(); - } + if !is_resize && let Some(ref mut fun) = callbacks.moved { + fun(); + } - Ok(()) - } + Ok(()) + } - pub fn set_active(&self, focus: bool) { - let callback = self.callbacks.borrow_mut().active_status_change.take(); - if let Some(mut fun) = callback { - fun(focus); - self.callbacks.borrow_mut().active_status_change = Some(fun); - } - if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { - adapter.update_window_focus_state(focus); - } - } + pub fn set_active(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().active_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().active_status_change = Some(fun); + } + if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { + adapter.update_window_focus_state(focus); + } + } - pub fn set_hovered(&self, focus: bool) { - let callback = self.callbacks.borrow_mut().hovered_status_change.take(); - if let Some(mut fun) = callback { - fun(focus); - self.callbacks.borrow_mut().hovered_status_change = Some(fun); - } - } + pub fn set_hovered(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().hovered_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().hovered_status_change = Some(fun); + } + } - pub fn set_appearance(&mut self, appearance: WindowAppearance) { - let mut state = self.state.borrow_mut(); - state.appearance = appearance; - let is_transparent = state.is_transparent(); - state.renderer.update_transparency(is_transparent); - state.appearance = appearance; - drop(state); - let callback = self.callbacks.borrow_mut().appearance_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().appearance_changed = Some(fun); - } - } + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + let mut state = self.state.borrow_mut(); + state.appearance = appearance; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + state.appearance = appearance; + drop(state); + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } - pub fn set_button_layout(&self) { - let callback = self.callbacks.borrow_mut().button_layout_changed.take(); - if let Some(mut fun) = callback { - fun(); - self.callbacks.borrow_mut().button_layout_changed = Some(fun); - } - } + pub fn set_button_layout(&self) { + let callback = self.callbacks.borrow_mut().button_layout_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().button_layout_changed = Some(fun); + } + } } impl PlatformWindow for X11Window { - fn bounds(&self) -> Bounds { - self.0.state.borrow().bounds - } - - fn is_maximized(&self) -> bool { - let state = self.0.state.borrow(); - - // A maximized window that gets minimized will still retain its maximized state. - !state.hidden && state.maximized_vertical && state.maximized_horizontal - } - - fn window_bounds(&self) -> WindowBounds { - let state = self.0.state.borrow(); - if self.is_maximized() { - WindowBounds::Maximized(state.bounds) - } else { - WindowBounds::Windowed(state.bounds) - } - } - - fn inner_window_bounds(&self) -> WindowBounds { - let state = self.0.state.borrow(); - if self.is_maximized() { - WindowBounds::Maximized(state.bounds) - } else { - let mut bounds = state.bounds; - let [left, right, top, bottom] = state.last_insets; - - let [left, right, top, bottom] = [ - px((left as f32) / state.scale_factor), - px((right as f32) / state.scale_factor), - px((top as f32) / state.scale_factor), - px((bottom as f32) / state.scale_factor), - ]; - - bounds.origin.x += left; - bounds.origin.y += top; - bounds.size.width -= left + right; - bounds.size.height -= top + bottom; - - WindowBounds::Windowed(bounds) - } - } - - fn content_size(&self) -> Size { - // After the wgpu migration, X11WindowState::content_size() returns logical pixels - // (bounds.size is already divided by scale_factor in set_bounds), so no further - // division is needed here. This matches the Wayland implementation. - self.0.state.borrow().content_size() - } - - fn resize(&mut self, size: Size) { - let state = self.0.state.borrow(); - let size = size.to_device_pixels(state.scale_factor); - let width = size.width.0 as u32; - let height = size.height.0 as u32; - - check_reply( - || { - format!( - "X11 ConfigureWindow failed. width: {}, height: {}", - width, height - ) - }, - self.0.xcb.configure_window( - self.0.x_window, - &xproto::ConfigureWindowAux::new() - .width(width) - .height(height), - ), - ) - .log_err(); - xcb_flush(&self.0.xcb); - } - - fn scale_factor(&self) -> f32 { - self.0.state.borrow().scale_factor - } - - fn appearance(&self) -> WindowAppearance { - self.0.state.borrow().appearance - } - - fn display(&self) -> Option> { - Some(self.0.state.borrow().display.clone()) - } - - fn mouse_position(&self) -> Point { - get_reply( - || "X11 QueryPointer failed.", - self.0.xcb.query_pointer(self.0.x_window), - ) - .log_err() - .map_or(Point::new(Pixels::ZERO, Pixels::ZERO), |reply| { - Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) - }) - } - - fn modifiers(&self) -> Modifiers { - self.0 - .state - .borrow() - .client - .0 - .upgrade() - .map(|ref_cell| ref_cell.borrow().modifiers) - .unwrap_or_default() - } - - fn capslock(&self) -> gpui::Capslock { - self.0 - .state - .borrow() - .client - .0 - .upgrade() - .map(|ref_cell| ref_cell.borrow().capslock) - .unwrap_or_default() - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.state.borrow_mut().input_handler = Some(input_handler); - } - - fn take_input_handler(&mut self) -> Option { - self.0.state.borrow_mut().input_handler.take() - } - - fn prompt( - &self, - _level: PromptLevel, - _msg: &str, - _detail: Option<&str>, - _answers: &[PromptButton], - ) -> Option> { - None - } - - fn activate(&self) { - let data = [1, xproto::Time::CURRENT_TIME.into(), 0, 0, 0]; - let message = xproto::ClientMessageEvent::new( - 32, - self.0.x_window, - self.0.state.borrow().atoms._NET_ACTIVE_WINDOW, - data, - ); - self.0 - .xcb - .send_event( - false, - self.0.state.borrow().x_root_window, - xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, - message, - ) - .log_err(); - self.0 - .xcb - .set_input_focus( - xproto::InputFocus::POINTER_ROOT, - self.0.x_window, - xproto::Time::CURRENT_TIME, - ) - .log_err(); - xcb_flush(&self.0.xcb); - } - - fn is_active(&self) -> bool { - self.0.state.borrow().active - } - - fn is_hovered(&self) -> bool { - self.0.state.borrow().hovered - } - - fn set_title(&mut self, title: &str) { - check_reply( - || "X11 ChangeProperty8 on WM_NAME failed.", - self.0.xcb.change_property8( - xproto::PropMode::REPLACE, - self.0.x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ), - ) - .log_err(); - - check_reply( - || "X11 ChangeProperty8 on _NET_WM_NAME failed.", - self.0.xcb.change_property8( - xproto::PropMode::REPLACE, - self.0.x_window, - self.0.state.borrow().atoms._NET_WM_NAME, - self.0.state.borrow().atoms.UTF8_STRING, - title.as_bytes(), - ), - ) - .log_err(); - xcb_flush(&self.0.xcb); - } - - fn set_app_id(&mut self, app_id: &str) { - let mut data = Vec::with_capacity(app_id.len() * 2 + 1); - data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170 - data.push(b'\0'); - data.extend(app_id.bytes()); // class - - check_reply( - || "X11 ChangeProperty8 for WM_CLASS failed.", - self.0.xcb.change_property8( - xproto::PropMode::REPLACE, - self.0.x_window, - xproto::AtomEnum::WM_CLASS, - xproto::AtomEnum::STRING, - &data, - ), - ) - .log_err(); - } - - fn map_window(&mut self) -> anyhow::Result<()> { - check_reply( - || "X11 MapWindow failed.", - self.0.xcb.map_window(self.0.x_window), - )?; - Ok(()) - } - - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - let mut state = self.0.state.borrow_mut(); - state.background_appearance = background_appearance; - let transparent = state.is_transparent(); - state.renderer.update_transparency(transparent); - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - self.0.state.borrow().background_appearance - } - - fn is_subpixel_rendering_supported(&self) -> bool { - self.0 - .state - .borrow() - .client - .0 - .upgrade() - .map(|ref_cell| { - let state = ref_cell.borrow(); - state - .gpu_context - .borrow() - .as_ref() - .is_some_and(|ctx| ctx.supports_dual_source_blending()) - }) - .unwrap_or_default() - } - - fn minimize(&self) { - let state = self.0.state.borrow(); - const WINDOW_ICONIC_STATE: u32 = 3; - let message = ClientMessageEvent::new( - 32, - self.0.x_window, - state.atoms.WM_CHANGE_STATE, - [WINDOW_ICONIC_STATE, 0, 0, 0, 0], - ); - check_reply( - || "X11 SendEvent to minimize window failed.", - self.0.xcb.send_event( - false, - state.x_root_window, - xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, - message, - ), - ) - .log_err(); - } - - fn zoom(&self) { - let state = self.0.state.borrow(); - self.set_wm_hints( - || "X11 SendEvent to maximize a window failed.", - WmHintPropertyState::Toggle, - state.atoms._NET_WM_STATE_MAXIMIZED_VERT, - state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, - ) - .log_err(); - } - - fn toggle_fullscreen(&self) { - let state = self.0.state.borrow(); - self.set_wm_hints( - || "X11 SendEvent to fullscreen a window failed.", - WmHintPropertyState::Toggle, - state.atoms._NET_WM_STATE_FULLSCREEN, - xproto::AtomEnum::NONE.into(), - ) - .log_err(); - } - - fn is_fullscreen(&self) -> bool { - self.0.state.borrow().fullscreen - } - - fn on_request_frame(&self, callback: Box) { - self.0.callbacks.borrow_mut().request_frame = Some(callback); - } - - fn on_input(&self, callback: Box gpui::DispatchEventResult>) { - self.0.callbacks.borrow_mut().input = Some(callback); - } - - fn on_active_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().active_status_change = Some(callback); - } - - fn on_hover_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().hovered_status_change = Some(callback); - } - - fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.borrow_mut().resize = Some(callback); - } - - fn on_moved(&self, callback: Box) { - self.0.callbacks.borrow_mut().moved = Some(callback); - } - - fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.borrow_mut().should_close = Some(callback); - } - - fn on_close(&self, callback: Box) { - self.0.callbacks.borrow_mut().close = Some(callback); - } - - fn on_hit_test_window_control(&self, _callback: Box Option>) { - } - - fn on_appearance_changed(&self, callback: Box) { - self.0.callbacks.borrow_mut().appearance_changed = Some(callback); - } - - fn on_button_layout_changed(&self, callback: Box) { - self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); - } - - fn draw(&self, scene: &Scene) { - let mut inner = self.0.state.borrow_mut(); - - if inner.renderer.device_lost() { - let raw_window = RawWindow { - connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( - &*self.0.xcb, - ) as *mut _, - screen_id: inner.x_screen_index, - window_id: self.0.x_window, - visual_id: inner.visual_id, - }; - match inner.renderer.recover(&raw_window) { - Ok(()) => {} - Err(err) => { - log::warn!("GPU recovery failed, will retry on next frame: {err}"); - } - } - - inner.force_render_after_recovery = true; - return; - } - - inner.renderer.draw(scene); - - if inner.renderer.needs_redraw() { - inner.force_render_after_recovery = true; - } - } - - fn sprite_atlas(&self) -> Arc { - let inner = self.0.state.borrow(); - inner.renderer.sprite_atlas().clone() - } - - fn show_window_menu(&self, position: Point) { - let state = self.0.state.borrow(); - - check_reply( - || "X11 UngrabPointer failed.", - self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), - ) - .log_err(); - - let Some(coords) = self.get_root_position(position).log_err() else { - return; - }; - let message = ClientMessageEvent::new( - 32, - self.0.x_window, - state.atoms._GTK_SHOW_WINDOW_MENU, - [ - XINPUT_ALL_DEVICE_GROUPS as u32, - coords.dst_x as u32, - coords.dst_y as u32, - 0, - 0, - ], - ); - check_reply( - || "X11 SendEvent to show window menu failed.", - self.0.xcb.send_event( - false, - state.x_root_window, - xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, - message, - ), - ) - .log_err(); - } - - fn start_window_move(&self) { - const MOVERESIZE_MOVE: u32 = 8; - self.send_moveresize(MOVERESIZE_MOVE).log_err(); - } - - fn start_window_resize(&self, edge: ResizeEdge) { - self.send_moveresize(resize_edge_to_moveresize(edge)) - .log_err(); - } - - fn window_decorations(&self) -> gpui::Decorations { - let state = self.0.state.borrow(); - - // Client window decorations require compositor support - if !state.client_side_decorations_supported { - return Decorations::Server; - } - - match state.decorations { - WindowDecorations::Server => Decorations::Server, - WindowDecorations::Client => { - let tiling = if state.fullscreen { - Tiling::tiled() - } else if let Some(edge_constraints) = &state.edge_constraints { - edge_constraints.to_tiling() - } else { - // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d - Tiling { - top: state.maximized_vertical, - bottom: state.maximized_vertical, - left: state.maximized_horizontal, - right: state.maximized_horizontal, - } - }; - Decorations::Client { tiling } - } - } - } - - fn set_client_inset(&self, inset: Pixels) { - let mut state = self.0.state.borrow_mut(); - - let dp = (f32::from(inset) * state.scale_factor) as u32; - - let insets = if state.fullscreen { - [0, 0, 0, 0] - } else if let Some(edge_constraints) = &state.edge_constraints { - let left = if edge_constraints.left_tiled { 0 } else { dp }; - let top = if edge_constraints.top_tiled { 0 } else { dp }; - let right = if edge_constraints.right_tiled { 0 } else { dp }; - let bottom = if edge_constraints.bottom_tiled { 0 } else { dp }; - - [left, right, top, bottom] - } else { - let (left, right) = if state.maximized_horizontal { - (0, 0) - } else { - (dp, dp) - }; - let (top, bottom) = if state.maximized_vertical { - (0, 0) - } else { - (dp, dp) - }; - [left, right, top, bottom] - }; - - if state.last_insets != insets { - state.last_insets = insets; - - check_reply( - || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", - self.0.xcb.change_property( - xproto::PropMode::REPLACE, - self.0.x_window, - state.atoms._GTK_FRAME_EXTENTS, - xproto::AtomEnum::CARDINAL, - size_of::() as u8 * 8, - 4, - bytemuck::cast_slice::(&insets), - ), - ) - .log_err(); - } - } - - fn request_decorations(&self, mut decorations: gpui::WindowDecorations) { - let mut state = self.0.state.borrow_mut(); - - if matches!(decorations, gpui::WindowDecorations::Client) - && !state.client_side_decorations_supported - { - log::info!( - "x11: no compositor present, falling back to server-side window decorations" - ); - decorations = gpui::WindowDecorations::Server; - } - - // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87 - let hints_data: [u32; 5] = match decorations { - WindowDecorations::Server => [1 << 1, 0, 1, 0, 0], - WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], - }; - - let success = check_reply( - || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", - self.0.xcb.change_property( - xproto::PropMode::REPLACE, - self.0.x_window, - state.atoms._MOTIF_WM_HINTS, - state.atoms._MOTIF_WM_HINTS, - size_of::() as u8 * 8, - 5, - bytemuck::cast_slice::(&hints_data), - ), - ) - .log_err(); - - let Some(()) = success else { - return; - }; - - match decorations { - WindowDecorations::Server => { - state.decorations = WindowDecorations::Server; - let is_transparent = state.is_transparent(); - state.renderer.update_transparency(is_transparent); - } - WindowDecorations::Client => { - state.decorations = WindowDecorations::Client; - let is_transparent = state.is_transparent(); - state.renderer.update_transparency(is_transparent); - } - } - - drop(state); - let mut callbacks = self.0.callbacks.borrow_mut(); - if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { - appearance_changed(); - } - } - - fn update_ime_position(&self, bounds: Bounds) { - let state = self.0.state.borrow(); - let client = state.client.clone(); - drop(state); - client.update_ime_position(bounds); - } - - fn gpu_specs(&self) -> Option { - self.0.state.borrow().renderer.gpu_specs().into() - } - - fn gpu_context(&self) -> Option> { - let (device, queue) = self.0.state.borrow().renderer.gpu_context(); - Some(Box::new((device, queue))) - } - - fn gpu_device_lost(&self) -> Option { - // Only loads an atomic flag — safe even mid-recovery, when - // `gpu_context` would panic on the torn-down resources. - Some(self.0.state.borrow().renderer.device_lost()) - } - - fn play_system_bell(&self) { - // Volume 0% means don't increase or decrease from system volume - let _ = self.0.xcb.bell(0); - } - - fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { - let activation_handler = TrivialActivationHandler { - callback: callbacks.activation, - }; - let action_handler = TrivialActionHandler(callbacks.action); - let deactivation_handler = TrivialDeactivationHandler { - callback: callbacks.deactivation, - }; - - let adapter = - accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); - - self.0.state.borrow_mut().accesskit_adapter = Some(adapter); - } - - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { - let mut state = self.0.state.borrow_mut(); - if let Some(adapter) = state.accesskit_adapter.as_mut() { - adapter.update_if_active(|| tree_update); - } - } - - fn a11y_update_window_bounds(&self) { - let mut state = self.0.state.borrow_mut(); - let scale = state.scale_factor; - let bounds = state.bounds; - let [left, right, top, bottom] = state.last_insets; - - let x = f32::from(bounds.origin.x); - let y = f32::from(bounds.origin.y); - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - - let outer = accesskit::Rect { - x0: (x * scale) as f64, - y0: (y * scale) as f64, - x1: ((x + width) * scale) as f64, - y1: ((y + height) * scale) as f64, - }; - - let inner = accesskit::Rect { - x0: (x * scale) as f64 + left as f64, - y0: (y * scale) as f64 + top as f64, - x1: ((x + width) * scale) as f64 - right as f64, - y1: ((y + height) * scale) as f64 - bottom as f64, - }; - - if let Some(adapter) = state.accesskit_adapter.as_mut() { - adapter.set_root_window_bounds(outer, inner); - } - } + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds + } + + fn is_maximized(&self) -> bool { + let state = self.0.state.borrow(); + + // A maximized window that gets minimized will still retain its maximized state. + !state.hidden && state.maximized_vertical && state.maximized_horizontal + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + WindowBounds::Windowed(state.bounds) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + let mut bounds = state.bounds; + let [left, right, top, bottom] = state.last_insets; + + let [left, right, top, bottom] = [ + px((left as f32) / state.scale_factor), + px((right as f32) / state.scale_factor), + px((top as f32) / state.scale_factor), + px((bottom as f32) / state.scale_factor), + ]; + + bounds.origin.x += left; + bounds.origin.y += top; + bounds.size.width -= left + right; + bounds.size.height -= top + bottom; + + WindowBounds::Windowed(bounds) + } + } + + fn content_size(&self) -> Size { + // After the wgpu migration, X11WindowState::content_size() returns logical pixels + // (bounds.size is already divided by scale_factor in set_bounds), so no further + // division is needed here. This matches the Wayland implementation. + self.0.state.borrow().content_size() + } + + fn resize(&mut self, size: Size) { + let state = self.0.state.borrow(); + let size = size.to_device_pixels(state.scale_factor); + let width = size.width.0 as u32; + let height = size.height.0 as u32; + + check_reply( + || { + format!( + "X11 ConfigureWindow failed. width: {}, height: {}", + width, height + ) + }, + self.0.xcb.configure_window( + self.0.x_window, + &xproto::ConfigureWindowAux::new() + .width(width) + .height(height), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + self.0.state.borrow().appearance + } + + fn display(&self) -> Option> { + Some(self.0.state.borrow().display.clone()) + } + + fn mouse_position(&self) -> Point { + get_reply( + || "X11 QueryPointer failed.", + self.0.xcb.query_pointer(self.0.x_window), + ) + .log_err() + .map_or(Point::new(Pixels::ZERO, Pixels::ZERO), |reply| { + Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) + }) + } + + fn modifiers(&self) -> Modifiers { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().modifiers) + .unwrap_or_default() + } + + fn capslock(&self) -> gpui::Capslock { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().capslock) + .unwrap_or_default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + let data = [1, xproto::Time::CURRENT_TIME.into(), 0, 0, 0]; + let message = xproto::ClientMessageEvent::new( + 32, + self.0.x_window, + self.0.state.borrow().atoms._NET_ACTIVE_WINDOW, + data, + ); + self.0 + .xcb + .send_event( + false, + self.0.state.borrow().x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .log_err(); + self.0 + .xcb + .set_input_focus( + xproto::InputFocus::POINTER_ROOT, + self.0.x_window, + xproto::Time::CURRENT_TIME, + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn is_active(&self) -> bool { + self.0.state.borrow().active + } + + fn is_hovered(&self) -> bool { + self.0.state.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + ) + .log_err(); + + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_NAME, + self.0.state.borrow().atoms.UTF8_STRING, + title.as_bytes(), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn set_app_id(&mut self, app_id: &str) { + let mut data = Vec::with_capacity(app_id.len() * 2 + 1); + data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170 + data.push(b'\0'); + data.extend(app_id.bytes()); // class + + check_reply( + || "X11 ChangeProperty8 for WM_CLASS failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_CLASS, + xproto::AtomEnum::STRING, + &data, + ), + ) + .log_err(); + } + + fn map_window(&mut self) -> anyhow::Result<()> { + check_reply( + || "X11 MapWindow failed.", + self.0.xcb.map_window(self.0.x_window), + )?; + Ok(()) + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.0.state.borrow_mut(); + state.background_appearance = background_appearance; + let transparent = state.is_transparent(); + state.renderer.update_transparency(transparent); + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.0.state.borrow().background_appearance + } + + fn is_subpixel_rendering_supported(&self) -> bool { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| { + let state = ref_cell.borrow(); + state + .gpu_context + .borrow() + .as_ref() + .is_some_and(|ctx| ctx.supports_dual_source_blending()) + }) + .unwrap_or_default() + } + + fn minimize(&self) { + let state = self.0.state.borrow(); + const WINDOW_ICONIC_STATE: u32 = 3; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms.WM_CHANGE_STATE, + [WINDOW_ICONIC_STATE, 0, 0, 0, 0], + ); + check_reply( + || "X11 SendEvent to minimize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn zoom(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to maximize a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_MAXIMIZED_VERT, + state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + ) + .log_err(); + } + + fn toggle_fullscreen(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to fullscreen a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_FULLSCREEN, + xproto::AtomEnum::NONE.into(), + ) + .log_err(); + } + + fn is_fullscreen(&self) -> bool { + self.0.state.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box gpui::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hovered_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn on_button_layout_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut inner = self.0.state.borrow_mut(); + + if inner.renderer.device_lost() { + let raw_window = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + &*self.0.xcb, + ) as *mut _, + screen_id: inner.x_screen_index, + window_id: self.0.x_window, + visual_id: inner.visual_id, + }; + match inner.renderer.recover(&raw_window) { + Ok(()) => {} + Err(err) => { + log::warn!("GPU recovery failed, will retry on next frame: {err}"); + } + } + + inner.force_render_after_recovery = true; + return; + } + + inner.renderer.draw(scene); + + if inner.renderer.needs_redraw() { + inner.force_render_after_recovery = true; + } + } + + fn sprite_atlas(&self) -> Arc { + let inner = self.0.state.borrow(); + inner.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.0.state.borrow(); + + check_reply( + || "X11 UngrabPointer failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + ) + .log_err(); + + let Some(coords) = self.get_root_position(position).log_err() else { + return; + }; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._GTK_SHOW_WINDOW_MENU, + [ + XINPUT_ALL_DEVICE_GROUPS as u32, + coords.dst_x as u32, + coords.dst_y as u32, + 0, + 0, + ], + ); + check_reply( + || "X11 SendEvent to show window menu failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn start_window_move(&self) { + const MOVERESIZE_MOVE: u32 = 8; + self.send_moveresize(MOVERESIZE_MOVE).log_err(); + } + + fn start_window_resize(&self, edge: ResizeEdge) { + self.send_moveresize(resize_edge_to_moveresize(edge)) + .log_err(); + } + + fn window_decorations(&self) -> gpui::Decorations { + let state = self.0.state.borrow(); + + // Client window decorations require compositor support + if !state.client_side_decorations_supported { + return Decorations::Server; + } + + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => { + let tiling = if state.fullscreen { + Tiling::tiled() + } else if let Some(edge_constraints) = &state.edge_constraints { + edge_constraints.to_tiling() + } else { + // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d + Tiling { + top: state.maximized_vertical, + bottom: state.maximized_vertical, + left: state.maximized_horizontal, + right: state.maximized_horizontal, + } + }; + Decorations::Client { tiling } + } + } + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.0.state.borrow_mut(); + + let dp = (f32::from(inset) * state.scale_factor) as u32; + + let insets = if state.fullscreen { + [0, 0, 0, 0] + } else if let Some(edge_constraints) = &state.edge_constraints { + let left = if edge_constraints.left_tiled { 0 } else { dp }; + let top = if edge_constraints.top_tiled { 0 } else { dp }; + let right = if edge_constraints.right_tiled { 0 } else { dp }; + let bottom = if edge_constraints.bottom_tiled { 0 } else { dp }; + + [left, right, top, bottom] + } else { + let (left, right) = if state.maximized_horizontal { + (0, 0) + } else { + (dp, dp) + }; + let (top, bottom) = if state.maximized_vertical { + (0, 0) + } else { + (dp, dp) + }; + [left, right, top, bottom] + }; + + if state.last_insets != insets { + state.last_insets = insets; + + check_reply( + || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._GTK_FRAME_EXTENTS, + xproto::AtomEnum::CARDINAL, + size_of::() as u8 * 8, + 4, + bytemuck::cast_slice::(&insets), + ), + ) + .log_err(); + } + } + + fn request_decorations(&self, mut decorations: gpui::WindowDecorations) { + let mut state = self.0.state.borrow_mut(); + + if matches!(decorations, gpui::WindowDecorations::Client) + && !state.client_side_decorations_supported + { + log::info!( + "x11: no compositor present, falling back to server-side window decorations" + ); + decorations = gpui::WindowDecorations::Server; + } + + // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87 + let hints_data: [u32; 5] = match decorations { + WindowDecorations::Server => [1 << 1, 0, 1, 0, 0], + WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], + }; + + let success = check_reply( + || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._MOTIF_WM_HINTS, + state.atoms._MOTIF_WM_HINTS, + size_of::() as u8 * 8, + 5, + bytemuck::cast_slice::(&hints_data), + ), + ) + .log_err(); + + let Some(()) = success else { + return; + }; + + match decorations { + WindowDecorations::Server => { + state.decorations = WindowDecorations::Server; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + WindowDecorations::Client => { + state.decorations = WindowDecorations::Client; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + } + + drop(state); + let mut callbacks = self.0.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let state = self.0.state.borrow(); + let client = state.client.clone(); + drop(state); + client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.0.state.borrow().renderer.gpu_specs().into() + } + + fn gpu_context(&self) -> Option> { + let (device, queue) = self.0.state.borrow().renderer.gpu_context(); + Some(Box::new((device, queue))) + } + + fn gpu_device_lost(&self) -> Option { + // Only loads an atomic flag — safe even mid-recovery, when + // `gpu_context` would panic on the torn-down resources. + Some(self.0.state.borrow().renderer.device_lost()) + } + + fn play_system_bell(&self) { + // Volume 0% means don't increase or decrease from system volume + let _ = self.0.xcb.bell(0); + } + + fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { + let activation_handler = TrivialActivationHandler { + callback: callbacks.activation, + }; + let action_handler = TrivialActionHandler(callbacks.action); + let deactivation_handler = TrivialDeactivationHandler { + callback: callbacks.deactivation, + }; + + let adapter = + accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); + + self.0.state.borrow_mut().accesskit_adapter = Some(adapter); + } + + fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + let mut state = self.0.state.borrow_mut(); + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.update_if_active(|| tree_update); + } + } + + fn a11y_update_window_bounds(&self) { + let mut state = self.0.state.borrow_mut(); + let scale = state.scale_factor; + let bounds = state.bounds; + let [left, right, top, bottom] = state.last_insets; + + let x = f32::from(bounds.origin.x); + let y = f32::from(bounds.origin.y); + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + + let outer = accesskit::Rect { + x0: (x * scale) as f64, + y0: (y * scale) as f64, + x1: ((x + width) * scale) as f64, + y1: ((y + height) * scale) as f64, + }; + + let inner = accesskit::Rect { + x0: (x * scale) as f64 + left as f64, + y0: (y * scale) as f64 + top as f64, + x1: ((x + width) * scale) as f64 - right as f64, + y1: ((y + height) * scale) as f64 - bottom as f64, + }; + + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.set_root_window_bounds(outer, inner); + } + } } struct TrivialActivationHandler { - callback: Box Option + Send + 'static>, + callback: Box Option + Send + 'static>, } impl accesskit::ActivationHandler for TrivialActivationHandler { - fn request_initial_tree(&mut self) -> Option { - (self.callback)() - } + fn request_initial_tree(&mut self) -> Option { + (self.callback)() + } } struct TrivialActionHandler(Box); impl accesskit::ActionHandler for TrivialActionHandler { - fn do_action(&mut self, request: accesskit::ActionRequest) { - (self.0)(request); - } + fn do_action(&mut self, request: accesskit::ActionRequest) { + (self.0)(request); + } } struct TrivialDeactivationHandler { - callback: Box, + callback: Box, } impl accesskit::DeactivationHandler for TrivialDeactivationHandler { - fn deactivate_accessibility(&mut self) { - (self.callback)(); - } + fn deactivate_accessibility(&mut self) { + (self.callback)(); + } } diff --git a/crates/gpui_linux/src/linux/x11/xim_handler.rs b/crates/gpui_linux/src/linux/x11/xim_handler.rs index 82b7c96312..8d4bb2f73a 100644 --- a/crates/gpui_linux/src/linux/x11/xim_handler.rs +++ b/crates/gpui_linux/src/linux/x11/xim_handler.rs @@ -4,130 +4,130 @@ use x11rb::protocol::{Event, xproto}; use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle}; pub enum XimCallbackEvent { - XimXEvent(x11rb::protocol::Event), - XimPreeditEvent(xproto::Window, String), - XimCommitEvent(xproto::Window, String), + XimXEvent(x11rb::protocol::Event), + XimPreeditEvent(xproto::Window, String), + XimCommitEvent(xproto::Window, String), } pub struct XimHandler { - pub im_id: u16, - pub ic_id: u16, - pub connected: bool, - pub window: xproto::Window, - pub last_callback_event: Option, + pub im_id: u16, + pub ic_id: u16, + pub connected: bool, + pub window: xproto::Window, + pub last_callback_event: Option, } impl XimHandler { - pub fn new() -> Self { - Self { - im_id: Default::default(), - ic_id: Default::default(), - connected: false, - window: Default::default(), - last_callback_event: None, - } - } + pub fn new() -> Self { + Self { + im_id: Default::default(), + ic_id: Default::default(), + connected: false, + window: Default::default(), + last_callback_event: None, + } + } } impl> ClientHandler for XimHandler { - fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { - client.open("C") - } + fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { + client.open("C") + } - fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { - self.im_id = input_method_id; + fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + self.im_id = input_method_id; - client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) - } + client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) + } - fn handle_get_im_values( - &mut self, - client: &mut C, - input_method_id: u16, - _attributes: AHashMap>, - ) -> Result<(), ClientError> { - let ic_attributes = client - .build_ic_attributes() - .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) - .push(AttributeName::ClientWindow, self.window) - .push(AttributeName::FocusWindow, self.window) - .build(); - client.create_ic(input_method_id, ic_attributes) - } + fn handle_get_im_values( + &mut self, + client: &mut C, + input_method_id: u16, + _attributes: AHashMap>, + ) -> Result<(), ClientError> { + let ic_attributes = client + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, self.window) + .push(AttributeName::FocusWindow, self.window) + .build(); + client.create_ic(input_method_id, ic_attributes) + } - fn handle_create_ic( - &mut self, - _client: &mut C, - _input_method_id: u16, - input_context_id: u16, - ) -> Result<(), ClientError> { - self.connected = true; - self.ic_id = input_context_id; - Ok(()) - } + fn handle_create_ic( + &mut self, + _client: &mut C, + _input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + self.connected = true; + self.ic_id = input_context_id; + Ok(()) + } - fn handle_commit( - &mut self, - _client: &mut C, - _input_method_id: u16, - _input_context_id: u16, - text: &str, - ) -> Result<(), ClientError> { - self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent( - self.window, - String::from(text), - )); - Ok(()) - } + fn handle_commit( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + text: &str, + ) -> Result<(), ClientError> { + self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent( + self.window, + String::from(text), + )); + Ok(()) + } - fn handle_forward_event( - &mut self, - _client: &mut C, - _input_method_id: u16, - _input_context_id: u16, - _flag: xim::ForwardEventFlag, - xev: C::XEvent, - ) -> Result<(), ClientError> { - match xev.response_type { - x11rb::protocol::xproto::KEY_PRESS_EVENT => { - self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev))); - } - x11rb::protocol::xproto::KEY_RELEASE_EVENT => { - self.last_callback_event = - Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev))); - } - _ => {} - } - Ok(()) - } + fn handle_forward_event( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _flag: xim::ForwardEventFlag, + xev: C::XEvent, + ) -> Result<(), ClientError> { + match xev.response_type { + x11rb::protocol::xproto::KEY_PRESS_EVENT => { + self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev))); + } + x11rb::protocol::xproto::KEY_RELEASE_EVENT => { + self.last_callback_event = + Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev))); + } + _ => {} + } + Ok(()) + } - fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> { - client.disconnect() - } + fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> { + client.disconnect() + } - fn handle_preedit_draw( - &mut self, - _client: &mut C, - _input_method_id: u16, - _input_context_id: u16, - _caret: i32, - _chg_first: i32, - _chg_len: i32, - _status: xim::PreeditDrawStatus, - preedit_string: &str, - _feedbacks: Vec, - ) -> Result<(), ClientError> { - // XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text - // XIMUnderline: 2, XIMSecondary: 16: underlined text - // XIMHighlight: 4: normal text - // XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position - // XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified, - // but interchangeable as above - // Currently there's no way to support these. - self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent( - self.window, - String::from(preedit_string), - )); - Ok(()) - } + fn handle_preedit_draw( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _caret: i32, + _chg_first: i32, + _chg_len: i32, + _status: xim::PreeditDrawStatus, + preedit_string: &str, + _feedbacks: Vec, + ) -> Result<(), ClientError> { + // XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text + // XIMUnderline: 2, XIMSecondary: 16: underlined text + // XIMHighlight: 4: normal text + // XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position + // XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified, + // but interchangeable as above + // Currently there's no way to support these. + self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent( + self.window, + String::from(preedit_string), + )); + Ok(()) + } } diff --git a/crates/gpui_linux/src/linux/xdg_desktop_portal.rs b/crates/gpui_linux/src/linux/xdg_desktop_portal.rs index 9b5d72476b..eff3edaa1f 100644 --- a/crates/gpui_linux/src/linux/xdg_desktop_portal.rs +++ b/crates/gpui_linux/src/linux/xdg_desktop_portal.rs @@ -10,182 +10,182 @@ use smol::stream::StreamExt; use gpui::{BackgroundExecutor, WindowAppearance}; pub enum Event { - WindowAppearance(WindowAppearance), - #[cfg_attr(feature = "x11", allow(dead_code))] - CursorTheme(String), - #[cfg_attr(feature = "x11", allow(dead_code))] - CursorSize(u32), - ButtonLayout(String), + WindowAppearance(WindowAppearance), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorTheme(String), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorSize(u32), + ButtonLayout(String), } pub struct XDPEventSource { - channel: Channel, + channel: Channel, } impl XDPEventSource { - pub fn new(executor: &BackgroundExecutor) -> Self { - let (sender, channel) = calloop::channel::channel(); + pub fn new(executor: &BackgroundExecutor) -> Self { + let (sender, channel) = calloop::channel::channel(); - let background = executor.clone(); + let background = executor.clone(); - executor - .spawn(async move { - let settings = Settings::new().await?; + executor + .spawn(async move { + let settings = Settings::new().await?; - if let Ok(initial_appearance) = settings.color_scheme().await { - sender.send(Event::WindowAppearance( - window_appearance_from_color_scheme(initial_appearance), - ))?; - } - if let Ok(initial_theme) = settings - .read::("org.gnome.desktop.interface", "cursor-theme") - .await - { - sender.send(Event::CursorTheme(initial_theme))?; - } + if let Ok(initial_appearance) = settings.color_scheme().await { + sender.send(Event::WindowAppearance( + window_appearance_from_color_scheme(initial_appearance), + ))?; + } + if let Ok(initial_theme) = settings + .read::("org.gnome.desktop.interface", "cursor-theme") + .await + { + sender.send(Event::CursorTheme(initial_theme))?; + } - // If u32 is used here, it throws invalid type error - if let Ok(initial_size) = settings - .read::("org.gnome.desktop.interface", "cursor-size") - .await - { - sender.send(Event::CursorSize(initial_size as u32))?; - } + // If u32 is used here, it throws invalid type error + if let Ok(initial_size) = settings + .read::("org.gnome.desktop.interface", "cursor-size") + .await + { + sender.send(Event::CursorSize(initial_size as u32))?; + } - if let Ok(initial_layout) = settings - .read::("org.gnome.desktop.wm.preferences", "button-layout") - .await - { - sender.send(Event::ButtonLayout(initial_layout))?; - } + if let Ok(initial_layout) = settings + .read::("org.gnome.desktop.wm.preferences", "button-layout") + .await + { + sender.send(Event::ButtonLayout(initial_layout))?; + } - if let Ok(mut cursor_theme_changed) = settings - .receive_setting_changed_with_args( - "org.gnome.desktop.interface", - "cursor-theme", - ) - .await - { - let sender = sender.clone(); - background - .spawn(async move { - while let Some(theme) = cursor_theme_changed.next().await { - let theme = theme?; - sender.send(Event::CursorTheme(theme))?; - } - anyhow::Ok(()) - }) - .detach(); - } + if let Ok(mut cursor_theme_changed) = settings + .receive_setting_changed_with_args( + "org.gnome.desktop.interface", + "cursor-theme", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(theme) = cursor_theme_changed.next().await { + let theme = theme?; + sender.send(Event::CursorTheme(theme))?; + } + anyhow::Ok(()) + }) + .detach(); + } - if let Ok(mut cursor_size_changed) = settings - .receive_setting_changed_with_args::( - "org.gnome.desktop.interface", - "cursor-size", - ) - .await - { - let sender = sender.clone(); - background - .spawn(async move { - while let Some(size) = cursor_size_changed.next().await { - let size = size?; - sender.send(Event::CursorSize(size as u32))?; - } - anyhow::Ok(()) - }) - .detach(); - } + if let Ok(mut cursor_size_changed) = settings + .receive_setting_changed_with_args::( + "org.gnome.desktop.interface", + "cursor-size", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(size) = cursor_size_changed.next().await { + let size = size?; + sender.send(Event::CursorSize(size as u32))?; + } + anyhow::Ok(()) + }) + .detach(); + } - if let Ok(mut button_layout_changed) = settings - .receive_setting_changed_with_args( - "org.gnome.desktop.wm.preferences", - "button-layout", - ) - .await - { - let sender = sender.clone(); - background - .spawn(async move { - while let Some(layout) = button_layout_changed.next().await { - let layout = layout?; - sender.send(Event::ButtonLayout(layout))?; - } - anyhow::Ok(()) - }) - .detach(); - } + if let Ok(mut button_layout_changed) = settings + .receive_setting_changed_with_args( + "org.gnome.desktop.wm.preferences", + "button-layout", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(layout) = button_layout_changed.next().await { + let layout = layout?; + sender.send(Event::ButtonLayout(layout))?; + } + anyhow::Ok(()) + }) + .detach(); + } - let mut appearance_changed = settings.receive_color_scheme_changed().await?; - while let Some(scheme) = appearance_changed.next().await { - sender.send(Event::WindowAppearance( - window_appearance_from_color_scheme(scheme), - ))?; - } + let mut appearance_changed = settings.receive_color_scheme_changed().await?; + while let Some(scheme) = appearance_changed.next().await { + sender.send(Event::WindowAppearance( + window_appearance_from_color_scheme(scheme), + ))?; + } - anyhow::Ok(()) - }) - .detach(); + anyhow::Ok(()) + }) + .detach(); - Self { channel } - } + Self { channel } + } } impl EventSource for XDPEventSource { - type Event = Event; - type Metadata = (); - type Ret = (); - type Error = anyhow::Error; + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = anyhow::Error; - fn process_events( - &mut self, - readiness: Readiness, - token: Token, - mut callback: F, - ) -> Result - where - F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, - { - self.channel.process_events(readiness, token, |evt, _| { - if let calloop::channel::Event::Msg(msg) = evt { - (callback)(msg, &mut ()) - } - })?; + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.channel.process_events(readiness, token, |evt, _| { + if let calloop::channel::Event::Msg(msg) = evt { + (callback)(msg, &mut ()) + } + })?; - Ok(PostAction::Continue) - } + Ok(PostAction::Continue) + } - fn register( - &mut self, - poll: &mut Poll, - token_factory: &mut TokenFactory, - ) -> calloop::Result<()> { - self.channel.register(poll, token_factory)?; + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.register(poll, token_factory)?; - Ok(()) - } + Ok(()) + } - fn reregister( - &mut self, - poll: &mut Poll, - token_factory: &mut TokenFactory, - ) -> calloop::Result<()> { - self.channel.reregister(poll, token_factory)?; + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.reregister(poll, token_factory)?; - Ok(()) - } + Ok(()) + } - fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { - self.channel.unregister(poll)?; + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.channel.unregister(poll)?; - Ok(()) - } + Ok(()) + } } fn window_appearance_from_color_scheme(cs: ColorScheme) -> WindowAppearance { - match cs { - ColorScheme::PreferDark => WindowAppearance::Dark, - ColorScheme::PreferLight => WindowAppearance::Light, - ColorScheme::NoPreference => WindowAppearance::Light, - } + match cs { + ColorScheme::PreferDark => WindowAppearance::Dark, + ColorScheme::PreferLight => WindowAppearance::Light, + ColorScheme::NoPreference => WindowAppearance::Light, + } } diff --git a/crates/gpui_macos/build.rs b/crates/gpui_macos/build.rs index d5c1893f4c..ae05da7905 100644 --- a/crates/gpui_macos/build.rs +++ b/crates/gpui_macos/build.rs @@ -1,174 +1,174 @@ #![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] fn main() { - #[cfg(target_os = "macos")] - macos_build::run(); + #[cfg(target_os = "macos")] + macos_build::run(); } #[cfg(target_os = "macos")] mod macos_build { - use std::{ - env, - path::{Path, PathBuf}, - }; + use std::{ + env, + path::{Path, PathBuf}, + }; - use cbindgen::Config; + use cbindgen::Config; - pub fn run() { - let header_path = generate_shader_bindings(); + 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); - } + #[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"); + 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 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(), - "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 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(), + "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 mut builder = cbindgen::Builder::new(); - let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + 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/geometry.rs"), - gpui_dir.join("src/color.rs"), - gpui_dir.join("src/window.rs"), - gpui_dir.join("src/platform.rs"), - ]; + // Source files from gpui that define types used in shaders + let gpui_src_paths = [ + gpui_dir.join("src/scene.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")]; + // 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); - } + 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); + builder + .with_config(config) + .generate() + .expect("Unable to generate bindings") + .write_to_file(&output_path); - output_path - } + output_path + } - /// Locate the gpui crate directory relative to this crate. - fn find_gpui_crate_dir() -> PathBuf { - gpui::GPUI_MANIFEST_DIR.into() - } + /// Locate the gpui crate directory relative to this crate. + fn find_gpui_crate_dir() -> PathBuf { + gpui::GPUI_MANIFEST_DIR.into() + } - /// 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); - } + /// 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); + #[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); - let output = Command::new("xcrun") - .args([ - "-sdk", - "macosx", - "metal", - "-gline-tables-only", - "-mmacosx-version-min=10.15.7", - "-MO", - "-c", - shader_path, - "-include", - (header_path.to_str().unwrap()), - "-o", - ]) - .arg(&air_output_path) - .output() - .unwrap(); + let output = Command::new("xcrun") + .args([ + "-sdk", + "macosx", + "metal", + "-gline-tables-only", + "-mmacosx-version-min=10.15.7", + "-MO", + "-c", + shader_path, + "-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); - } + 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(); + 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); - } - } + if !output.status.success() { + println!( + "cargo::error=metallib compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + process::exit(1); + } + } } diff --git a/crates/gpui_macos/src/dispatcher.rs b/crates/gpui_macos/src/dispatcher.rs index f4b80ec7cb..02610a27e0 100644 --- a/crates/gpui_macos/src/dispatcher.rs +++ b/crates/gpui_macos/src/dispatcher.rs @@ -1,201 +1,201 @@ use dispatch2::{DispatchQueue, DispatchQueueGlobalPriority, DispatchTime, GlobalQueueIdentifier}; use gpui::{ - GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, RunnableMeta, RunnableVariant, TaskTiming, - ThreadTaskTimings, add_task_timing, + GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, RunnableMeta, RunnableVariant, TaskTiming, + ThreadTaskTimings, add_task_timing, }; use mach2::{ - kern_return::KERN_SUCCESS, - mach_time::mach_timebase_info_data_t, - thread_policy::{ - THREAD_EXTENDED_POLICY, THREAD_EXTENDED_POLICY_COUNT, THREAD_PRECEDENCE_POLICY, - THREAD_PRECEDENCE_POLICY_COUNT, THREAD_TIME_CONSTRAINT_POLICY, - THREAD_TIME_CONSTRAINT_POLICY_COUNT, thread_extended_policy_data_t, - thread_precedence_policy_data_t, thread_time_constraint_policy_data_t, - }, + kern_return::KERN_SUCCESS, + mach_time::mach_timebase_info_data_t, + thread_policy::{ + THREAD_EXTENDED_POLICY, THREAD_EXTENDED_POLICY_COUNT, THREAD_PRECEDENCE_POLICY, + THREAD_PRECEDENCE_POLICY_COUNT, THREAD_TIME_CONSTRAINT_POLICY, + THREAD_TIME_CONSTRAINT_POLICY_COUNT, thread_extended_policy_data_t, + thread_precedence_policy_data_t, thread_time_constraint_policy_data_t, + }, }; use util::ResultExt; use async_task::Runnable; use objc::{ - class, msg_send, - runtime::{BOOL, YES}, - sel, sel_impl, + class, msg_send, + runtime::{BOOL, YES}, + sel, sel_impl, }; use std::{ - ffi::c_void, - ptr::NonNull, - time::{Duration, Instant}, + ffi::c_void, + ptr::NonNull, + time::{Duration, Instant}, }; pub(crate) struct MacDispatcher; impl MacDispatcher { - pub fn new() -> Self { - Self - } + pub fn new() -> Self { + Self + } } impl PlatformDispatcher for MacDispatcher { - fn get_all_timings(&self) -> Vec { - let global_timings = GLOBAL_THREAD_TIMINGS.lock(); - ThreadTaskTimings::convert(&global_timings) - } + fn get_all_timings(&self) -> Vec { + let global_timings = GLOBAL_THREAD_TIMINGS.lock(); + ThreadTaskTimings::convert(&global_timings) + } - fn get_current_thread_timings(&self) -> ThreadTaskTimings { - gpui::profiler::get_current_thread_task_timings() - } + fn get_current_thread_timings(&self) -> ThreadTaskTimings { + gpui::profiler::get_current_thread_task_timings() + } - fn is_main_thread(&self) -> bool { - let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] }; - is_main_thread == YES - } + fn is_main_thread(&self) -> bool { + let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] }; + is_main_thread == YES + } - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - let context = runnable.into_raw().as_ptr() as *mut c_void; + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + let context = runnable.into_raw().as_ptr() as *mut c_void; - let queue_priority = match priority { - Priority::RealtimeAudio => { - panic!("RealtimeAudio priority should use spawn_realtime, not dispatch") - } - Priority::High => DispatchQueueGlobalPriority::High, - Priority::Medium => DispatchQueueGlobalPriority::Default, - Priority::Low => DispatchQueueGlobalPriority::Low, - }; + let queue_priority = match priority { + Priority::RealtimeAudio => { + panic!("RealtimeAudio priority should use spawn_realtime, not dispatch") + } + Priority::High => DispatchQueueGlobalPriority::High, + Priority::Medium => DispatchQueueGlobalPriority::Default, + Priority::Low => DispatchQueueGlobalPriority::Low, + }; - unsafe { - DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(queue_priority)) - .exec_async_f(context, trampoline); - } - } + unsafe { + DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(queue_priority)) + .exec_async_f(context, trampoline); + } + } - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) { - let context = runnable.into_raw().as_ptr() as *mut c_void; - unsafe { - DispatchQueue::main().exec_async_f(context, trampoline); - } - } + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) { + let context = runnable.into_raw().as_ptr() as *mut c_void; + unsafe { + DispatchQueue::main().exec_async_f(context, trampoline); + } + } - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { - let context = runnable.into_raw().as_ptr() as *mut c_void; - let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority( - DispatchQueueGlobalPriority::High, - )); - let when = DispatchTime::NOW.time(duration.as_nanos() as i64); - unsafe { - DispatchQueue::exec_after_f(when, &queue, context, trampoline); - } - } + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { + let context = runnable.into_raw().as_ptr() as *mut c_void; + let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority( + DispatchQueueGlobalPriority::High, + )); + let when = DispatchTime::NOW.time(duration.as_nanos() as i64); + unsafe { + DispatchQueue::exec_after_f(when, &queue, context, trampoline); + } + } - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - set_audio_thread_priority().log_err(); - f(); - }); - } + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + set_audio_thread_priority().log_err(); + f(); + }); + } } fn set_audio_thread_priority() -> anyhow::Result<()> { - // https://chromium.googlesource.com/chromium/chromium/+/master/base/threading/platform_thread_mac.mm#93 + // https://chromium.googlesource.com/chromium/chromium/+/master/base/threading/platform_thread_mac.mm#93 - // SAFETY: always safe to call - let thread_id = unsafe { libc::pthread_self() }; + // SAFETY: always safe to call + let thread_id = unsafe { libc::pthread_self() }; - // SAFETY: thread_id is a valid thread id - let thread_id = unsafe { libc::pthread_mach_thread_np(thread_id) }; + // SAFETY: thread_id is a valid thread id + let thread_id = unsafe { libc::pthread_mach_thread_np(thread_id) }; - // Fixed priority thread - let mut policy = thread_extended_policy_data_t { timeshare: 0 }; + // Fixed priority thread + let mut policy = thread_extended_policy_data_t { timeshare: 0 }; - // SAFETY: thread_id is a valid thread id - // SAFETY: thread_extended_policy_data_t is passed as THREAD_EXTENDED_POLICY - let result = unsafe { - mach2::thread_policy::thread_policy_set( - thread_id, - THREAD_EXTENDED_POLICY, - &mut policy as *mut _ as *mut _, - THREAD_EXTENDED_POLICY_COUNT, - ) - }; + // SAFETY: thread_id is a valid thread id + // SAFETY: thread_extended_policy_data_t is passed as THREAD_EXTENDED_POLICY + let result = unsafe { + mach2::thread_policy::thread_policy_set( + thread_id, + THREAD_EXTENDED_POLICY, + &mut policy as *mut _ as *mut _, + THREAD_EXTENDED_POLICY_COUNT, + ) + }; - if result != KERN_SUCCESS { - anyhow::bail!("failed to set thread extended policy"); - } + if result != KERN_SUCCESS { + anyhow::bail!("failed to set thread extended policy"); + } - // relatively high priority - let mut precedence = thread_precedence_policy_data_t { importance: 63 }; + // relatively high priority + let mut precedence = thread_precedence_policy_data_t { importance: 63 }; - // SAFETY: thread_id is a valid thread id - // SAFETY: thread_precedence_policy_data_t is passed as THREAD_PRECEDENCE_POLICY - let result = unsafe { - mach2::thread_policy::thread_policy_set( - thread_id, - THREAD_PRECEDENCE_POLICY, - &mut precedence as *mut _ as *mut _, - THREAD_PRECEDENCE_POLICY_COUNT, - ) - }; + // SAFETY: thread_id is a valid thread id + // SAFETY: thread_precedence_policy_data_t is passed as THREAD_PRECEDENCE_POLICY + let result = unsafe { + mach2::thread_policy::thread_policy_set( + thread_id, + THREAD_PRECEDENCE_POLICY, + &mut precedence as *mut _ as *mut _, + THREAD_PRECEDENCE_POLICY_COUNT, + ) + }; - if result != KERN_SUCCESS { - anyhow::bail!("failed to set thread precedence policy"); - } + if result != KERN_SUCCESS { + anyhow::bail!("failed to set thread precedence policy"); + } - const GUARANTEED_AUDIO_DUTY_CYCLE: f32 = 0.75; - const MAX_AUDIO_DUTY_CYCLE: f32 = 0.85; + const GUARANTEED_AUDIO_DUTY_CYCLE: f32 = 0.75; + const MAX_AUDIO_DUTY_CYCLE: f32 = 0.85; - // ~128 frames @ 44.1KHz - const TIME_QUANTUM: f32 = 2.9; + // ~128 frames @ 44.1KHz + const TIME_QUANTUM: f32 = 2.9; - const AUDIO_TIME_NEEDED: f32 = GUARANTEED_AUDIO_DUTY_CYCLE * TIME_QUANTUM; - const MAX_TIME_ALLOWED: f32 = MAX_AUDIO_DUTY_CYCLE * TIME_QUANTUM; + const AUDIO_TIME_NEEDED: f32 = GUARANTEED_AUDIO_DUTY_CYCLE * TIME_QUANTUM; + const MAX_TIME_ALLOWED: f32 = MAX_AUDIO_DUTY_CYCLE * TIME_QUANTUM; - let mut timebase_info = mach_timebase_info_data_t { numer: 0, denom: 0 }; - // SAFETY: timebase_info is a valid pointer to a mach_timebase_info_data_t struct - unsafe { mach2::mach_time::mach_timebase_info(&mut timebase_info) }; + let mut timebase_info = mach_timebase_info_data_t { numer: 0, denom: 0 }; + // SAFETY: timebase_info is a valid pointer to a mach_timebase_info_data_t struct + unsafe { mach2::mach_time::mach_timebase_info(&mut timebase_info) }; - let ms_to_abs_time = ((timebase_info.denom as f32) / (timebase_info.numer as f32)) * 1000000f32; + let ms_to_abs_time = ((timebase_info.denom as f32) / (timebase_info.numer as f32)) * 1000000f32; - let mut time_constraints = thread_time_constraint_policy_data_t { - period: (TIME_QUANTUM * ms_to_abs_time) as u32, - computation: (AUDIO_TIME_NEEDED * ms_to_abs_time) as u32, - constraint: (MAX_TIME_ALLOWED * ms_to_abs_time) as u32, - preemptible: 0, - }; + let mut time_constraints = thread_time_constraint_policy_data_t { + period: (TIME_QUANTUM * ms_to_abs_time) as u32, + computation: (AUDIO_TIME_NEEDED * ms_to_abs_time) as u32, + constraint: (MAX_TIME_ALLOWED * ms_to_abs_time) as u32, + preemptible: 0, + }; - // SAFETY: thread_id is a valid thread id - // SAFETY: thread_precedence_pthread_time_constraint_policy_data_t is passed as THREAD_TIME_CONSTRAINT_POLICY - let result = unsafe { - mach2::thread_policy::thread_policy_set( - thread_id, - THREAD_TIME_CONSTRAINT_POLICY, - &mut time_constraints as *mut _ as *mut _, - THREAD_TIME_CONSTRAINT_POLICY_COUNT, - ) - }; + // SAFETY: thread_id is a valid thread id + // SAFETY: thread_precedence_pthread_time_constraint_policy_data_t is passed as THREAD_TIME_CONSTRAINT_POLICY + let result = unsafe { + mach2::thread_policy::thread_policy_set( + thread_id, + THREAD_TIME_CONSTRAINT_POLICY, + &mut time_constraints as *mut _ as *mut _, + THREAD_TIME_CONSTRAINT_POLICY_COUNT, + ) + }; - if result != KERN_SUCCESS { - anyhow::bail!("failed to set thread time constraint policy"); - } + if result != KERN_SUCCESS { + anyhow::bail!("failed to set thread time constraint policy"); + } - Ok(()) + Ok(()) } extern "C" fn trampoline(context: *mut c_void) { - let runnable = - unsafe { Runnable::::from_raw(NonNull::new_unchecked(context as *mut ())) }; + let runnable = + unsafe { Runnable::::from_raw(NonNull::new_unchecked(context as *mut ())) }; - let location = runnable.metadata().location; + let location = runnable.metadata().location; - let start = Instant::now(); - let mut timing = TaskTiming { - location, - start, - end: None, - }; + let start = Instant::now(); + let mut timing = TaskTiming { + location, + start, + end: None, + }; - add_task_timing(timing); + add_task_timing(timing); - runnable.run(); + runnable.run(); - timing.end = Some(Instant::now()); - add_task_timing(timing); + timing.end = Some(Instant::now()); + add_task_timing(timing); } diff --git a/crates/gpui_macos/src/display.rs b/crates/gpui_macos/src/display.rs index 8e5db58935..167afc84d5 100644 --- a/crates/gpui_macos/src/display.rs +++ b/crates/gpui_macos/src/display.rs @@ -1,9 +1,9 @@ use crate::ns_string; use anyhow::Result; use cocoa::{ - appkit::NSScreen, - base::{id, nil}, - foundation::{NSArray, NSDictionary}, + appkit::NSScreen, + base::{id, nil}, + foundation::{NSArray, NSDictionary}, }; use core_foundation::base::CFRelease; use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; @@ -18,152 +18,152 @@ pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID); unsafe impl Send for MacDisplay {} impl MacDisplay { - /// Get the screen with the given [`DisplayId`]. - pub fn find_by_id(id: DisplayId) -> Option { - Self::all().find(|screen| screen.id() == id) - } + /// Get the screen with the given [`DisplayId`]. + pub fn find_by_id(id: DisplayId) -> Option { + Self::all().find(|screen| screen.id() == id) + } - /// Get the primary screen - the one with the menu bar, and whose bottom left - /// corner is at the origin of the AppKit coordinate system. - pub fn primary() -> Self { - // Instead of iterating through all active systems displays via `all()` we use the first - // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList` - // will always return a list of active displays (machine might be sleeping). - // - // The following is what Chromium does too: - // - // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56 - unsafe { - let screens = NSScreen::screens(nil); - let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0); - let device_description = NSScreen::deviceDescription(screen); - let screen_number_key: id = ns_string("NSScreenNumber"); - let screen_number = device_description.objectForKey_(screen_number_key); - let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; - Self(screen_number) - } - } + /// Get the primary screen - the one with the menu bar, and whose bottom left + /// corner is at the origin of the AppKit coordinate system. + pub fn primary() -> Self { + // Instead of iterating through all active systems displays via `all()` we use the first + // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList` + // will always return a list of active displays (machine might be sleeping). + // + // The following is what Chromium does too: + // + // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56 + unsafe { + let screens = NSScreen::screens(nil); + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0); + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = ns_string("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; + Self(screen_number) + } + } - /// Obtains an iterator over all currently active system displays. - pub fn all() -> impl Iterator { - unsafe { - // We're assuming there aren't more than 32 displays connected to the system. - let mut displays = Vec::with_capacity(32); - let mut display_count = 0; - let result = CGGetActiveDisplayList( - displays.capacity() as u32, - displays.as_mut_ptr(), - &mut display_count, - ); + /// Obtains an iterator over all currently active system displays. + pub fn all() -> impl Iterator { + unsafe { + // We're assuming there aren't more than 32 displays connected to the system. + let mut displays = Vec::with_capacity(32); + let mut display_count = 0; + let result = CGGetActiveDisplayList( + displays.capacity() as u32, + displays.as_mut_ptr(), + &mut display_count, + ); - if result == 0 { - displays.set_len(display_count as usize); - displays.into_iter().map(MacDisplay) - } else { - panic!("Failed to get active display list. Result: {result}"); - } - } - } + if result == 0 { + displays.set_len(display_count as usize); + displays.into_iter().map(MacDisplay) + } else { + panic!("Failed to get active display list. Result: {result}"); + } + } + } } #[link(name = "ApplicationServices", kind = "framework")] unsafe extern "C" { - fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; } impl PlatformDisplay for MacDisplay { - fn id(&self) -> DisplayId { - DisplayId::new(self.0 as u64) - } + fn id(&self) -> DisplayId { + DisplayId::new(self.0 as u64) + } - fn uuid(&self) -> Result { - let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) }; - anyhow::ensure!( - !cfuuid.is_null(), - "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID" - ); + fn uuid(&self) -> Result { + let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) }; + anyhow::ensure!( + !cfuuid.is_null(), + "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID" + ); - let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) }; - unsafe { CFRelease(cfuuid as _) }; - Ok(Uuid::from_bytes([ - bytes.byte0, - bytes.byte1, - bytes.byte2, - bytes.byte3, - bytes.byte4, - bytes.byte5, - bytes.byte6, - bytes.byte7, - bytes.byte8, - bytes.byte9, - bytes.byte10, - bytes.byte11, - bytes.byte12, - bytes.byte13, - bytes.byte14, - bytes.byte15, - ])) - } + let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) }; + unsafe { CFRelease(cfuuid as _) }; + Ok(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } - fn bounds(&self) -> Bounds { - unsafe { - // CGDisplayBounds is in "global display" coordinates, where 0 is - // the top left of the primary display. - let bounds = CGDisplayBounds(self.0); + fn bounds(&self) -> Bounds { + unsafe { + // CGDisplayBounds is in "global display" coordinates, where 0 is + // the top left of the primary display. + let bounds = CGDisplayBounds(self.0); - Bounds { - origin: Default::default(), - size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)), - } - } - } + Bounds { + origin: Default::default(), + size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)), + } + } + } - fn visible_bounds(&self) -> Bounds { - unsafe { - let dominated_screen = self.get_nsscreen(); + fn visible_bounds(&self) -> Bounds { + unsafe { + let dominated_screen = self.get_nsscreen(); - if dominated_screen == nil { - return self.bounds(); - } + if dominated_screen == nil { + return self.bounds(); + } - let screen_frame = NSScreen::frame(dominated_screen); - let visible_frame = NSScreen::visibleFrame(dominated_screen); + let screen_frame = NSScreen::frame(dominated_screen); + let visible_frame = NSScreen::visibleFrame(dominated_screen); - // Convert from bottom-left origin (AppKit) to top-left origin - let origin_y = - screen_frame.size.height - visible_frame.origin.y - visible_frame.size.height - + screen_frame.origin.y; + // Convert from bottom-left origin (AppKit) to top-left origin + let origin_y = + screen_frame.size.height - visible_frame.origin.y - visible_frame.size.height + + screen_frame.origin.y; - Bounds { - origin: point( - px(visible_frame.origin.x as f32 - screen_frame.origin.x as f32), - px(origin_y as f32), - ), - size: size( - px(visible_frame.size.width as f32), - px(visible_frame.size.height as f32), - ), - } - } - } + Bounds { + origin: point( + px(visible_frame.origin.x as f32 - screen_frame.origin.x as f32), + px(origin_y as f32), + ), + size: size( + px(visible_frame.size.width as f32), + px(visible_frame.size.height as f32), + ), + } + } + } } impl MacDisplay { - /// Find the NSScreen corresponding to this display - unsafe fn get_nsscreen(&self) -> id { - let screens = unsafe { NSScreen::screens(nil) }; - let count = unsafe { NSArray::count(screens) }; - let screen_number_key: id = unsafe { ns_string("NSScreenNumber") }; + /// Find the NSScreen corresponding to this display + unsafe fn get_nsscreen(&self) -> id { + let screens = unsafe { NSScreen::screens(nil) }; + let count = unsafe { NSArray::count(screens) }; + let screen_number_key: id = unsafe { ns_string("NSScreenNumber") }; - for i in 0..count { - let screen = unsafe { NSArray::objectAtIndex(screens, i) }; - let device_description = unsafe { NSScreen::deviceDescription(screen) }; - let screen_number = unsafe { device_description.objectForKey_(screen_number_key) }; - let screen_id: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; - if screen_id == self.0 { - return screen; - } - } - nil - } + for i in 0..count { + let screen = unsafe { NSArray::objectAtIndex(screens, i) }; + let device_description = unsafe { NSScreen::deviceDescription(screen) }; + let screen_number = unsafe { device_description.objectForKey_(screen_number_key) }; + let screen_id: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; + if screen_id == self.0 { + return screen; + } + } + nil + } } diff --git a/crates/gpui_macos/src/display_link.rs b/crates/gpui_macos/src/display_link.rs index 86e9b4072b..3c7445ef08 100644 --- a/crates/gpui_macos/src/display_link.rs +++ b/crates/gpui_macos/src/display_link.rs @@ -1,266 +1,266 @@ use anyhow::Result; use core_graphics::display::CGDirectDisplayID; use dispatch2::{ - _dispatch_source_type_data_add, DispatchObject, DispatchQueue, DispatchRetained, DispatchSource, + _dispatch_source_type_data_add, DispatchObject, DispatchQueue, DispatchRetained, DispatchSource, }; use std::ffi::c_void; use util::ResultExt; pub struct DisplayLink { - display_link: Option, - frame_requests: DispatchRetained, + display_link: Option, + frame_requests: DispatchRetained, } impl DisplayLink { - pub fn new( - display_id: CGDirectDisplayID, - data: *mut c_void, - callback: extern "C" fn(*mut c_void), - ) -> Result { - unsafe extern "C" fn display_link_callback( - _display_link_out: *mut sys::CVDisplayLink, - _current_time: *const sys::CVTimeStamp, - _output_time: *const sys::CVTimeStamp, - _flags_in: i64, - _flags_out: *mut i64, - frame_requests: *mut c_void, - ) -> i32 { - unsafe { - let frame_requests = &*(frame_requests as *const DispatchSource); - frame_requests.merge_data(1); - 0 - } - } + pub fn new( + display_id: CGDirectDisplayID, + data: *mut c_void, + callback: extern "C" fn(*mut c_void), + ) -> Result { + unsafe extern "C" fn display_link_callback( + _display_link_out: *mut sys::CVDisplayLink, + _current_time: *const sys::CVTimeStamp, + _output_time: *const sys::CVTimeStamp, + _flags_in: i64, + _flags_out: *mut i64, + frame_requests: *mut c_void, + ) -> i32 { + unsafe { + let frame_requests = &*(frame_requests as *const DispatchSource); + frame_requests.merge_data(1); + 0 + } + } - unsafe { - let frame_requests = DispatchSource::new( - &raw const _dispatch_source_type_data_add as *mut _, - 0, - 0, - Some(DispatchQueue::main()), - ); - frame_requests.set_context(data); - frame_requests.set_event_handler_f(callback); - frame_requests.resume(); + unsafe { + let frame_requests = DispatchSource::new( + &raw const _dispatch_source_type_data_add as *mut _, + 0, + 0, + Some(DispatchQueue::main()), + ); + frame_requests.set_context(data); + frame_requests.set_event_handler_f(callback); + frame_requests.resume(); - let display_link = sys::DisplayLink::new( - display_id, - display_link_callback, - &*frame_requests as *const DispatchSource as *mut c_void, - )?; + let display_link = sys::DisplayLink::new( + display_id, + display_link_callback, + &*frame_requests as *const DispatchSource as *mut c_void, + )?; - Ok(Self { - display_link: Some(display_link), - frame_requests, - }) - } - } + Ok(Self { + display_link: Some(display_link), + frame_requests, + }) + } + } - pub fn start(&mut self) -> Result<()> { - unsafe { - self.display_link.as_mut().unwrap().start()?; - } - Ok(()) - } + pub fn start(&mut self) -> Result<()> { + unsafe { + self.display_link.as_mut().unwrap().start()?; + } + Ok(()) + } - pub fn stop(&mut self) -> Result<()> { - unsafe { - self.display_link.as_mut().unwrap().stop()?; - } - Ok(()) - } + pub fn stop(&mut self) -> Result<()> { + unsafe { + self.display_link.as_mut().unwrap().stop()?; + } + Ok(()) + } } impl Drop for DisplayLink { - fn drop(&mut self) { - self.stop().log_err(); - // We see occasional segfaults on the CVDisplayLink thread. - // - // It seems possible that this happens because CVDisplayLinkRelease releases the CVDisplayLink - // on the main thread immediately, but the background thread that CVDisplayLink uses for timers - // is still accessing it. - // - // We might also want to upgrade to CADisplayLink, but that requires dropping old macOS support. - std::mem::forget(self.display_link.take()); - self.frame_requests.cancel(); - } + fn drop(&mut self) { + self.stop().log_err(); + // We see occasional segfaults on the CVDisplayLink thread. + // + // It seems possible that this happens because CVDisplayLinkRelease releases the CVDisplayLink + // on the main thread immediately, but the background thread that CVDisplayLink uses for timers + // is still accessing it. + // + // We might also want to upgrade to CADisplayLink, but that requires dropping old macOS support. + std::mem::forget(self.display_link.take()); + self.frame_requests.cancel(); + } } mod sys { - //! Derived from display-link crate under the following license: - //! - //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc) - #![allow(dead_code, non_upper_case_globals)] + //! Derived from display-link crate under the following license: + //! + //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc) + #![allow(dead_code, non_upper_case_globals)] - use anyhow::Result; - use core_graphics::display::CGDirectDisplayID; - use foreign_types::{ForeignType, foreign_type}; - use std::{ - ffi::c_void, - fmt::{self, Debug, Formatter}, - }; + use anyhow::Result; + use core_graphics::display::CGDirectDisplayID; + use foreign_types::{ForeignType, foreign_type}; + use std::{ + ffi::c_void, + fmt::{self, Debug, Formatter}, + }; - #[derive(Debug)] - pub enum CVDisplayLink {} + #[derive(Debug)] + pub enum CVDisplayLink {} - foreign_type! { - pub unsafe type DisplayLink { - type CType = CVDisplayLink; - fn drop = CVDisplayLinkRelease; - fn clone = CVDisplayLinkRetain; - } - } + foreign_type! { + pub unsafe type DisplayLink { + type CType = CVDisplayLink; + fn drop = CVDisplayLinkRelease; + fn clone = CVDisplayLinkRetain; + } + } - impl Debug for DisplayLink { - fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { - formatter - .debug_tuple("DisplayLink") - .field(&self.as_ptr()) - .finish() - } - } + impl Debug for DisplayLink { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter + .debug_tuple("DisplayLink") + .field(&self.as_ptr()) + .finish() + } + } - #[repr(C)] - #[derive(Clone, Copy)] - pub(crate) struct CVTimeStamp { - pub version: u32, - pub video_time_scale: i32, - pub video_time: i64, - pub host_time: u64, - pub rate_scalar: f64, - pub video_refresh_period: i64, - pub smpte_time: CVSMPTETime, - pub flags: u64, - pub reserved: u64, - } + #[repr(C)] + #[derive(Clone, Copy)] + pub(crate) struct CVTimeStamp { + pub version: u32, + pub video_time_scale: i32, + pub video_time: i64, + pub host_time: u64, + pub rate_scalar: f64, + pub video_refresh_period: i64, + pub smpte_time: CVSMPTETime, + pub flags: u64, + pub reserved: u64, + } - pub type CVTimeStampFlags = u64; + pub type CVTimeStampFlags = u64; - pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0; - pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1; - pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2; - pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3; - pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4; - pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16; - pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17; - pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags = - kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid; - pub const kCVTimeStampIsInterlaced: CVTimeStampFlags = - kCVTimeStampTopField | kCVTimeStampBottomField; + pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0; + pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1; + pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2; + pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3; + pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4; + pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16; + pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17; + pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags = + kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid; + pub const kCVTimeStampIsInterlaced: CVTimeStampFlags = + kCVTimeStampTopField | kCVTimeStampBottomField; - #[repr(C)] - #[derive(Clone, Copy, Default)] - pub(crate) struct CVSMPTETime { - pub subframes: i16, - pub subframe_divisor: i16, - pub counter: u32, - pub time_type: u32, - pub flags: u32, - pub hours: i16, - pub minutes: i16, - pub seconds: i16, - pub frames: i16, - } + #[repr(C)] + #[derive(Clone, Copy, Default)] + pub(crate) struct CVSMPTETime { + pub subframes: i16, + pub subframe_divisor: i16, + pub counter: u32, + pub time_type: u32, + pub flags: u32, + pub hours: i16, + pub minutes: i16, + pub seconds: i16, + pub frames: i16, + } - pub type CVSMPTETimeType = u32; + pub type CVSMPTETimeType = u32; - pub const kCVSMPTETimeType24: CVSMPTETimeType = 0; - pub const kCVSMPTETimeType25: CVSMPTETimeType = 1; - pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2; - pub const kCVSMPTETimeType30: CVSMPTETimeType = 3; - pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4; - pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5; - pub const kCVSMPTETimeType60: CVSMPTETimeType = 6; - pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7; + pub const kCVSMPTETimeType24: CVSMPTETimeType = 0; + pub const kCVSMPTETimeType25: CVSMPTETimeType = 1; + pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2; + pub const kCVSMPTETimeType30: CVSMPTETimeType = 3; + pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4; + pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5; + pub const kCVSMPTETimeType60: CVSMPTETimeType = 6; + pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7; - pub type CVSMPTETimeFlags = u32; + pub type CVSMPTETimeFlags = u32; - pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0; - pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1; + pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0; + pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1; - pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn( - display_link_out: *mut CVDisplayLink, - // A pointer to the current timestamp. This represents the timestamp when the callback is called. - current_time: *const CVTimeStamp, - // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed. - output_time: *const CVTimeStamp, - // Unused - flags_in: i64, - // Unused - flags_out: *mut i64, - // A pointer to app-defined data. - display_link_context: *mut c_void, - ) -> i32; + pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn( + display_link_out: *mut CVDisplayLink, + // A pointer to the current timestamp. This represents the timestamp when the callback is called. + current_time: *const CVTimeStamp, + // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed. + output_time: *const CVTimeStamp, + // Unused + flags_in: i64, + // Unused + flags_out: *mut i64, + // A pointer to app-defined data. + display_link_context: *mut c_void, + ) -> i32; - #[link(name = "CoreFoundation", kind = "framework")] - #[link(name = "CoreVideo", kind = "framework")] - #[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)] - unsafe extern "C" { - pub fn CVDisplayLinkCreateWithActiveCGDisplays( - display_link_out: *mut *mut CVDisplayLink, - ) -> i32; - pub fn CVDisplayLinkSetCurrentCGDisplay( - display_link: &mut DisplayLinkRef, - display_id: u32, - ) -> i32; - pub fn CVDisplayLinkSetOutputCallback( - display_link: &mut DisplayLinkRef, - callback: CVDisplayLinkOutputCallback, - user_info: *mut c_void, - ) -> i32; - pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32; - pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32; - pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink); - pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink; - } + #[link(name = "CoreFoundation", kind = "framework")] + #[link(name = "CoreVideo", kind = "framework")] + #[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)] + unsafe extern "C" { + pub fn CVDisplayLinkCreateWithActiveCGDisplays( + display_link_out: *mut *mut CVDisplayLink, + ) -> i32; + pub fn CVDisplayLinkSetCurrentCGDisplay( + display_link: &mut DisplayLinkRef, + display_id: u32, + ) -> i32; + pub fn CVDisplayLinkSetOutputCallback( + display_link: &mut DisplayLinkRef, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> i32; + pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink); + pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink; + } - impl DisplayLink { - /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc) - pub unsafe fn new( - display_id: CGDirectDisplayID, - callback: CVDisplayLinkOutputCallback, - user_info: *mut c_void, - ) -> Result { - unsafe { - let mut display_link: *mut CVDisplayLink = 0 as _; + impl DisplayLink { + /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc) + pub unsafe fn new( + display_id: CGDirectDisplayID, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> Result { + unsafe { + let mut display_link: *mut CVDisplayLink = 0 as _; - let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link); - anyhow::ensure!(code == 0, "could not create display link, code: {}", code); + let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link); + anyhow::ensure!(code == 0, "could not create display link, code: {}", code); - let mut display_link = DisplayLink::from_ptr(display_link); + let mut display_link = DisplayLink::from_ptr(display_link); - let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info); - anyhow::ensure!(code == 0, "could not set output callback, code: {}", code); + let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info); + anyhow::ensure!(code == 0, "could not set output callback, code: {}", code); - let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id); - anyhow::ensure!( - code == 0, - "could not assign display to display link, code: {}", - code - ); + let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id); + anyhow::ensure!( + code == 0, + "could not assign display to display link, code: {}", + code + ); - Ok(display_link) - } - } - } + Ok(display_link) + } + } + } - impl DisplayLinkRef { - /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc) - pub unsafe fn start(&mut self) -> Result<()> { - unsafe { - let code = CVDisplayLinkStart(self); - anyhow::ensure!(code == 0, "could not start display link, code: {}", code); - Ok(()) - } - } + impl DisplayLinkRef { + /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc) + pub unsafe fn start(&mut self) -> Result<()> { + unsafe { + let code = CVDisplayLinkStart(self); + anyhow::ensure!(code == 0, "could not start display link, code: {}", code); + Ok(()) + } + } - /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc) - pub unsafe fn stop(&mut self) -> Result<()> { - unsafe { - let code = CVDisplayLinkStop(self); - anyhow::ensure!(code == 0, "could not stop display link, code: {}", code); - Ok(()) - } - } - } + /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc) + pub unsafe fn stop(&mut self) -> Result<()> { + unsafe { + let code = CVDisplayLinkStop(self); + anyhow::ensure!(code == 0, "could not stop display link, code: {}", code); + Ok(()) + } + } + } } diff --git a/crates/gpui_macos/src/events.rs b/crates/gpui_macos/src/events.rs index 71bcb105e8..b903dd85eb 100644 --- a/crates/gpui_macos/src/events.rs +++ b/crates/gpui_macos/src/events.rs @@ -1,17 +1,17 @@ use gpui::{ - Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, - MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, - NavigationDirection, PinchEvent, Pixels, PlatformInput, PressureStage, ScrollDelta, - ScrollWheelEvent, TouchPhase, point, px, + Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, + NavigationDirection, PinchEvent, Pixels, PlatformInput, PressureStage, ScrollDelta, + ScrollWheelEvent, TouchPhase, point, px, }; use crate::{ - LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, - UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData, + LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, + UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData, }; use cocoa::{ - appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, - base::{YES, id}, + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, + base::{YES, id}, }; use core_foundation::data::{CFDataGetBytePtr, CFDataRef}; use core_graphics::event::CGKeyCode; @@ -27,481 +27,481 @@ const TAB_KEY: u16 = 0x09; const SHIFT_TAB_KEY: u16 = 0x19; pub fn key_to_native(key: &str) -> Cow<'_, str> { - use cocoa::appkit::*; - let code = match key { - "space" => SPACE_KEY, - "backspace" => BACKSPACE_KEY, - "escape" => ESCAPE_KEY, - "up" => NSUpArrowFunctionKey, - "down" => NSDownArrowFunctionKey, - "left" => NSLeftArrowFunctionKey, - "right" => NSRightArrowFunctionKey, - "pageup" => NSPageUpFunctionKey, - "pagedown" => NSPageDownFunctionKey, - "home" => NSHomeFunctionKey, - "end" => NSEndFunctionKey, - "delete" => NSDeleteFunctionKey, - "insert" => NSHelpFunctionKey, - "f1" => NSF1FunctionKey, - "f2" => NSF2FunctionKey, - "f3" => NSF3FunctionKey, - "f4" => NSF4FunctionKey, - "f5" => NSF5FunctionKey, - "f6" => NSF6FunctionKey, - "f7" => NSF7FunctionKey, - "f8" => NSF8FunctionKey, - "f9" => NSF9FunctionKey, - "f10" => NSF10FunctionKey, - "f11" => NSF11FunctionKey, - "f12" => NSF12FunctionKey, - "f13" => NSF13FunctionKey, - "f14" => NSF14FunctionKey, - "f15" => NSF15FunctionKey, - "f16" => NSF16FunctionKey, - "f17" => NSF17FunctionKey, - "f18" => NSF18FunctionKey, - "f19" => NSF19FunctionKey, - "f20" => NSF20FunctionKey, - "f21" => NSF21FunctionKey, - "f22" => NSF22FunctionKey, - "f23" => NSF23FunctionKey, - "f24" => NSF24FunctionKey, - "f25" => NSF25FunctionKey, - "f26" => NSF26FunctionKey, - "f27" => NSF27FunctionKey, - "f28" => NSF28FunctionKey, - "f29" => NSF29FunctionKey, - "f30" => NSF30FunctionKey, - "f31" => NSF31FunctionKey, - "f32" => NSF32FunctionKey, - "f33" => NSF33FunctionKey, - "f34" => NSF34FunctionKey, - "f35" => NSF35FunctionKey, - _ => return Cow::Borrowed(key), - }; - Cow::Owned(String::from_utf16(&[code]).unwrap()) + use cocoa::appkit::*; + let code = match key { + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, + "escape" => ESCAPE_KEY, + "up" => NSUpArrowFunctionKey, + "down" => NSDownArrowFunctionKey, + "left" => NSLeftArrowFunctionKey, + "right" => NSRightArrowFunctionKey, + "pageup" => NSPageUpFunctionKey, + "pagedown" => NSPageDownFunctionKey, + "home" => NSHomeFunctionKey, + "end" => NSEndFunctionKey, + "delete" => NSDeleteFunctionKey, + "insert" => NSHelpFunctionKey, + "f1" => NSF1FunctionKey, + "f2" => NSF2FunctionKey, + "f3" => NSF3FunctionKey, + "f4" => NSF4FunctionKey, + "f5" => NSF5FunctionKey, + "f6" => NSF6FunctionKey, + "f7" => NSF7FunctionKey, + "f8" => NSF8FunctionKey, + "f9" => NSF9FunctionKey, + "f10" => NSF10FunctionKey, + "f11" => NSF11FunctionKey, + "f12" => NSF12FunctionKey, + "f13" => NSF13FunctionKey, + "f14" => NSF14FunctionKey, + "f15" => NSF15FunctionKey, + "f16" => NSF16FunctionKey, + "f17" => NSF17FunctionKey, + "f18" => NSF18FunctionKey, + "f19" => NSF19FunctionKey, + "f20" => NSF20FunctionKey, + "f21" => NSF21FunctionKey, + "f22" => NSF22FunctionKey, + "f23" => NSF23FunctionKey, + "f24" => NSF24FunctionKey, + "f25" => NSF25FunctionKey, + "f26" => NSF26FunctionKey, + "f27" => NSF27FunctionKey, + "f28" => NSF28FunctionKey, + "f29" => NSF29FunctionKey, + "f30" => NSF30FunctionKey, + "f31" => NSF31FunctionKey, + "f32" => NSF32FunctionKey, + "f33" => NSF33FunctionKey, + "f34" => NSF34FunctionKey, + "f35" => NSF35FunctionKey, + _ => return Cow::Borrowed(key), + }; + Cow::Owned(String::from_utf16(&[code]).unwrap()) } unsafe fn read_modifiers(native_event: id) -> Modifiers { - unsafe { - let modifiers = native_event.modifierFlags(); - let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + unsafe { + let modifiers = native_event.modifierFlags(); + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - Modifiers { - control, - alt, - shift, - platform: command, - function, - } - } + Modifiers { + control, + alt, + shift, + platform: command, + function, + } + } } pub(crate) unsafe fn platform_input_from_native( - native_event: id, - window_height: Option, + native_event: id, + window_height: Option, ) -> Option { - unsafe { - let event_type = native_event.eventType(); + unsafe { + let event_type = native_event.eventType(); - // Filter out event types that aren't in the NSEventType enum. - // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. - match event_type as u64 { - 0 | 21 | 32 | 33 | 35 | 36 | 37 => { - return None; - } - _ => {} - } + // Filter out event types that aren't in the NSEventType enum. + // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. + match event_type as u64 { + 0 | 21 | 32 | 33 | 35 | 36 | 37 => { + return None; + } + _ => {} + } - match event_type { - NSEventType::NSFlagsChanged => { - Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers: read_modifiers(native_event), - capslock: Capslock { - on: native_event - .modifierFlags() - .contains(NSEventModifierFlags::NSAlphaShiftKeyMask), - }, - })) - } - NSEventType::NSKeyDown => Some(PlatformInput::KeyDown(KeyDownEvent { - keystroke: parse_keystroke(native_event), - is_held: native_event.isARepeat() == YES, - prefer_character_input: false, - })), - NSEventType::NSKeyUp => Some(PlatformInput::KeyUp(KeyUpEvent { - keystroke: parse_keystroke(native_event), - })), - NSEventType::NSLeftMouseDown - | NSEventType::NSRightMouseDown - | NSEventType::NSOtherMouseDown => { - let button = match native_event.buttonNumber() { - 0 => MouseButton::Left, - 1 => MouseButton::Right, - 2 => MouseButton::Middle, - 3 => MouseButton::Navigate(NavigationDirection::Back), - 4 => MouseButton::Navigate(NavigationDirection::Forward), - // Other mouse buttons aren't tracked currently - _ => return None, - }; - window_height.map(|window_height| { - PlatformInput::MouseDown(MouseDownEvent { - button, - position: point( - px(native_event.locationInWindow().x as f32), - // MacOS screen coordinates are relative to bottom left - window_height - px(native_event.locationInWindow().y as f32), - ), - modifiers: read_modifiers(native_event), - click_count: native_event.clickCount() as usize, - first_mouse: false, - }) - }) - } - NSEventType::NSLeftMouseUp - | NSEventType::NSRightMouseUp - | NSEventType::NSOtherMouseUp => { - let button = match native_event.buttonNumber() { - 0 => MouseButton::Left, - 1 => MouseButton::Right, - 2 => MouseButton::Middle, - 3 => MouseButton::Navigate(NavigationDirection::Back), - 4 => MouseButton::Navigate(NavigationDirection::Forward), - // Other mouse buttons aren't tracked currently - _ => return None, - }; + match event_type { + NSEventType::NSFlagsChanged => { + Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: read_modifiers(native_event), + capslock: Capslock { + on: native_event + .modifierFlags() + .contains(NSEventModifierFlags::NSAlphaShiftKeyMask), + }, + })) + } + NSEventType::NSKeyDown => Some(PlatformInput::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + prefer_character_input: false, + })), + NSEventType::NSKeyUp => Some(PlatformInput::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), + NSEventType::NSLeftMouseDown + | NSEventType::NSRightMouseDown + | NSEventType::NSOtherMouseDown => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + window_height.map(|window_height| { + PlatformInput::MouseDown(MouseDownEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + first_mouse: false, + }) + }) + } + NSEventType::NSLeftMouseUp + | NSEventType::NSRightMouseUp + | NSEventType::NSOtherMouseUp => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; - window_height.map(|window_height| { - PlatformInput::MouseUp(MouseUpEvent { - button, - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - modifiers: read_modifiers(native_event), - click_count: native_event.clickCount() as usize, - }) - }) - } - NSEventType::NSEventTypePressure => { - let stage = native_event.stage(); - let pressure = native_event.pressure(); + window_height.map(|window_height| { + PlatformInput::MouseUp(MouseUpEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + }) + }) + } + NSEventType::NSEventTypePressure => { + let stage = native_event.stage(); + let pressure = native_event.pressure(); - window_height.map(|window_height| { - PlatformInput::MousePressure(MousePressureEvent { - stage: match stage { - 1 => PressureStage::Normal, - 2 => PressureStage::Force, - _ => PressureStage::Zero, - }, - pressure, - modifiers: read_modifiers(native_event), - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - }) - }) - } - // Some mice (like Logitech MX Master) send navigation buttons as swipe events - NSEventType::NSEventTypeSwipe => { - let navigation_direction = match native_event.phase() { - NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() { - x if x > 0.0 => Some(NavigationDirection::Back), - x if x < 0.0 => Some(NavigationDirection::Forward), - _ => return None, - }, - _ => return None, - }; + window_height.map(|window_height| { + PlatformInput::MousePressure(MousePressureEvent { + stage: match stage { + 1 => PressureStage::Normal, + 2 => PressureStage::Force, + _ => PressureStage::Zero, + }, + pressure, + modifiers: read_modifiers(native_event), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + }) + }) + } + // Some mice (like Logitech MX Master) send navigation buttons as swipe events + NSEventType::NSEventTypeSwipe => { + let navigation_direction = match native_event.phase() { + NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() { + x if x > 0.0 => Some(NavigationDirection::Back), + x if x < 0.0 => Some(NavigationDirection::Forward), + _ => return None, + }, + _ => return None, + }; - match navigation_direction { - Some(direction) => window_height.map(|window_height| { - PlatformInput::MouseDown(MouseDownEvent { - button: MouseButton::Navigate(direction), - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - modifiers: read_modifiers(native_event), - click_count: 1, - first_mouse: false, - }) - }), - _ => None, - } - } - NSEventType::NSEventTypeMagnify => window_height.map(|window_height| { - let phase = match native_event.phase() { - NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { - TouchPhase::Started - } - NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, - _ => TouchPhase::Moved, - }; + match navigation_direction { + Some(direction) => window_height.map(|window_height| { + PlatformInput::MouseDown(MouseDownEvent { + button: MouseButton::Navigate(direction), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: 1, + first_mouse: false, + }) + }), + _ => None, + } + } + NSEventType::NSEventTypeMagnify => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; - let magnification = native_event.magnification() as f32; + let magnification = native_event.magnification() as f32; - PlatformInput::Pinch(PinchEvent { - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - delta: magnification, - modifiers: read_modifiers(native_event), - phase, - }) - }), - NSEventType::NSScrollWheel => window_height.map(|window_height| { - let phase = match native_event.phase() { - NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { - TouchPhase::Started - } - NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, - _ => TouchPhase::Moved, - }; + PlatformInput::Pinch(PinchEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta: magnification, + modifiers: read_modifiers(native_event), + phase, + }) + }), + NSEventType::NSScrollWheel => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; - let raw_data = point( - native_event.scrollingDeltaX() as f32, - native_event.scrollingDeltaY() as f32, - ); + let raw_data = point( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ); - let delta = if native_event.hasPreciseScrollingDeltas() == YES { - ScrollDelta::Pixels(raw_data.map(px)) - } else { - ScrollDelta::Lines(raw_data) - }; + let delta = if native_event.hasPreciseScrollingDeltas() == YES { + ScrollDelta::Pixels(raw_data.map(px)) + } else { + ScrollDelta::Lines(raw_data) + }; - PlatformInput::ScrollWheel(ScrollWheelEvent { - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - delta, - touch_phase: phase, - modifiers: read_modifiers(native_event), - }) - }), - NSEventType::NSLeftMouseDragged - | NSEventType::NSRightMouseDragged - | NSEventType::NSOtherMouseDragged => { - let pressed_button = match native_event.buttonNumber() { - 0 => MouseButton::Left, - 1 => MouseButton::Right, - 2 => MouseButton::Middle, - 3 => MouseButton::Navigate(NavigationDirection::Back), - 4 => MouseButton::Navigate(NavigationDirection::Forward), - // Other mouse buttons aren't tracked currently - _ => return None, - }; + PlatformInput::ScrollWheel(ScrollWheelEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta, + touch_phase: phase, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSLeftMouseDragged + | NSEventType::NSRightMouseDragged + | NSEventType::NSOtherMouseDragged => { + let pressed_button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; - window_height.map(|window_height| { - PlatformInput::MouseMove(MouseMoveEvent { - pressed_button: Some(pressed_button), - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - modifiers: read_modifiers(native_event), - }) - }) - } - NSEventType::NSMouseMoved => window_height.map(|window_height| { - PlatformInput::MouseMove(MouseMoveEvent { - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), - pressed_button: None, - modifiers: read_modifiers(native_event), - }) - }), - NSEventType::NSMouseExited => window_height.map(|window_height| { - PlatformInput::MouseExited(MouseExitEvent { - position: point( - px(native_event.locationInWindow().x as f32), - window_height - px(native_event.locationInWindow().y as f32), - ), + window_height.map(|window_height| { + PlatformInput::MouseMove(MouseMoveEvent { + pressed_button: Some(pressed_button), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + }) + }) + } + NSEventType::NSMouseMoved => window_height.map(|window_height| { + PlatformInput::MouseMove(MouseMoveEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSMouseExited => window_height.map(|window_height| { + PlatformInput::MouseExited(MouseExitEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), - pressed_button: None, - modifiers: read_modifiers(native_event), - }) - }), - _ => None, - } - } + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + _ => None, + } + } } unsafe fn parse_keystroke(native_event: id) -> Keystroke { - unsafe { - use cocoa::appkit::*; + unsafe { + use cocoa::appkit::*; - let characters = native_event - .charactersIgnoringModifiers() - .to_str() - .to_string(); - let mut key_char = None; - let first_char = characters.chars().next().map(|ch| ch as u16); - let modifiers = native_event.modifierFlags(); + let characters = native_event + .charactersIgnoringModifiers() + .to_str() + .to_string(); + let mut key_char = None; + let first_char = characters.chars().next().map(|ch| ch as u16); + let modifiers = native_event.modifierFlags(); - let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) - && first_char - .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char + .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); - #[allow(non_upper_case_globals)] - let key = match first_char { - Some(SPACE_KEY) => { - key_char = Some(" ".to_string()); - "space".to_string() - } - Some(TAB_KEY) => { - key_char = Some("\t".to_string()); - "tab".to_string() - } - Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => { - key_char = Some("\n".to_string()); - "enter".to_string() - } - Some(BACKSPACE_KEY) => "backspace".to_string(), - Some(ESCAPE_KEY) => "escape".to_string(), - Some(SHIFT_TAB_KEY) => "tab".to_string(), - Some(NSUpArrowFunctionKey) => "up".to_string(), - Some(NSDownArrowFunctionKey) => "down".to_string(), - Some(NSLeftArrowFunctionKey) => "left".to_string(), - Some(NSRightArrowFunctionKey) => "right".to_string(), - Some(NSPageUpFunctionKey) => "pageup".to_string(), - Some(NSPageDownFunctionKey) => "pagedown".to_string(), - Some(NSHomeFunctionKey) => "home".to_string(), - Some(NSEndFunctionKey) => "end".to_string(), - Some(NSDeleteFunctionKey) => "delete".to_string(), - // Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey. - Some(NSHelpFunctionKey) => "insert".to_string(), - Some(NSF1FunctionKey) => "f1".to_string(), - Some(NSF2FunctionKey) => "f2".to_string(), - Some(NSF3FunctionKey) => "f3".to_string(), - Some(NSF4FunctionKey) => "f4".to_string(), - Some(NSF5FunctionKey) => "f5".to_string(), - Some(NSF6FunctionKey) => "f6".to_string(), - Some(NSF7FunctionKey) => "f7".to_string(), - Some(NSF8FunctionKey) => "f8".to_string(), - Some(NSF9FunctionKey) => "f9".to_string(), - Some(NSF10FunctionKey) => "f10".to_string(), - Some(NSF11FunctionKey) => "f11".to_string(), - Some(NSF12FunctionKey) => "f12".to_string(), - Some(NSF13FunctionKey) => "f13".to_string(), - Some(NSF14FunctionKey) => "f14".to_string(), - Some(NSF15FunctionKey) => "f15".to_string(), - Some(NSF16FunctionKey) => "f16".to_string(), - Some(NSF17FunctionKey) => "f17".to_string(), - Some(NSF18FunctionKey) => "f18".to_string(), - Some(NSF19FunctionKey) => "f19".to_string(), - Some(NSF20FunctionKey) => "f20".to_string(), - Some(NSF21FunctionKey) => "f21".to_string(), - Some(NSF22FunctionKey) => "f22".to_string(), - Some(NSF23FunctionKey) => "f23".to_string(), - Some(NSF24FunctionKey) => "f24".to_string(), - Some(NSF25FunctionKey) => "f25".to_string(), - Some(NSF26FunctionKey) => "f26".to_string(), - Some(NSF27FunctionKey) => "f27".to_string(), - Some(NSF28FunctionKey) => "f28".to_string(), - Some(NSF29FunctionKey) => "f29".to_string(), - Some(NSF30FunctionKey) => "f30".to_string(), - Some(NSF31FunctionKey) => "f31".to_string(), - Some(NSF32FunctionKey) => "f32".to_string(), - Some(NSF33FunctionKey) => "f33".to_string(), - Some(NSF34FunctionKey) => "f34".to_string(), - Some(NSF35FunctionKey) => "f35".to_string(), - _ => { - // Cases to test when modifying this: - // - // qwerty key | none | cmd | cmd-shift - // * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout) - // * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd) - // * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S) - // * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers) - // * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/) - // * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout) - // * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z) - // - let mut chars_ignoring_modifiers = - chars_for_modified_key(native_event.keyCode(), NO_MOD); - let mut chars_with_shift = - chars_for_modified_key(native_event.keyCode(), SHIFT_MOD); - let always_use_cmd_layout = always_use_command_layout(); + #[allow(non_upper_case_globals)] + let key = match first_char { + Some(SPACE_KEY) => { + key_char = Some(" ".to_string()); + "space".to_string() + } + Some(TAB_KEY) => { + key_char = Some("\t".to_string()); + "tab".to_string() + } + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => { + key_char = Some("\n".to_string()); + "enter".to_string() + } + Some(BACKSPACE_KEY) => "backspace".to_string(), + Some(ESCAPE_KEY) => "escape".to_string(), + Some(SHIFT_TAB_KEY) => "tab".to_string(), + Some(NSUpArrowFunctionKey) => "up".to_string(), + Some(NSDownArrowFunctionKey) => "down".to_string(), + Some(NSLeftArrowFunctionKey) => "left".to_string(), + Some(NSRightArrowFunctionKey) => "right".to_string(), + Some(NSPageUpFunctionKey) => "pageup".to_string(), + Some(NSPageDownFunctionKey) => "pagedown".to_string(), + Some(NSHomeFunctionKey) => "home".to_string(), + Some(NSEndFunctionKey) => "end".to_string(), + Some(NSDeleteFunctionKey) => "delete".to_string(), + // Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey. + Some(NSHelpFunctionKey) => "insert".to_string(), + Some(NSF1FunctionKey) => "f1".to_string(), + Some(NSF2FunctionKey) => "f2".to_string(), + Some(NSF3FunctionKey) => "f3".to_string(), + Some(NSF4FunctionKey) => "f4".to_string(), + Some(NSF5FunctionKey) => "f5".to_string(), + Some(NSF6FunctionKey) => "f6".to_string(), + Some(NSF7FunctionKey) => "f7".to_string(), + Some(NSF8FunctionKey) => "f8".to_string(), + Some(NSF9FunctionKey) => "f9".to_string(), + Some(NSF10FunctionKey) => "f10".to_string(), + Some(NSF11FunctionKey) => "f11".to_string(), + Some(NSF12FunctionKey) => "f12".to_string(), + Some(NSF13FunctionKey) => "f13".to_string(), + Some(NSF14FunctionKey) => "f14".to_string(), + Some(NSF15FunctionKey) => "f15".to_string(), + Some(NSF16FunctionKey) => "f16".to_string(), + Some(NSF17FunctionKey) => "f17".to_string(), + Some(NSF18FunctionKey) => "f18".to_string(), + Some(NSF19FunctionKey) => "f19".to_string(), + Some(NSF20FunctionKey) => "f20".to_string(), + Some(NSF21FunctionKey) => "f21".to_string(), + Some(NSF22FunctionKey) => "f22".to_string(), + Some(NSF23FunctionKey) => "f23".to_string(), + Some(NSF24FunctionKey) => "f24".to_string(), + Some(NSF25FunctionKey) => "f25".to_string(), + Some(NSF26FunctionKey) => "f26".to_string(), + Some(NSF27FunctionKey) => "f27".to_string(), + Some(NSF28FunctionKey) => "f28".to_string(), + Some(NSF29FunctionKey) => "f29".to_string(), + Some(NSF30FunctionKey) => "f30".to_string(), + Some(NSF31FunctionKey) => "f31".to_string(), + Some(NSF32FunctionKey) => "f32".to_string(), + Some(NSF33FunctionKey) => "f33".to_string(), + Some(NSF34FunctionKey) => "f34".to_string(), + Some(NSF35FunctionKey) => "f35".to_string(), + _ => { + // Cases to test when modifying this: + // + // qwerty key | none | cmd | cmd-shift + // * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout) + // * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd) + // * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S) + // * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers) + // * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/) + // * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout) + // * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z) + // + let mut chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), NO_MOD); + let mut chars_with_shift = + chars_for_modified_key(native_event.keyCode(), SHIFT_MOD); + let always_use_cmd_layout = always_use_command_layout(); - // Handle Dvorak+QWERTY / Russian / Armenian - if command || always_use_cmd_layout { - let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD); - let chars_with_both = - chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD); + // Handle Dvorak+QWERTY / Russian / Armenian + if command || always_use_cmd_layout { + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD); + let chars_with_both = + chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD); - // We don't do this in the case that the shifted command key generates - // the same character as the unshifted command key (Norwegian, e.g.) - if chars_with_both != chars_with_cmd { - chars_with_shift = chars_with_both; + // We don't do this in the case that the shifted command key generates + // the same character as the unshifted command key (Norwegian, e.g.) + if chars_with_both != chars_with_cmd { + chars_with_shift = chars_with_both; - // Handle edge-case where cmd-shift-s reports cmd-s instead of - // cmd-shift-s (Ukrainian, etc.) - } else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd { - chars_with_shift = chars_with_cmd.to_ascii_uppercase(); - } - chars_ignoring_modifiers = chars_with_cmd; - } + // Handle edge-case where cmd-shift-s reports cmd-s instead of + // cmd-shift-s (Ukrainian, etc.) + } else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd { + chars_with_shift = chars_with_cmd.to_ascii_uppercase(); + } + chars_ignoring_modifiers = chars_with_cmd; + } - if !control && !command && !function { - let mut mods = NO_MOD; - if shift { - mods |= SHIFT_MOD; - } - if alt { - mods |= OPTION_MOD; - } + if !control && !command && !function { + let mut mods = NO_MOD; + if shift { + mods |= SHIFT_MOD; + } + if alt { + mods |= OPTION_MOD; + } - key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); - } + key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); + } - if shift - && chars_ignoring_modifiers - .chars() - .all(|c| c.is_ascii_lowercase()) - { - chars_ignoring_modifiers - } else if shift { - shift = false; - chars_with_shift - } else { - chars_ignoring_modifiers - } - } - }; + if shift + && chars_ignoring_modifiers + .chars() + .all(|c| c.is_ascii_lowercase()) + { + chars_ignoring_modifiers + } else if shift { + shift = false; + chars_with_shift + } else { + chars_ignoring_modifiers + } + } + }; - Keystroke { - modifiers: Modifiers { - control, - alt, - shift, - platform: command, - function, - }, - key, - key_char, - } - } + Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + platform: command, + function, + }, + key, + key_char, + } + } } fn always_use_command_layout() -> bool { - if chars_for_modified_key(0, NO_MOD).is_ascii() { - return false; - } + if chars_for_modified_key(0, NO_MOD).is_ascii() { + return false; + } - chars_for_modified_key(0, CMD_MOD).is_ascii() + chars_for_modified_key(0, CMD_MOD).is_ascii() } const NO_MOD: u32 = 0; @@ -510,65 +510,65 @@ const SHIFT_MOD: u32 = 2; const OPTION_MOD: u32 = 8; fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String { - // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126 - // shifted >> 8 for UCKeyTranslate - const CG_SPACE_KEY: u16 = 49; - // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278 - #[allow(non_upper_case_globals)] - const kUCKeyActionDown: u16 = 0; - #[allow(non_upper_case_globals)] - const kUCKeyTranslateNoDeadKeysMask: u32 = 0; + // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126 + // shifted >> 8 for UCKeyTranslate + const CG_SPACE_KEY: u16 = 49; + // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278 + #[allow(non_upper_case_globals)] + const kUCKeyActionDown: u16 = 0; + #[allow(non_upper_case_globals)] + const kUCKeyTranslateNoDeadKeysMask: u32 = 0; - let keyboard_type = unsafe { LMGetKbdType() as u32 }; - const BUFFER_SIZE: usize = 4; - let mut dead_key_state = 0; - let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; - let mut buffer_size: usize = 0; + let keyboard_type = unsafe { LMGetKbdType() as u32 }; + const BUFFER_SIZE: usize = 4; + let mut dead_key_state = 0; + let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let mut buffer_size: usize = 0; - let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() }; - if keyboard.is_null() { - return "".to_string(); - } - let layout_data = unsafe { - TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void) - as CFDataRef - }; - if layout_data.is_null() { - unsafe { - let _: () = msg_send![keyboard, release]; - } - return "".to_string(); - } - let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) }; + let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() }; + if keyboard.is_null() { + return "".to_string(); + } + let layout_data = unsafe { + TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void) + as CFDataRef + }; + if layout_data.is_null() { + unsafe { + let _: () = msg_send![keyboard, release]; + } + return "".to_string(); + } + let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) }; - unsafe { - UCKeyTranslate( - keyboard_layout as *const c_void, - code, - kUCKeyActionDown, - modifiers, - keyboard_type, - kUCKeyTranslateNoDeadKeysMask, - &mut dead_key_state, - BUFFER_SIZE, - &mut buffer_size as *mut usize, - &mut buffer as *mut u16, - ); - if dead_key_state != 0 { - UCKeyTranslate( - keyboard_layout as *const c_void, - CG_SPACE_KEY, - kUCKeyActionDown, - modifiers, - keyboard_type, - kUCKeyTranslateNoDeadKeysMask, - &mut dead_key_state, - BUFFER_SIZE, - &mut buffer_size as *mut usize, - &mut buffer as *mut u16, - ); - } - let _: () = msg_send![keyboard, release]; - } - String::from_utf16(&buffer[..buffer_size]).unwrap_or_default() + unsafe { + UCKeyTranslate( + keyboard_layout as *const c_void, + code, + kUCKeyActionDown, + modifiers, + keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &mut dead_key_state, + BUFFER_SIZE, + &mut buffer_size as *mut usize, + &mut buffer as *mut u16, + ); + if dead_key_state != 0 { + UCKeyTranslate( + keyboard_layout as *const c_void, + CG_SPACE_KEY, + kUCKeyActionDown, + modifiers, + keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &mut dead_key_state, + BUFFER_SIZE, + &mut buffer_size as *mut usize, + &mut buffer as *mut u16, + ); + } + let _: () = msg_send![keyboard, release]; + } + String::from_utf16(&buffer[..buffer_size]).unwrap_or_default() } diff --git a/crates/gpui_macos/src/gpui_macos.rs b/crates/gpui_macos/src/gpui_macos.rs index 63e542bb4d..ebe1fd3439 100644 --- a/crates/gpui_macos/src/gpui_macos.rs +++ b/crates/gpui_macos/src/gpui_macos.rs @@ -31,14 +31,14 @@ mod window; mod window_appearance; use cocoa::{ - base::{id, nil}, - foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger}, + base::{id, nil}, + foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger}, }; use objc::runtime::{BOOL, NO, YES}; use std::{ - ffi::{CStr, c_char}, - ops::Range, + ffi::{CStr, c_char}, + ops::Range, }; pub(crate) use dispatcher::*; @@ -54,84 +54,84 @@ pub(crate) use text_system::*; pub use platform::MacPlatform; trait BoolExt { - fn to_objc(self) -> BOOL; + fn to_objc(self) -> BOOL; } impl BoolExt for bool { - fn to_objc(self) -> BOOL { - if self { YES } else { NO } - } + fn to_objc(self) -> BOOL { + if self { YES } else { NO } + } } trait NSStringExt { - unsafe fn to_str(&self) -> &str; + unsafe fn to_str(&self) -> &str; } impl NSStringExt for id { - unsafe fn to_str(&self) -> &str { - unsafe { - let cstr = self.UTF8String(); - if cstr.is_null() { - "" - } else { - CStr::from_ptr(cstr as *mut c_char).to_str().unwrap() - } - } - } + unsafe fn to_str(&self) -> &str { + unsafe { + let cstr = self.UTF8String(); + if cstr.is_null() { + "" + } else { + CStr::from_ptr(cstr as *mut c_char).to_str().unwrap() + } + } + } } #[repr(C)] #[derive(Copy, Clone, Debug)] struct NSRange { - pub location: NSUInteger, - pub length: NSUInteger, + pub location: NSUInteger, + pub length: NSUInteger, } impl NSRange { - fn invalid() -> Self { - Self { - location: NSNotFound as NSUInteger, - length: 0, - } - } + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } - fn is_valid(&self) -> bool { - self.location != NSNotFound as NSUInteger - } + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } - fn to_range(self) -> Option> { - if self.is_valid() { - let start = self.location as usize; - let end = start + self.length as usize; - Some(start..end) - } else { - None - } - } + fn to_range(self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } } impl From> for NSRange { - fn from(range: Range) -> Self { - NSRange { - location: range.start as NSUInteger, - length: range.len() as NSUInteger, - } - } + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } } unsafe impl objc::Encode for NSRange { - fn encode() -> objc::Encoding { - let encoding = format!( - "{{NSRange={}{}}}", - NSUInteger::encode().as_str(), - NSUInteger::encode().as_str() - ); - unsafe { objc::Encoding::from_str(&encoding) } - } + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } } /// Allow NSString::alloc use here because it sets autorelease #[allow(clippy::disallowed_methods)] unsafe fn ns_string(string: &str) -> id { - unsafe { NSString::alloc(nil).init_str(string).autorelease() } + unsafe { NSString::alloc(nil).init_str(string).autorelease() } } diff --git a/crates/gpui_macos/src/haptic_feedback.rs b/crates/gpui_macos/src/haptic_feedback.rs index 21926218d7..f29f891cb6 100644 --- a/crates/gpui_macos/src/haptic_feedback.rs +++ b/crates/gpui_macos/src/haptic_feedback.rs @@ -7,88 +7,88 @@ use objc::{class, msg_send, sel, sel_impl}; /// Delivers transient taps through the Force Touch trackpad (macOS 10.11+). /// Fire and forget. On machines without haptic hardware, calls are silently ignored by AppKit. pub(crate) struct MacHaptics { - supported: bool, + supported: bool, } /// #[allow(dead_code)] mod feedback_pattern { - pub const GENERIC: isize = 0; - pub const ALIGNMENT: isize = 1; - pub const LEVEL_CHANGE: isize = 2; + pub const GENERIC: isize = 0; + pub const ALIGNMENT: isize = 1; + pub const LEVEL_CHANGE: isize = 2; } impl MacHaptics { - pub fn new(headless: bool) -> Self { - Self { - supported: !headless, - } - } + pub fn new(headless: bool) -> Self { + Self { + supported: !headless, + } + } - pub fn supported(&self) -> bool { - self.supported - } + pub fn supported(&self) -> bool { + self.supported + } - fn pattern_for_style(style: HapticFeedbackStyle) -> isize { - match style { - HapticFeedbackStyle::Generic => feedback_pattern::GENERIC, - HapticFeedbackStyle::Alignment => feedback_pattern::ALIGNMENT, - HapticFeedbackStyle::LevelChange => feedback_pattern::LEVEL_CHANGE, - } - } + fn pattern_for_style(style: HapticFeedbackStyle) -> isize { + match style { + HapticFeedbackStyle::Generic => feedback_pattern::GENERIC, + HapticFeedbackStyle::Alignment => feedback_pattern::ALIGNMENT, + HapticFeedbackStyle::LevelChange => feedback_pattern::LEVEL_CHANGE, + } + } - pub fn play(&self, style: HapticFeedbackStyle) { - if !self.supported { - return; - } + pub fn play(&self, style: HapticFeedbackStyle) { + if !self.supported { + return; + } - let pattern = Self::pattern_for_style(style); + let pattern = Self::pattern_for_style(style); - /// - const PERFORMANCE_TIME_NOW: usize = 1; + /// + const PERFORMANCE_TIME_NOW: usize = 1; - // Safety: NSHapticFeedbackManager is always available on macOS 10.11+. - // All Platform trait methods run on the main thread. - unsafe { - let manager: id = msg_send![class!(NSHapticFeedbackManager), defaultPerformer]; - let _: () = msg_send![ - manager, - performFeedbackPattern: pattern - performanceTime: PERFORMANCE_TIME_NOW - ]; - } - } + // Safety: NSHapticFeedbackManager is always available on macOS 10.11+. + // All Platform trait methods run on the main thread. + unsafe { + let manager: id = msg_send![class!(NSHapticFeedbackManager), defaultPerformer]; + let _: () = msg_send![ + manager, + performFeedbackPattern: pattern + performanceTime: PERFORMANCE_TIME_NOW + ]; + } + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_supported() { - let haptics = MacHaptics::new(false); - assert!(haptics.supported()); - } + #[test] + fn test_supported() { + let haptics = MacHaptics::new(false); + assert!(haptics.supported()); + } - #[test] - fn test_headless_is_unsupported() { - let haptics = MacHaptics::new(true); - assert!(!haptics.supported()); - } + #[test] + fn test_headless_is_unsupported() { + let haptics = MacHaptics::new(true); + assert!(!haptics.supported()); + } - #[test] - fn test_style_to_pattern_mapping() { - assert_eq!( - MacHaptics::pattern_for_style(HapticFeedbackStyle::Generic), - feedback_pattern::GENERIC - ); - assert_eq!( - MacHaptics::pattern_for_style(HapticFeedbackStyle::Alignment), - feedback_pattern::ALIGNMENT - ); - assert_eq!( - MacHaptics::pattern_for_style(HapticFeedbackStyle::LevelChange), - feedback_pattern::LEVEL_CHANGE - ); - } + #[test] + fn test_style_to_pattern_mapping() { + assert_eq!( + MacHaptics::pattern_for_style(HapticFeedbackStyle::Generic), + feedback_pattern::GENERIC + ); + assert_eq!( + MacHaptics::pattern_for_style(HapticFeedbackStyle::Alignment), + feedback_pattern::ALIGNMENT + ); + assert_eq!( + MacHaptics::pattern_for_style(HapticFeedbackStyle::LevelChange), + feedback_pattern::LEVEL_CHANGE + ); + } } diff --git a/crates/gpui_macos/src/keyboard.rs b/crates/gpui_macos/src/keyboard.rs index 63e56a260a..669d82730d 100644 --- a/crates/gpui_macos/src/keyboard.rs +++ b/crates/gpui_macos/src/keyboard.rs @@ -6,82 +6,82 @@ use objc::{msg_send, runtime::Object, sel, sel_impl}; use gpui::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; use crate::{ - TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, - kTISPropertyLocalizedName, + TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, + kTISPropertyLocalizedName, }; pub(crate) struct MacKeyboardLayout { - id: String, - name: String, + id: String, + name: String, } pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, + key_equivalents: Option>, } impl PlatformKeyboardLayout for MacKeyboardLayout { - fn id(&self) -> &str { - &self.id - } + fn id(&self) -> &str { + &self.id + } - fn name(&self) -> &str { - &self.name - } + fn name(&self) -> &str { + &self.name + } } impl PlatformKeyboardMapper for MacKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { - if keystroke.key.chars().count() == 1 - && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - KeybindingKeystroke::from_keystroke(keystroke) - } + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { + if keystroke.key.chars().count() == 1 + && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + KeybindingKeystroke::from_keystroke(keystroke) + } - fn get_key_equivalents(&self) -> Option<&HashMap> { - self.key_equivalents.as_ref() - } + fn get_key_equivalents(&self) -> Option<&HashMap> { + self.key_equivalents.as_ref() + } } impl MacKeyboardLayout { - pub(crate) fn new() -> Self { - unsafe { - let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource(); + pub(crate) fn new() -> Self { + unsafe { + let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource(); - let id: *mut Object = TISGetInputSourceProperty( - current_keyboard, - kTISPropertyInputSourceID as *const c_void, - ); - let id: *const std::os::raw::c_char = msg_send![id, UTF8String]; - let id = CStr::from_ptr(id).to_str().unwrap().to_string(); + let id: *mut Object = TISGetInputSourceProperty( + current_keyboard, + kTISPropertyInputSourceID as *const c_void, + ); + let id: *const std::os::raw::c_char = msg_send![id, UTF8String]; + let id = CStr::from_ptr(id).to_str().unwrap().to_string(); - let name: *mut Object = TISGetInputSourceProperty( - current_keyboard, - kTISPropertyLocalizedName as *const c_void, - ); - let name: *const std::os::raw::c_char = msg_send![name, UTF8String]; - let name = CStr::from_ptr(name).to_str().unwrap().to_string(); + let name: *mut Object = TISGetInputSourceProperty( + current_keyboard, + kTISPropertyLocalizedName as *const c_void, + ); + let name: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let name = CStr::from_ptr(name).to_str().unwrap().to_string(); - let _: () = msg_send![current_keyboard, release]; + let _: () = msg_send![current_keyboard, release]; - Self { id, name } - } - } + Self { id, name } + } + } } impl MacKeyboardMapper { - pub(crate) fn new(layout_id: &str) -> Self { - let key_equivalents = get_key_equivalents(layout_id); + pub(crate) fn new(layout_id: &str) -> Self { + let key_equivalents = get_key_equivalents(layout_id); - Self { key_equivalents } - } + Self { key_equivalents } + } } // On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range @@ -109,1394 +109,1394 @@ impl MacKeyboardMapper { // jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' // From there I used multi-cursor to produce this match statement. fn get_key_equivalents(layout_id: &str) -> Option> { - let mappings: &[(char, char)] = match layout_id { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], + let mappings: &[(char, char)] = match layout_id { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], - _ => return None, - }; + _ => return None, + }; - Some(HashMap::from_iter(mappings.iter().cloned())) + Some(HashMap::from_iter(mappings.iter().cloned())) } diff --git a/crates/gpui_macos/src/metal_atlas.rs b/crates/gpui_macos/src/metal_atlas.rs index 5bedf9df8c..6fa8a8feaf 100644 --- a/crates/gpui_macos/src/metal_atlas.rs +++ b/crates/gpui_macos/src/metal_atlas.rs @@ -3,8 +3,8 @@ use collections::FxHashMap; use derive_more::{Deref, DerefMut}; use etagere::BucketedAtlasAllocator; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, + PlatformAtlas, Point, Size, }; use metal::Device; use parking_lot::Mutex; @@ -13,256 +13,256 @@ use std::borrow::Cow; pub(crate) 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 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() - } + 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, + 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 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(id) = lock.tiles_by_key.remove(key).map(|v| v.texture_id) else { - return; - }; + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + let Some(id) = lock.tiles_by_key.remove(key).map(|v| v.texture_id) else { + return; + }; - let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Subpixel => unreachable!(), - }; + 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; - }; + 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.decrement_ref_count(); - if texture.is_unreferenced() { - textures.free_list.push(id.index as usize); - } else { - *texture_slot = Some(texture); - } - } - } + if let Some(mut texture) = texture_slot.take() { + 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!(), - }; + 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); - } - } + 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) - } + 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); + 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 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 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, - }; + 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() - } + 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() - } + 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, + 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 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 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 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 decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } - fn is_unreferenced(&mut self) -> bool { - self.live_atlas_keys == 0 - } + 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()) + 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), - } + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } } #[derive(Deref, DerefMut)] @@ -272,78 +272,78 @@ unsafe impl Send for AssertSend {} #[cfg(test)] mod tests { - use super::*; - use gpui::PlatformAtlas; - use std::borrow::Cow; + 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 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 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") - } + 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; - }; + #[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 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 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); + 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); + 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 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 B: texture still has C. + atlas.remove(&key_b); - // Remove C: texture becomes unreferenced and is deleted. - atlas.remove(&key_c); + // 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); + // 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); - } + // The texture must actually exist — this would panic before the fix. + let _texture = atlas.metal_texture(tile_a2.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); - } + #[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_macos/src/metal_renderer.rs b/crates/gpui_macos/src/metal_renderer.rs index ae108fbb76..249b33b88c 100644 --- a/crates/gpui_macos/src/metal_renderer.rs +++ b/crates/gpui_macos/src/metal_renderer.rs @@ -2,14 +2,14 @@ use crate::metal_atlas::MetalAtlas; use anyhow::Result; use block::ConcreteBlock; use cocoa::{ - base::{NO, YES}, - foundation::{NSSize, NSUInteger}, - quartzcore::AutoresizingMask, + base::{NO, YES}, + foundation::{NSSize, NSUInteger}, + quartzcore::AutoresizingMask, }; use gpui::{ - AtlasTextureId, Background, Bounds, ContentMask, Corners, DevicePixels, FilterBoundary, - MonochromeSprite, PaintSurface, Path, Point, PolychromeSprite, PrimitiveBatch, Quad, - ScaledFilter, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size, + AtlasTextureId, Background, Bounds, ContentMask, Corners, DevicePixels, FilterBoundary, + MonochromeSprite, PaintSurface, Path, Point, PolychromeSprite, PrimitiveBatch, Quad, + ScaledFilter, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size, }; /// The largest blur radius in a scene-space filter chain, in device pixels — used to size the @@ -19,24 +19,23 @@ use gpui::{ /// forcing this backend to handle (or deliberately ignore) the new filter rather than silently /// dropping it. fn max_blur_radius(filters: &[ScaledFilter]) -> f32 { - filters.iter().fold(0.0, |acc, filter| match filter { - ScaledFilter::Blur(radius) => acc.max(radius.0), - }) + filters.iter().fold(0.0, |acc, filter| match filter { + ScaledFilter::Blur(radius) => acc.max(radius.0), + }) } #[cfg(any(test, feature = "test-support"))] use image::RgbaImage; use core_foundation::base::TCFType; use core_video::{ - metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache, - pixel_buffer::{ - kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, - }, + metal_texture::CVMetalTextureGetTexture, + metal_texture_cache::CVMetalTextureCache, + pixel_buffer::{kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange}, }; use foreign_types::{ForeignType, ForeignTypeRef}; use metal::{ - CAMetalLayer, CommandQueue, MTLGPUFamily, MTLPixelFormat, MTLResourceOptions, NSRange, - RenderPassColorAttachmentDescriptorRef, + CAMetalLayer, CommandQueue, MTLGPUFamily, MTLPixelFormat, MTLResourceOptions, NSRange, + RenderPassColorAttachmentDescriptorRef, }; use objc::{self, msg_send, sel, sel_impl}; use parking_lot::Mutex; @@ -64,111 +63,111 @@ pub(crate) type Context = Arc>; pub(crate) type Renderer = MetalRenderer; pub(crate) unsafe fn new_renderer( - context: self::Context, - _native_window: *mut c_void, - _native_view: *mut c_void, - _bounds: gpui::Size, - transparent: bool, + context: self::Context, + _native_window: *mut c_void, + _native_view: *mut c_void, + _bounds: gpui::Size, + transparent: bool, ) -> Renderer { - MetalRenderer::new(context, transparent) + MetalRenderer::new(context, transparent) } pub(crate) struct InstanceBufferPool { - buffer_size: usize, - buffers: Vec, + buffer_size: usize, + buffers: Vec, } impl Default for InstanceBufferPool { - fn default() -> Self { - Self { - buffer_size: 2 * 1024 * 1024, - buffers: Vec::new(), - } - } + fn default() -> Self { + Self { + buffer_size: 2 * 1024 * 1024, + buffers: Vec::new(), + } + } } pub(crate) struct InstanceBuffer { - metal_buffer: metal::Buffer, - size: usize, + 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 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 + 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 - }; + } else { + MTLResourceOptions::StorageModeManaged + }; - device.new_buffer(self.buffer_size as u64, options) - }); - InstanceBuffer { - metal_buffer: buffer, - size: self.buffer_size, - } - } + 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(crate) fn release(&mut self, buffer: InstanceBuffer) { + if buffer.size == self.buffer_size { + self.buffers.push(buffer.metal_buffer) + } + } } pub(crate) 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, - // Blur pipelines: downsample (no blend, also used for the final blit), separable gaussian - // (no blend), and composite (alpha blend into a rounded rect). See `shaders.metal`. - blur_downsample_pipeline_state: metal::RenderPipelineState, - blur_pipeline_state: metal::RenderPipelineState, - blur_composite_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, - // Offscreen scene target (the scene is rendered here, then blitted to the drawable, so blur - // passes can sample already-painted content), the half-res ping/pong blur targets, and a - // full-res target for content-filter groups. - scene_color_texture: Option, - blur_ping_texture: Option, - blur_pong_texture: Option, - /// Full-resolution offscreen targets a content-filter (`filter`) group renders into before - /// being blurred and composited back. One per nesting level (indexed by isolation depth) so - /// nested content blurs isolate correctly, up to [`MAX_FILTER_DEPTH`]; deeper nests render - /// inline. - group_textures: Vec, - path_sample_count: u32, + 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, + // Blur pipelines: downsample (no blend, also used for the final blit), separable gaussian + // (no blend), and composite (alpha blend into a rounded rect). See `shaders.metal`. + blur_downsample_pipeline_state: metal::RenderPipelineState, + blur_pipeline_state: metal::RenderPipelineState, + blur_composite_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, + // Offscreen scene target (the scene is rendered here, then blitted to the drawable, so blur + // passes can sample already-painted content), the half-res ping/pong blur targets, and a + // full-res target for content-filter groups. + scene_color_texture: Option, + blur_ping_texture: Option, + blur_pong_texture: Option, + /// Full-resolution offscreen targets a content-filter (`filter`) group renders into before + /// being blurred and composited back. One per nesting level (indexed by isolation depth) so + /// nested content blurs isolate correctly, up to [`MAX_FILTER_DEPTH`]; deeper nests render + /// inline. + group_textures: Vec, + path_sample_count: u32, } /// Mirrors the `BlurParams` struct in `shaders.metal`. Passed to the blur pipelines via @@ -176,2019 +175,2018 @@ pub(crate) struct MetalRenderer { #[repr(C)] #[derive(Clone, Copy)] struct BlurUniform { - bounds: Bounds, - content_mask: Bounds, - corner_radii: Corners, - direction: [f32; 2], - sigma: f32, - opacity: f32, - tap_count: f32, - /// 1.0 clips the composite to the rounded rect (backdrop); 0.0 lets content blur bleed past - /// its bounds like CSS `filter: blur`. - clip_rounded: f32, - /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the - /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy - /// (the scene blit, which must not downsample). Downsample pass only. - downsample: f32, - /// Spacing between taps in pixels (gaussian passes only); >1 lets `tap_count` taps span very - /// large radii without truncating the gaussian, matching the wgpu backend. - tap_step: f32, + bounds: Bounds, + content_mask: Bounds, + corner_radii: Corners, + direction: [f32; 2], + sigma: f32, + opacity: f32, + tap_count: f32, + /// 1.0 clips the composite to the rounded rect (backdrop); 0.0 lets content blur bleed past + /// its bounds like CSS `filter: blur`. + clip_rounded: f32, + /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the + /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy + /// (the scene blit, which must not downsample). Downsample pass only. + downsample: f32, + /// Spacing between taps in pixels (gaussian passes only); >1 lets `tap_count` taps span very + /// large radii without truncating the gaussian, matching the wgpu backend. + tap_step: f32, } impl Default for BlurUniform { - fn default() -> Self { - BlurUniform { - bounds: Bounds::default(), - content_mask: Bounds::default(), - corner_radii: Corners::default(), - direction: [0.0, 0.0], - sigma: 0.0, - opacity: 1.0, - tap_count: 0.0, - clip_rounded: 0.0, - downsample: 0.0, - tap_step: 0.0, - } - } + fn default() -> Self { + BlurUniform { + bounds: Bounds::default(), + content_mask: Bounds::default(), + corner_radii: Corners::default(), + direction: [0.0, 0.0], + sigma: 0.0, + opacity: 1.0, + tap_count: 0.0, + clip_rounded: 0.0, + downsample: 0.0, + tap_step: 0.0, + } + } } #[repr(C)] enum BlurInputIndex { - Vertices = 0, - Params = 1, - ViewportSize = 2, + Vertices = 0, + Params = 1, + ViewportSize = 2, } #[repr(C)] pub struct PathRasterizationVertex { - pub xy_position: Point, - pub st_position: Point, - pub color: Background, - pub bounds: Bounds, + pub xy_position: Point, + pub st_position: Point, + pub color: Background, + pub bounds: Bounds, } 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 = "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 blur_downsample_pipeline_state = build_blur_pipeline_state( - &device, - &library, - "blur_downsample", - "blur_fullscreen_vertex", - "blur_downsample_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let blur_pipeline_state = build_blur_pipeline_state( - &device, - &library, - "blur", - "blur_fullscreen_vertex", - "blur_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - // Premultiplied blend (One / OneMinusSourceAlpha) — the composite outputs a premultiplied - // blurred sample; straight-alpha blending would darken the faded edges. - let blur_composite_pipeline_state = build_path_sprite_pipeline_state( - &device, - &library, - "blur_composite", - "blur_composite_vertex", - "blur_composite_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, - blur_downsample_pipeline_state, - blur_pipeline_state, - blur_composite_pipeline_state, - unit_vertices, - instance_buffer_pool, - sprite_atlas, - core_video_texture_cache, - path_intermediate_texture: None, - path_intermediate_msaa_texture: None, - scene_color_texture: None, - blur_ping_texture: None, - blur_pong_texture: None, - group_textures: Vec::new(), - path_sample_count: PATH_SAMPLE_COUNT, - } - } - - 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; - self.scene_color_texture = None; - self.blur_ping_texture = None; - self.blur_pong_texture = None; - self.group_textures.clear(); - 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)); - - // Full-res scene + group targets, and half-res ping/pong blur targets. - let make_color_texture = |width: u64, height: u64| { - let descriptor = metal::TextureDescriptor::new(); - descriptor.set_width(width.max(1)); - descriptor.set_height(height.max(1)); - descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); - descriptor.set_storage_mode(metal::MTLStorageMode::Private); - descriptor.set_usage( - metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, - ); - self.device.new_texture(&descriptor) - }; - let full_w = size.width.0 as u64; - let full_h = size.height.0 as u64; - self.scene_color_texture = Some(make_color_texture(full_w, full_h)); - self.group_textures = (0..MAX_FILTER_DEPTH) - .map(|_| make_color_texture(full_w, full_h)) - .collect(); - self.blur_ping_texture = Some(make_color_texture(full_w / 2, full_h / 2)); - self.blur_pong_texture = Some(make_color_texture(full_w / 2, full_h / 2)); - - 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; - }; - - loop { - let mut instance_buffer = self - .instance_buffer_pool - .lock() - .acquire(&self.device, self.is_unified_memory); - - let command_buffer = - self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size); - - match command_buffer { - Ok(command_buffer) => { - let instance_buffer_pool = self.instance_buffer_pool.clone(); - let instance_buffer = Cell::new(Some(instance_buffer)); - 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); - - if self.presents_with_transaction { - command_buffer.commit(); - command_buffer.wait_until_scheduled(); - drawable.present(); - } else { - command_buffer.present_drawable(drawable); - command_buffer.commit(); - } - return; - } - Err(err) => { - log::error!( - "failed to render: {}. retrying with larger instance buffer size", - err - ); - let mut instance_buffer_pool = self.instance_buffer_pool.lock(); - let buffer_size = instance_buffer_pool.buffer_size; - if buffer_size >= 256 * 1024 * 1024 { - log::error!("instance buffer size grew too large: {}", buffer_size); - break; - } - instance_buffer_pool.reset(buffer_size * 2); - log::info!( - "increased instance buffer size to {}", - instance_buffer_pool.buffer_size - ); - } - } - } - } - - /// 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"))?; - - loop { - let mut instance_buffer = self - .instance_buffer_pool - .lock() - .acquire(&self.device, self.is_unified_memory); - - let command_buffer = - self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size); - - match command_buffer { - Ok(command_buffer) => { - let instance_buffer_pool = self.instance_buffer_pool.clone(); - let instance_buffer = Cell::new(Some(instance_buffer)); - 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); - - // Commit and wait for completion without presenting - command_buffer.commit(); - command_buffer.wait_until_completed(); - - // Read pixels from the texture - let texture = drawable.texture(); - let width = texture.width() as u32; - let height = texture.height() as u32; - let bytes_per_row = width as usize * 4; - let buffer_size = height as usize * bytes_per_row; - - let mut pixels = vec![0u8; buffer_size]; - - 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); - } - - return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| { - anyhow::anyhow!("Failed to create RgbaImage from pixel data") - }); - } - Err(err) => { - log::error!( - "failed to render: {}. retrying with larger instance buffer size", - err - ); - let mut instance_buffer_pool = self.instance_buffer_pool.lock(); - let buffer_size = instance_buffer_pool.buffer_size; - if buffer_size >= 256 * 1024 * 1024 { - anyhow::bail!("instance buffer size grew too large: {}", buffer_size); - } - instance_buffer_pool.reset(buffer_size * 2); - log::info!( - "increased instance buffer size to {}", - instance_buffer_pool.buffer_size - ); - } - } - } - } - - /// 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 = "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); - - loop { - let mut instance_buffer = self - .instance_buffer_pool - .lock() - .acquire(&self.device, self.is_unified_memory); - - let command_buffer = - self.draw_primitives_to_texture(scene, &mut instance_buffer, &target_texture, size); - - match command_buffer { - Ok(command_buffer) => { - let instance_buffer_pool = self.instance_buffer_pool.clone(); - let instance_buffer = Cell::new(Some(instance_buffer)); - 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); - - // 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 pixels from the texture - let width = size.width.0 as u32; - let height = size.height.0 as u32; - let bytes_per_row = width as usize * 4; - let buffer_size = height as usize * bytes_per_row; - - let mut pixels = vec![0u8; buffer_size]; - - 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, - }, - }; - - target_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); - } - - return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| { - anyhow::anyhow!("Failed to create RgbaImage from pixel data") - }); - } - Err(err) => { - log::error!( - "failed to render: {}. retrying with larger instance buffer size", - err - ); - let mut instance_buffer_pool = self.instance_buffer_pool.lock(); - let buffer_size = instance_buffer_pool.buffer_size; - if buffer_size >= 256 * 1024 * 1024 { - anyhow::bail!("instance buffer size grew too large: {}", buffer_size); - } - instance_buffer_pool.reset(buffer_size * 2); - log::info!( - "increased instance buffer size to {}", - instance_buffer_pool.buffer_size - ); - } - } - } - } - - fn draw_primitives( - &mut self, - scene: &Scene, - instance_buffer: &mut InstanceBuffer, - drawable: &metal::MetalDrawableRef, - viewport_size: Size, - ) -> Result { - self.draw_primitives_to_texture(scene, instance_buffer, drawable.texture(), viewport_size) - } - - fn draw_primitives_to_texture( - &mut self, - scene: &Scene, - instance_buffer: &mut InstanceBuffer, - 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 instance_offset = 0; - - // Render the scene into an offscreen color texture (so filters can sample it), then - // blit it to `texture`. Owned clones keep the textures borrowable without borrowing - // `self` across the batch loop (which calls `&mut self` methods like `draw_surfaces`). - // Only route through the offscreen scene texture when the scene actually contains blur - // filters; otherwise render straight to `texture` exactly as before (no regression, no - // extra blit for the common case). - let use_offscreen = - !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); - let scene_color_owned = self.scene_color_texture.clone(); - let blur_ping_owned = self.blur_ping_texture.clone(); - let blur_pong_owned = self.blur_pong_texture.clone(); - let group_owned = self.group_textures.clone(); - let scene_color: &metal::TextureRef = if use_offscreen { - scene_color_owned.as_deref().unwrap_or(texture) - } else { - texture - }; - // The active render target; switches to the group texture inside a content-filter group. - let mut current_target: &metal::TextureRef = scene_color; - // (boundary, parent target to composite back into, whether this level is isolated). - let mut filter_stack: Vec<(FilterBoundary, &metal::TextureRef, bool)> = Vec::new(); - - let mut command_encoder = new_command_encoder_for_texture( - command_buffer, - current_target, - viewport_size, - |color_attachment| { - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); - }, - ); - - for batch in scene.batches() { - let ok = match batch { - PrimitiveBatch::Shadows(range) => self.draw_shadows( - &scene.shadows[range], - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ), - PrimitiveBatch::Quads(range) => self.draw_quads( - &scene.quads[range], - instance_buffer, - &mut instance_offset, - 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, - instance_buffer, - &mut instance_offset, - viewport_size, - command_buffer, - ); - - command_encoder = new_command_encoder_for_texture( - command_buffer, - current_target, - viewport_size, - |color_attachment| { - color_attachment.set_load_action(metal::MTLLoadAction::Load); - }, - ); - - if did_draw { - self.draw_paths_from_intermediate( - paths, - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ) - } else { - false - } - } - PrimitiveBatch::Underlines(range) => self.draw_underlines( - &scene.underlines[range], - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ), - PrimitiveBatch::MonochromeSprites { texture_id, range } => self - .draw_monochrome_sprites( - texture_id, - &scene.monochrome_sprites[range], - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ), - PrimitiveBatch::PolychromeSprites { texture_id, range } => self - .draw_polychrome_sprites( - texture_id, - &scene.polychrome_sprites[range], - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ), - PrimitiveBatch::Surfaces(range) => self.draw_surfaces( - &scene.surfaces[range], - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, - ), - PrimitiveBatch::BackdropFilters(range) => { - command_encoder.end_encoding(); - if let (Some(ping), Some(pong)) = - (blur_ping_owned.as_deref(), blur_pong_owned.as_deref()) - { - for filter in &scene.backdrop_filters[range] { - self.metal_blur_and_composite( - command_buffer, - current_target, - current_target, - ping, - pong, - viewport_size, - filter.bounds, - filter.content_mask.bounds, - filter.corner_radii, - max_blur_radius(&filter.filters), - filter.opacity, - true, - ); - } - } - command_encoder = new_command_encoder_for_texture( - command_buffer, - current_target, - viewport_size, - |color_attachment| { - color_attachment.set_load_action(metal::MTLLoadAction::Load); - }, - ); - true - } - PrimitiveBatch::FilterBoundary(ix) => { - let boundary = scene.filter_boundaries[ix].clone(); - if boundary.is_start { - // Each isolated nesting level uses its own group texture from the pool - // (indexed by current isolation depth). Beyond the pool size - // (MAX_FILTER_DEPTH) deeper filters render inline without isolation rather - // than corrupting an outer group. - let depth = filter_stack.iter().filter(|entry| entry.2).count(); - if depth < group_owned.len() { - command_encoder.end_encoding(); - let parent = current_target; - current_target = group_owned[depth].as_ref(); - filter_stack.push((boundary, parent, true)); - command_encoder = new_command_encoder_for_texture( - command_buffer, - current_target, - viewport_size, - |color_attachment| { - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment - .set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); - }, - ); - } else { - filter_stack.push((boundary, current_target, false)); - } - } else if let Some((boundary, parent, isolated)) = filter_stack.pop() { - if isolated { - command_encoder.end_encoding(); - if let (Some(ping), Some(pong)) = - (blur_ping_owned.as_deref(), blur_pong_owned.as_deref()) - { - self.metal_blur_and_composite( - command_buffer, - current_target, - parent, - ping, - pong, - viewport_size, - boundary.bounds, - boundary.content_mask.bounds, - boundary.corner_radii, - max_blur_radius(&boundary.filters), - boundary.opacity, - false, - ); - } - current_target = parent; - command_encoder = new_command_encoder_for_texture( - command_buffer, - current_target, - viewport_size, - |color_attachment| { - color_attachment.set_load_action(metal::MTLLoadAction::Load); - }, - ); - } - } - true - } - PrimitiveBatch::SubpixelSprites { .. } => unreachable!(), - }; - if !ok { - command_encoder.end_encoding(); - anyhow::bail!( - "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(), - ); - } - } - - command_encoder.end_encoding(); - - // Present the offscreen scene by copying it into the drawable/target texture. - if use_offscreen && scene_color_owned.is_some() { - self.run_metal_blur_pass( - command_buffer, - &self.blur_downsample_pipeline_state, - texture, - scene_color, - viewport_size, - BlurUniform::default(), - false, - ); - } - - if !self.is_unified_memory { - // Sync the instance buffer to the GPU - instance_buffer.metal_buffer.did_modify_range(NSRange { - location: 0, - length: instance_offset as NSUInteger, - }); - } - - Ok(command_buffer.to_owned()) - } - - /// Run a single blur pass: draw a full-screen (or composite) quad sampling `source` into - /// `target`. `params` is supplied to both shader stages; `load` keeps existing target - /// contents (used by the composite), otherwise the target is cleared. - #[allow(clippy::too_many_arguments)] - fn run_metal_blur_pass( - &self, - command_buffer: &metal::CommandBufferRef, - pipeline: &metal::RenderPipelineState, - target: &metal::TextureRef, - source: &metal::TextureRef, - target_viewport: Size, - params: BlurUniform, - load: bool, - ) { - let encoder = new_command_encoder_for_texture( - command_buffer, - target, - target_viewport, - |color_attachment| { - if load { - color_attachment.set_load_action(metal::MTLLoadAction::Load); - } else { - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); - } - }, - ); - encoder.set_render_pipeline_state(pipeline); - encoder.set_vertex_buffer( - BlurInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - encoder.set_vertex_bytes( - BlurInputIndex::Params as u64, - mem::size_of::() as u64, - ¶ms as *const BlurUniform as *const _, - ); - encoder.set_vertex_bytes( - BlurInputIndex::ViewportSize as u64, - mem::size_of_val(&target_viewport) as u64, - &target_viewport as *const Size as *const _, - ); - encoder.set_fragment_bytes( - BlurInputIndex::Params as u64, - mem::size_of::() as u64, - ¶ms as *const BlurUniform as *const _, - ); - encoder.set_fragment_bytes( - BlurInputIndex::ViewportSize as u64, - mem::size_of_val(&target_viewport) as u64, - &target_viewport as *const Size as *const _, - ); - encoder.set_fragment_texture(0, Some(source)); - encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); - encoder.end_encoding(); - } - - /// Blur `source` (full-resolution) using the half-res ping/pong textures and composite the - /// result into `target`, clipped to `bounds`/`corner_radii`/`content_mask` and modulated by - /// `opacity`. Shared by the backdrop and content-filter paths. - #[allow(clippy::too_many_arguments)] - fn metal_blur_and_composite( - &self, - command_buffer: &metal::CommandBufferRef, - source: &metal::TextureRef, - target: &metal::TextureRef, - ping: &metal::TextureRef, - pong: &metal::TextureRef, - viewport_size: Size, - bounds: Bounds, - content_mask: Bounds, - corner_radii: Corners, - blur_radius: f32, - opacity: f32, - // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. - clip_rounded: bool, - ) { - // Sigma is halved because the blur runs at half resolution. - let sigma = (blur_radius * 0.5).max(0.0); - if sigma <= 0.0 { - return; - } - // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather - // than truncating the kernel — keeps very large radii from clipping. Matches wgpu. - let ideal_taps = (3.0 * sigma).ceil(); - let tap_count = ideal_taps.clamp(1.0, 32.0); - let tap_step = (ideal_taps / tap_count).max(1.0); - // Content blur bleeds ~3·radius past the box, so its composite quad covers a dilated rect. - let composite_bounds = if clip_rounded { - bounds - } else { - bounds.dilate(ScaledPixels(3.0 * blur_radius)) - }; - let half = Size { - width: DevicePixels((i32::from(viewport_size.width) / 2).max(1)), - height: DevicePixels((i32::from(viewport_size.height) / 2).max(1)), - }; - let half_w = i32::from(half.width) as f32; - let half_h = i32::from(half.height) as f32; - - // Downsample source -> ping, then separable gaussian ping -> pong -> ping. - self.run_metal_blur_pass( - command_buffer, - &self.blur_downsample_pipeline_state, - ping, - source, - half, - BlurUniform { - downsample: 1.0, - ..Default::default() - }, - false, - ); - self.run_metal_blur_pass( - command_buffer, - &self.blur_pipeline_state, - pong, - ping, - half, - BlurUniform { - direction: [1.0 / half_w, 0.0], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - false, - ); - self.run_metal_blur_pass( - command_buffer, - &self.blur_pipeline_state, - ping, - pong, - half, - BlurUniform { - direction: [0.0, 1.0 / half_h], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - false, - ); - - // Composite the blurred result into the target (preserving its contents). - self.run_metal_blur_pass( - command_buffer, - &self.blur_composite_pipeline_state, - target, - ping, - viewport_size, - BlurUniform { - bounds: composite_bounds, - content_mask, - corner_radii, - opacity, - clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, - ..Default::default() - }, - true, - ); - } - - fn draw_paths_to_intermediate( - &self, - paths: &[Path], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_buffer: &metal::CommandBufferRef, - ) -> bool { - if paths.is_empty() { - return true; - } - let Some(intermediate_texture) = &self.path_intermediate_texture else { - return false; - }; - - 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_render_pipeline_state(&self.paths_rasterization_pipeline_state); - - align_offset(instance_offset); - 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), - })); - } - let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); - let next_offset = *instance_offset + vertices_bytes_len; - if next_offset > instance_buffer.size { - command_encoder.end_encoding(); - return false; - } - command_encoder.set_vertex_buffer( - PathRasterizationInputIndex::Vertices as u64, - Some(&instance_buffer.metal_buffer), - *instance_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(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - unsafe { - ptr::copy_nonoverlapping( - vertices.as_ptr() as *const u8, - buffer_contents, - vertices_bytes_len, - ); - } - command_encoder.draw_primitives( - metal::MTLPrimitiveType::Triangle, - 0, - vertices.len() as u64, - ); - *instance_offset = next_offset; - - command_encoder.end_encoding(); - true - } - - fn draw_shadows( - &self, - shadows: &[Shadow], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - if shadows.is_empty() { - return true; - } - align_offset(instance_offset); - - 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_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_fragment_buffer( - ShadowInputIndex::Shadows as u64, - Some(&instance_buffer.metal_buffer), - *instance_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 _, - ); - - let shadow_bytes_len = mem::size_of_val(shadows); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - - let next_offset = *instance_offset + shadow_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - unsafe { - ptr::copy_nonoverlapping( - shadows.as_ptr() as *const u8, - buffer_contents, - shadow_bytes_len, - ); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - shadows.len() as u64, - ); - *instance_offset = next_offset; - true - } - - fn draw_quads( - &self, - quads: &[Quad], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - if quads.is_empty() { - return true; - } - align_offset(instance_offset); - - 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_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_fragment_buffer( - QuadInputIndex::Quads as u64, - Some(&instance_buffer.metal_buffer), - *instance_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 _, - ); - - let quad_bytes_len = mem::size_of_val(quads); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - - let next_offset = *instance_offset + quad_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - unsafe { - ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - quads.len() as u64, - ); - *instance_offset = next_offset; - true - } - - fn draw_paths_from_intermediate( - &self, - paths: &[Path], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - let Some(first_path) = paths.first() else { - return true; - }; - - let Some(ref intermediate_texture) = self.path_intermediate_texture else { - return false; - }; - - 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 }]; - } - - align_offset(instance_offset); - let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); - let next_offset = *instance_offset + sprite_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - command_encoder.set_vertex_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - unsafe { - ptr::copy_nonoverlapping( - sprites.as_ptr() as *const u8, - buffer_contents, - sprite_bytes_len, - ); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - ); - *instance_offset = next_offset; - - true - } - - fn draw_underlines( - &self, - underlines: &[Underline], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - if underlines.is_empty() { - return true; - } - align_offset(instance_offset); - - 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_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_fragment_buffer( - UnderlineInputIndex::Underlines as u64, - Some(&instance_buffer.metal_buffer), - *instance_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 _, - ); - - let underline_bytes_len = mem::size_of_val(underlines); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - - let next_offset = *instance_offset + underline_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - unsafe { - ptr::copy_nonoverlapping( - underlines.as_ptr() as *const u8, - buffer_contents, - underline_bytes_len, - ); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - underlines.len() as u64, - ); - *instance_offset = next_offset; - true - } - - fn draw_monochrome_sprites( - &self, - texture_id: AtlasTextureId, - sprites: &[MonochromeSprite], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - if sprites.is_empty() { - return true; - } - align_offset(instance_offset); - - let sprite_bytes_len = mem::size_of_val(sprites); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - - let next_offset = *instance_offset + sprite_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - 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_buffer.metal_buffer), - *instance_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_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - - unsafe { - ptr::copy_nonoverlapping( - sprites.as_ptr() as *const u8, - buffer_contents, - sprite_bytes_len, - ); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - ); - *instance_offset = next_offset; - true - } - - fn draw_polychrome_sprites( - &self, - texture_id: AtlasTextureId, - sprites: &[PolychromeSprite], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - if sprites.is_empty() { - return true; - } - align_offset(instance_offset); - - 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_buffer.metal_buffer), - *instance_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_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - - let sprite_bytes_len = mem::size_of_val(sprites); - let buffer_contents = - unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; - - let next_offset = *instance_offset + sprite_bytes_len; - if next_offset > instance_buffer.size { - return false; - } - - unsafe { - ptr::copy_nonoverlapping( - sprites.as_ptr() as *const u8, - buffer_contents, - sprite_bytes_len, - ); - } - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - ); - *instance_offset = next_offset; - true - } - - fn draw_surfaces( - &mut self, - surfaces: &[PaintSurface], - instance_buffer: &mut InstanceBuffer, - instance_offset: &mut usize, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> bool { - 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_bytes( - SurfaceInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - for surface in surfaces { - let texture_size = size( - DevicePixels::from(surface.image_buffer.get_width() as i32), - DevicePixels::from(surface.image_buffer.get_height() as i32), - ); - - let pixel_format = surface.image_buffer.get_pixel_format(); - let is_bgra = if pixel_format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange { - false - } else if pixel_format == kCVPixelFormatType_32BGRA { - true - } else { - log::error!("unsupported surface pixel format: {pixel_format}"); - return false; - }; - - // Y (or the whole BGRA image) texture from plane 0. - let y_texture = self - .core_video_texture_cache - .create_texture_from_image( - surface.image_buffer.as_concrete_TypeRef(), - None, - if is_bgra { - MTLPixelFormat::BGRA8Unorm - } else { - MTLPixelFormat::R8Unorm - }, - surface.image_buffer.get_width_of_plane(0), - surface.image_buffer.get_height_of_plane(0), - 0, - ) - .unwrap(); - - let cb_cr_texture = if is_bgra { - None - } else { - Some( - 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(), - ) - }; - - align_offset(instance_offset); - let next_offset = *instance_offset + mem::size_of::(); - if next_offset > instance_buffer.size { - return false; - } - - command_encoder.set_vertex_buffer( - SurfaceInputIndex::Surfaces as u64, - Some(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_vertex_bytes( - SurfaceInputIndex::TextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); - 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, - cb_cr_texture.as_ref().map(|texture| unsafe { - let texture = CVMetalTextureGetTexture(texture.as_concrete_TypeRef()); - metal::TextureRef::from_ptr(texture as *mut _) - }), - ); - - unsafe { - let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8) - .add(*instance_offset) - as *mut SurfaceBounds; - ptr::write( - buffer_contents, - SurfaceBounds { - bounds: surface.bounds, - content_mask: surface.content_mask, - is_bgra: u32::from(is_bgra), - }, - ); - } - - command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); - *instance_offset = next_offset; - } - true - } + /// 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 = "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 blur_downsample_pipeline_state = build_blur_pipeline_state( + &device, + &library, + "blur_downsample", + "blur_fullscreen_vertex", + "blur_downsample_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let blur_pipeline_state = build_blur_pipeline_state( + &device, + &library, + "blur", + "blur_fullscreen_vertex", + "blur_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + // Premultiplied blend (One / OneMinusSourceAlpha) — the composite outputs a premultiplied + // blurred sample; straight-alpha blending would darken the faded edges. + let blur_composite_pipeline_state = build_path_sprite_pipeline_state( + &device, + &library, + "blur_composite", + "blur_composite_vertex", + "blur_composite_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, + blur_downsample_pipeline_state, + blur_pipeline_state, + blur_composite_pipeline_state, + unit_vertices, + instance_buffer_pool, + sprite_atlas, + core_video_texture_cache, + path_intermediate_texture: None, + path_intermediate_msaa_texture: None, + scene_color_texture: None, + blur_ping_texture: None, + blur_pong_texture: None, + group_textures: Vec::new(), + path_sample_count: PATH_SAMPLE_COUNT, + } + } + + 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; + self.scene_color_texture = None; + self.blur_ping_texture = None; + self.blur_pong_texture = None; + self.group_textures.clear(); + 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)); + + // Full-res scene + group targets, and half-res ping/pong blur targets. + let make_color_texture = |width: u64, height: u64| { + let descriptor = metal::TextureDescriptor::new(); + descriptor.set_width(width.max(1)); + descriptor.set_height(height.max(1)); + descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); + descriptor.set_storage_mode(metal::MTLStorageMode::Private); + descriptor.set_usage( + metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, + ); + self.device.new_texture(&descriptor) + }; + let full_w = size.width.0 as u64; + let full_h = size.height.0 as u64; + self.scene_color_texture = Some(make_color_texture(full_w, full_h)); + self.group_textures = (0..MAX_FILTER_DEPTH) + .map(|_| make_color_texture(full_w, full_h)) + .collect(); + self.blur_ping_texture = Some(make_color_texture(full_w / 2, full_h / 2)); + self.blur_pong_texture = Some(make_color_texture(full_w / 2, full_h / 2)); + + 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; + }; + + loop { + let mut instance_buffer = self + .instance_buffer_pool + .lock() + .acquire(&self.device, self.is_unified_memory); + + let command_buffer = + self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size); + + match command_buffer { + Ok(command_buffer) => { + let instance_buffer_pool = self.instance_buffer_pool.clone(); + let instance_buffer = Cell::new(Some(instance_buffer)); + 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); + + if self.presents_with_transaction { + command_buffer.commit(); + command_buffer.wait_until_scheduled(); + drawable.present(); + } else { + command_buffer.present_drawable(drawable); + command_buffer.commit(); + } + return; + } + Err(err) => { + log::error!( + "failed to render: {}. retrying with larger instance buffer size", + err + ); + let mut instance_buffer_pool = self.instance_buffer_pool.lock(); + let buffer_size = instance_buffer_pool.buffer_size; + if buffer_size >= 256 * 1024 * 1024 { + log::error!("instance buffer size grew too large: {}", buffer_size); + break; + } + instance_buffer_pool.reset(buffer_size * 2); + log::info!( + "increased instance buffer size to {}", + instance_buffer_pool.buffer_size + ); + } + } + } + } + + /// 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"))?; + + loop { + let mut instance_buffer = self + .instance_buffer_pool + .lock() + .acquire(&self.device, self.is_unified_memory); + + let command_buffer = + self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size); + + match command_buffer { + Ok(command_buffer) => { + let instance_buffer_pool = self.instance_buffer_pool.clone(); + let instance_buffer = Cell::new(Some(instance_buffer)); + 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); + + // Commit and wait for completion without presenting + command_buffer.commit(); + command_buffer.wait_until_completed(); + + // Read pixels from the texture + let texture = drawable.texture(); + let width = texture.width() as u32; + let height = texture.height() as u32; + let bytes_per_row = width as usize * 4; + let buffer_size = height as usize * bytes_per_row; + + let mut pixels = vec![0u8; buffer_size]; + + 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); + } + + return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| { + anyhow::anyhow!("Failed to create RgbaImage from pixel data") + }); + } + Err(err) => { + log::error!( + "failed to render: {}. retrying with larger instance buffer size", + err + ); + let mut instance_buffer_pool = self.instance_buffer_pool.lock(); + let buffer_size = instance_buffer_pool.buffer_size; + if buffer_size >= 256 * 1024 * 1024 { + anyhow::bail!("instance buffer size grew too large: {}", buffer_size); + } + instance_buffer_pool.reset(buffer_size * 2); + log::info!( + "increased instance buffer size to {}", + instance_buffer_pool.buffer_size + ); + } + } + } + } + + /// 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 = "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); + + loop { + let mut instance_buffer = self + .instance_buffer_pool + .lock() + .acquire(&self.device, self.is_unified_memory); + + let command_buffer = + self.draw_primitives_to_texture(scene, &mut instance_buffer, &target_texture, size); + + match command_buffer { + Ok(command_buffer) => { + let instance_buffer_pool = self.instance_buffer_pool.clone(); + let instance_buffer = Cell::new(Some(instance_buffer)); + 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); + + // 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 pixels from the texture + let width = size.width.0 as u32; + let height = size.height.0 as u32; + let bytes_per_row = width as usize * 4; + let buffer_size = height as usize * bytes_per_row; + + let mut pixels = vec![0u8; buffer_size]; + + 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, + }, + }; + + target_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); + } + + return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| { + anyhow::anyhow!("Failed to create RgbaImage from pixel data") + }); + } + Err(err) => { + log::error!( + "failed to render: {}. retrying with larger instance buffer size", + err + ); + let mut instance_buffer_pool = self.instance_buffer_pool.lock(); + let buffer_size = instance_buffer_pool.buffer_size; + if buffer_size >= 256 * 1024 * 1024 { + anyhow::bail!("instance buffer size grew too large: {}", buffer_size); + } + instance_buffer_pool.reset(buffer_size * 2); + log::info!( + "increased instance buffer size to {}", + instance_buffer_pool.buffer_size + ); + } + } + } + } + + fn draw_primitives( + &mut self, + scene: &Scene, + instance_buffer: &mut InstanceBuffer, + drawable: &metal::MetalDrawableRef, + viewport_size: Size, + ) -> Result { + self.draw_primitives_to_texture(scene, instance_buffer, drawable.texture(), viewport_size) + } + + fn draw_primitives_to_texture( + &mut self, + scene: &Scene, + instance_buffer: &mut InstanceBuffer, + 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 instance_offset = 0; + + // Render the scene into an offscreen color texture (so filters can sample it), then + // blit it to `texture`. Owned clones keep the textures borrowable without borrowing + // `self` across the batch loop (which calls `&mut self` methods like `draw_surfaces`). + // Only route through the offscreen scene texture when the scene actually contains blur + // filters; otherwise render straight to `texture` exactly as before (no regression, no + // extra blit for the common case). + let use_offscreen = + !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); + let scene_color_owned = self.scene_color_texture.clone(); + let blur_ping_owned = self.blur_ping_texture.clone(); + let blur_pong_owned = self.blur_pong_texture.clone(); + let group_owned = self.group_textures.clone(); + let scene_color: &metal::TextureRef = if use_offscreen { + scene_color_owned.as_deref().unwrap_or(texture) + } else { + texture + }; + // The active render target; switches to the group texture inside a content-filter group. + let mut current_target: &metal::TextureRef = scene_color; + // (boundary, parent target to composite back into, whether this level is isolated). + let mut filter_stack: Vec<(FilterBoundary, &metal::TextureRef, bool)> = Vec::new(); + + let mut command_encoder = new_command_encoder_for_texture( + command_buffer, + current_target, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); + }, + ); + + for batch in scene.batches() { + let ok = match batch { + PrimitiveBatch::Shadows(range) => self.draw_shadows( + &scene.shadows[range], + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Quads(range) => self.draw_quads( + &scene.quads[range], + instance_buffer, + &mut instance_offset, + 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, + instance_buffer, + &mut instance_offset, + viewport_size, + command_buffer, + ); + + command_encoder = new_command_encoder_for_texture( + command_buffer, + current_target, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + }, + ); + + if did_draw { + self.draw_paths_from_intermediate( + paths, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ) + } else { + false + } + } + PrimitiveBatch::Underlines(range) => self.draw_underlines( + &scene.underlines[range], + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::MonochromeSprites { texture_id, range } => self + .draw_monochrome_sprites( + texture_id, + &scene.monochrome_sprites[range], + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::PolychromeSprites { texture_id, range } => self + .draw_polychrome_sprites( + texture_id, + &scene.polychrome_sprites[range], + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Surfaces(range) => self.draw_surfaces( + &scene.surfaces[range], + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::BackdropFilters(range) => { + command_encoder.end_encoding(); + if let (Some(ping), Some(pong)) = + (blur_ping_owned.as_deref(), blur_pong_owned.as_deref()) + { + for filter in &scene.backdrop_filters[range] { + self.metal_blur_and_composite( + command_buffer, + current_target, + current_target, + ping, + pong, + viewport_size, + filter.bounds, + filter.content_mask.bounds, + filter.corner_radii, + max_blur_radius(&filter.filters), + filter.opacity, + true, + ); + } + } + command_encoder = new_command_encoder_for_texture( + command_buffer, + current_target, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + }, + ); + true + } + PrimitiveBatch::FilterBoundary(ix) => { + let boundary = scene.filter_boundaries[ix].clone(); + if boundary.is_start { + // Each isolated nesting level uses its own group texture from the pool + // (indexed by current isolation depth). Beyond the pool size + // (MAX_FILTER_DEPTH) deeper filters render inline without isolation rather + // than corrupting an outer group. + let depth = filter_stack.iter().filter(|entry| entry.2).count(); + if depth < group_owned.len() { + command_encoder.end_encoding(); + let parent = current_target; + current_target = group_owned[depth].as_ref(); + filter_stack.push((boundary, parent, true)); + command_encoder = new_command_encoder_for_texture( + command_buffer, + current_target, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment + .set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); + }, + ); + } else { + filter_stack.push((boundary, current_target, false)); + } + } else if let Some((boundary, parent, isolated)) = filter_stack.pop() { + if isolated { + command_encoder.end_encoding(); + if let (Some(ping), Some(pong)) = + (blur_ping_owned.as_deref(), blur_pong_owned.as_deref()) + { + self.metal_blur_and_composite( + command_buffer, + current_target, + parent, + ping, + pong, + viewport_size, + boundary.bounds, + boundary.content_mask.bounds, + boundary.corner_radii, + max_blur_radius(&boundary.filters), + boundary.opacity, + false, + ); + } + current_target = parent; + command_encoder = new_command_encoder_for_texture( + command_buffer, + current_target, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + }, + ); + } + } + true + } + PrimitiveBatch::SubpixelSprites { .. } => unreachable!(), + }; + if !ok { + command_encoder.end_encoding(); + anyhow::bail!( + "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(), + ); + } + } + + command_encoder.end_encoding(); + + // Present the offscreen scene by copying it into the drawable/target texture. + if use_offscreen && scene_color_owned.is_some() { + self.run_metal_blur_pass( + command_buffer, + &self.blur_downsample_pipeline_state, + texture, + scene_color, + viewport_size, + BlurUniform::default(), + false, + ); + } + + if !self.is_unified_memory { + // Sync the instance buffer to the GPU + instance_buffer.metal_buffer.did_modify_range(NSRange { + location: 0, + length: instance_offset as NSUInteger, + }); + } + + Ok(command_buffer.to_owned()) + } + + /// Run a single blur pass: draw a full-screen (or composite) quad sampling `source` into + /// `target`. `params` is supplied to both shader stages; `load` keeps existing target + /// contents (used by the composite), otherwise the target is cleared. + #[allow(clippy::too_many_arguments)] + fn run_metal_blur_pass( + &self, + command_buffer: &metal::CommandBufferRef, + pipeline: &metal::RenderPipelineState, + target: &metal::TextureRef, + source: &metal::TextureRef, + target_viewport: Size, + params: BlurUniform, + load: bool, + ) { + let encoder = new_command_encoder_for_texture( + command_buffer, + target, + target_viewport, + |color_attachment| { + if load { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + } else { + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); + } + }, + ); + encoder.set_render_pipeline_state(pipeline); + encoder.set_vertex_buffer( + BlurInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + encoder.set_vertex_bytes( + BlurInputIndex::Params as u64, + mem::size_of::() as u64, + ¶ms as *const BlurUniform as *const _, + ); + encoder.set_vertex_bytes( + BlurInputIndex::ViewportSize as u64, + mem::size_of_val(&target_viewport) as u64, + &target_viewport as *const Size as *const _, + ); + encoder.set_fragment_bytes( + BlurInputIndex::Params as u64, + mem::size_of::() as u64, + ¶ms as *const BlurUniform as *const _, + ); + encoder.set_fragment_bytes( + BlurInputIndex::ViewportSize as u64, + mem::size_of_val(&target_viewport) as u64, + &target_viewport as *const Size as *const _, + ); + encoder.set_fragment_texture(0, Some(source)); + encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + encoder.end_encoding(); + } + + /// Blur `source` (full-resolution) using the half-res ping/pong textures and composite the + /// result into `target`, clipped to `bounds`/`corner_radii`/`content_mask` and modulated by + /// `opacity`. Shared by the backdrop and content-filter paths. + #[allow(clippy::too_many_arguments)] + fn metal_blur_and_composite( + &self, + command_buffer: &metal::CommandBufferRef, + source: &metal::TextureRef, + target: &metal::TextureRef, + ping: &metal::TextureRef, + pong: &metal::TextureRef, + viewport_size: Size, + bounds: Bounds, + content_mask: Bounds, + corner_radii: Corners, + blur_radius: f32, + opacity: f32, + // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. + clip_rounded: bool, + ) { + // Sigma is halved because the blur runs at half resolution. + let sigma = (blur_radius * 0.5).max(0.0); + if sigma <= 0.0 { + return; + } + // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather + // than truncating the kernel — keeps very large radii from clipping. Matches wgpu. + let ideal_taps = (3.0 * sigma).ceil(); + let tap_count = ideal_taps.clamp(1.0, 32.0); + let tap_step = (ideal_taps / tap_count).max(1.0); + // Content blur bleeds ~3·radius past the box, so its composite quad covers a dilated rect. + let composite_bounds = if clip_rounded { + bounds + } else { + bounds.dilate(ScaledPixels(3.0 * blur_radius)) + }; + let half = Size { + width: DevicePixels((i32::from(viewport_size.width) / 2).max(1)), + height: DevicePixels((i32::from(viewport_size.height) / 2).max(1)), + }; + let half_w = i32::from(half.width) as f32; + let half_h = i32::from(half.height) as f32; + + // Downsample source -> ping, then separable gaussian ping -> pong -> ping. + self.run_metal_blur_pass( + command_buffer, + &self.blur_downsample_pipeline_state, + ping, + source, + half, + BlurUniform { + downsample: 1.0, + ..Default::default() + }, + false, + ); + self.run_metal_blur_pass( + command_buffer, + &self.blur_pipeline_state, + pong, + ping, + half, + BlurUniform { + direction: [1.0 / half_w, 0.0], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + false, + ); + self.run_metal_blur_pass( + command_buffer, + &self.blur_pipeline_state, + ping, + pong, + half, + BlurUniform { + direction: [0.0, 1.0 / half_h], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + false, + ); + + // Composite the blurred result into the target (preserving its contents). + self.run_metal_blur_pass( + command_buffer, + &self.blur_composite_pipeline_state, + target, + ping, + viewport_size, + BlurUniform { + bounds: composite_bounds, + content_mask, + corner_radii, + opacity, + clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, + ..Default::default() + }, + true, + ); + } + + fn draw_paths_to_intermediate( + &self, + paths: &[Path], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_buffer: &metal::CommandBufferRef, + ) -> bool { + if paths.is_empty() { + return true; + } + let Some(intermediate_texture) = &self.path_intermediate_texture else { + return false; + }; + + 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_render_pipeline_state(&self.paths_rasterization_pipeline_state); + + align_offset(instance_offset); + 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), + })); + } + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *instance_offset + vertices_bytes_len; + if next_offset > instance_buffer.size { + command_encoder.end_encoding(); + return false; + } + command_encoder.set_vertex_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_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(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + vertices.as_ptr() as *const u8, + buffer_contents, + vertices_bytes_len, + ); + } + command_encoder.draw_primitives( + metal::MTLPrimitiveType::Triangle, + 0, + vertices.len() as u64, + ); + *instance_offset = next_offset; + + command_encoder.end_encoding(); + true + } + + fn draw_shadows( + &self, + shadows: &[Shadow], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if shadows.is_empty() { + return true; + } + align_offset(instance_offset); + + 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_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + ShadowInputIndex::Shadows as u64, + Some(&instance_buffer.metal_buffer), + *instance_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 _, + ); + + let shadow_bytes_len = mem::size_of_val(shadows); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + shadow_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + shadows.as_ptr() as *const u8, + buffer_contents, + shadow_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + shadows.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_quads( + &self, + quads: &[Quad], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if quads.is_empty() { + return true; + } + align_offset(instance_offset); + + 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_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + QuadInputIndex::Quads as u64, + Some(&instance_buffer.metal_buffer), + *instance_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 _, + ); + + let quad_bytes_len = mem::size_of_val(quads); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + quad_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + quads.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_paths_from_intermediate( + &self, + paths: &[Path], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + let Some(first_path) = paths.first() else { + return true; + }; + + let Some(ref intermediate_texture) = self.path_intermediate_texture else { + return false; + }; + + 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 }]; + } + + align_offset(instance_offset); + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + + true + } + + fn draw_underlines( + &self, + underlines: &[Underline], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if underlines.is_empty() { + return true; + } + align_offset(instance_offset); + + 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_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + UnderlineInputIndex::Underlines as u64, + Some(&instance_buffer.metal_buffer), + *instance_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 _, + ); + + let underline_bytes_len = mem::size_of_val(underlines); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + underline_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + underlines.as_ptr() as *const u8, + buffer_contents, + underline_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + underlines.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_monochrome_sprites( + &self, + texture_id: AtlasTextureId, + sprites: &[MonochromeSprite], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + 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_buffer.metal_buffer), + *instance_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_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_polychrome_sprites( + &self, + texture_id: AtlasTextureId, + sprites: &[PolychromeSprite], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + 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_buffer.metal_buffer), + *instance_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_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_surfaces( + &mut self, + surfaces: &[PaintSurface], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + 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_bytes( + SurfaceInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + for surface in surfaces { + let texture_size = size( + DevicePixels::from(surface.image_buffer.get_width() as i32), + DevicePixels::from(surface.image_buffer.get_height() as i32), + ); + + let pixel_format = surface.image_buffer.get_pixel_format(); + let is_bgra = if pixel_format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange { + false + } else if pixel_format == kCVPixelFormatType_32BGRA { + true + } else { + log::error!("unsupported surface pixel format: {pixel_format}"); + return false; + }; + + // Y (or the whole BGRA image) texture from plane 0. + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + None, + if is_bgra { + MTLPixelFormat::BGRA8Unorm + } else { + MTLPixelFormat::R8Unorm + }, + surface.image_buffer.get_width_of_plane(0), + surface.image_buffer.get_height_of_plane(0), + 0, + ) + .unwrap(); + + let cb_cr_texture = if is_bgra { + None + } else { + Some( + 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(), + ) + }; + + align_offset(instance_offset); + let next_offset = *instance_offset + mem::size_of::(); + if next_offset > instance_buffer.size { + return false; + } + + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Surfaces as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::TextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + 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, + cb_cr_texture.as_ref().map(|texture| unsafe { + let texture = CVMetalTextureGetTexture(texture.as_concrete_TypeRef()); + metal::TextureRef::from_ptr(texture as *mut _) + }), + ); + + unsafe { + let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8) + .add(*instance_offset) as *mut SurfaceBounds; + ptr::write( + buffer_contents, + SurfaceBounds { + bounds: surface.bounds, + content_mask: surface.content_mask, + is_bgra: u32::from(is_bgra), + }, + ); + } + + command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + *instance_offset = next_offset; + } + true + } } fn new_command_encoder_for_texture<'a>( - command_buffer: &'a metal::CommandBufferRef, - texture: &'a metal::TextureRef, - viewport_size: Size, - configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef), + command_buffer: &'a metal::CommandBufferRef, + texture: &'a metal::TextureRef, + viewport_size: Size, + configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef), ) -> &'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); - configure_color_attachment(color_attachment); + 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); + configure_color_attachment(color_attachment); - 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 + 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 } fn build_pipeline_state( - device: &metal::DeviceRef, - library: &metal::LibraryRef, - label: &str, - vertex_fn_name: &str, - fragment_fn_name: &str, - pixel_format: metal::MTLPixelFormat, + 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 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); + 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") + 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, + 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 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); + 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") + 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, + 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 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); + 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") + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") } // Blur downsample/gaussian passes overwrite their target (no blending). The composite pass // uses the normal alpha-blending pipeline (`build_pipeline_state`) instead. fn build_blur_pipeline_state( - device: &metal::DeviceRef, - library: &metal::LibraryRef, - label: &str, - vertex_fn_name: &str, - fragment_fn_name: &str, - pixel_format: metal::MTLPixelFormat, + 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 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(false); + 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(false); - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") } // Align to multiples of 256 make Metal happy. fn align_offset(offset: &mut usize) { - *offset = (*offset).div_ceil(256) * 256; + *offset = (*offset).div_ceil(256) * 256; } #[repr(C)] enum ShadowInputIndex { - Vertices = 0, - Shadows = 1, - ViewportSize = 2, + Vertices = 0, + Shadows = 1, + ViewportSize = 2, } #[repr(C)] enum QuadInputIndex { - Vertices = 0, - Quads = 1, - ViewportSize = 2, + Vertices = 0, + Quads = 1, + ViewportSize = 2, } #[repr(C)] enum UnderlineInputIndex { - Vertices = 0, - Underlines = 1, - ViewportSize = 2, + Vertices = 0, + Underlines = 1, + ViewportSize = 2, } #[repr(C)] enum SpriteInputIndex { - Vertices = 0, - Sprites = 1, - ViewportSize = 2, - AtlasTextureSize = 3, - AtlasTexture = 4, + 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, + Vertices = 0, + Surfaces = 1, + ViewportSize = 2, + TextureSize = 3, + YTexture = 4, + CbCrTexture = 5, } #[repr(C)] enum PathRasterizationInputIndex { - Vertices = 0, - ViewportSize = 1, + Vertices = 0, + ViewportSize = 1, } #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct PathSprite { - pub bounds: Bounds, + pub bounds: Bounds, } #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct SurfaceBounds { - pub bounds: Bounds, - pub content_mask: ContentMask, - /// `1` when the surface is a single-plane BGRA buffer (sampled directly, - /// no YUV conversion), `0` for the biplanar 4:2:0 video format. - pub is_bgra: u32, + pub bounds: Bounds, + pub content_mask: ContentMask, + /// `1` when the surface is a single-plane BGRA buffer (sampled directly, + /// no YUV conversion), `0` for the biplanar 4:2:0 video format. + pub is_bgra: u32, } #[cfg(any(test, feature = "test-support"))] pub struct MetalHeadlessRenderer { - renderer: MetalRenderer, + renderer: MetalRenderer, } #[cfg(any(test, 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 } - } + 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 = "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_to_image( + &mut self, + scene: &Scene, + size: Size, + ) -> anyhow::Result { + self.renderer.render_scene_to_image(scene, size) + } - fn sprite_atlas(&self) -> Arc { - self.renderer.sprite_atlas().clone() - } + fn sprite_atlas(&self) -> Arc { + self.renderer.sprite_atlas().clone() + } } diff --git a/crates/gpui_macos/src/open_type.rs b/crates/gpui_macos/src/open_type.rs index 048ba13dd1..4ee2ed5afd 100644 --- a/crates/gpui_macos/src/open_type.rs +++ b/crates/gpui_macos/src/open_type.rs @@ -2,151 +2,151 @@ use cocoa::appkit::CGFloat; use core_foundation::{ - array::{ - CFArray, CFArrayAppendArray, CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount, - CFArrayGetValueAtIndex, CFArrayRef, CFMutableArrayRef, kCFTypeArrayCallBacks, - }, - base::{CFRelease, TCFType, kCFAllocatorDefault}, - dictionary::{ - CFDictionaryCreate, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, - }, - number::CFNumber, - string::{CFString, CFStringRef}, + array::{ + CFArray, CFArrayAppendArray, CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount, + CFArrayGetValueAtIndex, CFArrayRef, CFMutableArrayRef, kCFTypeArrayCallBacks, + }, + base::{CFRelease, TCFType, kCFAllocatorDefault}, + dictionary::{ + CFDictionaryCreate, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, + }, + number::CFNumber, + string::{CFString, CFStringRef}, }; use core_foundation_sys::locale::CFLocaleCopyPreferredLanguages; use core_graphics::{display::CFDictionary, geometry::CGAffineTransform}; use core_text::{ - font::{CTFont, CTFontRef, cascade_list_for_languages}, - font_descriptor::{ - CTFontDescriptor, CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature, - CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize, - CTFontDescriptorRef, kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute, - }, + font::{CTFont, CTFontRef, cascade_list_for_languages}, + font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature, + CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize, + CTFontDescriptorRef, kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute, + }, }; use font_kit::font::Font as FontKitFont; use gpui::{FontFallbacks, FontFeatures}; use std::ptr; pub fn apply_features_and_fallbacks( - font: &mut FontKitFont, - features: &FontFeatures, - fallbacks: Option<&FontFallbacks>, + font: &mut FontKitFont, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, ) -> anyhow::Result<()> { - unsafe { - let mut keys = vec![kCTFontFeatureSettingsAttribute]; - let mut values = vec![generate_feature_array(features)]; - if let Some(fallbacks) = fallbacks - && !fallbacks.fallback_list().is_empty() - { - keys.push(kCTFontCascadeListAttribute); - values.push(generate_fallback_array( - fallbacks, - font.native_font().as_concrete_TypeRef(), - )); - } - let attrs = CFDictionaryCreate( - kCFAllocatorDefault, - keys.as_ptr() as _, - values.as_ptr() as _, - keys.len() as isize, - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks, - ); + unsafe { + let mut keys = vec![kCTFontFeatureSettingsAttribute]; + let mut values = vec![generate_feature_array(features)]; + if let Some(fallbacks) = fallbacks + && !fallbacks.fallback_list().is_empty() + { + keys.push(kCTFontCascadeListAttribute); + values.push(generate_fallback_array( + fallbacks, + font.native_font().as_concrete_TypeRef(), + )); + } + let attrs = CFDictionaryCreate( + kCFAllocatorDefault, + keys.as_ptr() as _, + values.as_ptr() as _, + keys.len() as isize, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); - for value in &values { - CFRelease(*value as _); - } + for value in &values { + CFRelease(*value as _); + } - let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs); - CFRelease(attrs as _); - let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); - let new_font = CTFontCreateCopyWithAttributes( - font.native_font().as_concrete_TypeRef(), - 0.0, - std::ptr::null(), - new_descriptor.as_concrete_TypeRef(), - ); - let new_font = CTFont::wrap_under_create_rule(new_font); - *font = font_kit::font::Font::from_native_font(&new_font); + let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs); + CFRelease(attrs as _); + let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); + let new_font = CTFontCreateCopyWithAttributes( + font.native_font().as_concrete_TypeRef(), + 0.0, + std::ptr::null(), + new_descriptor.as_concrete_TypeRef(), + ); + let new_font = CTFont::wrap_under_create_rule(new_font); + *font = font_kit::font::Font::from_native_font(&new_font); - Ok(()) - } + Ok(()) + } } fn generate_feature_array(features: &FontFeatures) -> CFMutableArrayRef { - unsafe { - let feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); - for (tag, value) in features.tag_value_list() { - let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue]; - let values = [ - CFString::new(tag).as_CFTypeRef(), - CFNumber::from(*value as i32).as_CFTypeRef(), - ]; - let dict = CFDictionaryCreate( - kCFAllocatorDefault, - &keys as *const _ as _, - &values as *const _ as _, - 2, - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks, - ); - values.into_iter().for_each(|value| CFRelease(value)); - CFArrayAppendValue(feature_array, dict as _); - CFRelease(dict as _); - } - feature_array - } + unsafe { + let feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for (tag, value) in features.tag_value_list() { + let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue]; + let values = [ + CFString::new(tag).as_CFTypeRef(), + CFNumber::from(*value as i32).as_CFTypeRef(), + ]; + let dict = CFDictionaryCreate( + kCFAllocatorDefault, + &keys as *const _ as _, + &values as *const _ as _, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + values.into_iter().for_each(|value| CFRelease(value)); + CFArrayAppendValue(feature_array, dict as _); + CFRelease(dict as _); + } + feature_array + } } fn generate_fallback_array(fallbacks: &FontFallbacks, font_ref: CTFontRef) -> CFMutableArrayRef { - unsafe { - let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); - for user_fallback in fallbacks.fallback_list() { - let name = CFString::from(user_fallback.as_str()); - let fallback_desc = - CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0); - CFArrayAppendValue(fallback_array, fallback_desc as _); - CFRelease(fallback_desc as _); - } - append_system_fallbacks(fallback_array, font_ref); - fallback_array - } + unsafe { + let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for user_fallback in fallbacks.fallback_list() { + let name = CFString::from(user_fallback.as_str()); + let fallback_desc = + CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0); + CFArrayAppendValue(fallback_array, fallback_desc as _); + CFRelease(fallback_desc as _); + } + append_system_fallbacks(fallback_array, font_ref); + fallback_array + } } fn append_system_fallbacks(fallback_array: CFMutableArrayRef, font_ref: CTFontRef) { - unsafe { - let preferred_languages: CFArray = - CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages()); + unsafe { + let preferred_languages: CFArray = + CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages()); - let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages( - font_ref, - preferred_languages.as_concrete_TypeRef(), - ); - let default_fallbacks: CFArray = - CFArray::wrap_under_create_rule(default_fallbacks); + let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages( + font_ref, + preferred_languages.as_concrete_TypeRef(), + ); + let default_fallbacks: CFArray = + CFArray::wrap_under_create_rule(default_fallbacks); - default_fallbacks - .iter() - .filter(|desc| desc.font_path().is_some()) - .map(|desc| { - CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _); - }); - } + default_fallbacks + .iter() + .filter(|desc| desc.font_path().is_some()) + .map(|desc| { + CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _); + }); + } } #[link(name = "CoreText", kind = "framework")] unsafe extern "C" { - static kCTFontOpenTypeFeatureTag: CFStringRef; - static kCTFontOpenTypeFeatureValue: CFStringRef; + static kCTFontOpenTypeFeatureTag: CFStringRef; + static kCTFontOpenTypeFeatureValue: CFStringRef; - fn CTFontCreateCopyWithAttributes( - font: CTFontRef, - size: CGFloat, - matrix: *const CGAffineTransform, - attributes: CTFontDescriptorRef, - ) -> CTFontRef; - fn CTFontCopyDefaultCascadeListForLanguages( - font: CTFontRef, - languagePrefList: CFArrayRef, - ) -> CFArrayRef; + fn CTFontCreateCopyWithAttributes( + font: CTFontRef, + size: CGFloat, + matrix: *const CGAffineTransform, + attributes: CTFontDescriptorRef, + ) -> CTFontRef; + fn CTFontCopyDefaultCascadeListForLanguages( + font: CTFontRef, + languagePrefList: CFArrayRef, + ) -> CFArrayRef; } diff --git a/crates/gpui_macos/src/pasteboard.rs b/crates/gpui_macos/src/pasteboard.rs index 8362ab8f3b..5f5ecf5ff3 100644 --- a/crates/gpui_macos/src/pasteboard.rs +++ b/crates/gpui_macos/src/pasteboard.rs @@ -3,12 +3,12 @@ use std::ffi::{CStr, c_void}; use std::path::PathBuf; use cocoa::{ - appkit::{ - NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, - NSPasteboardTypeTIFF, - }, - base::{id, nil}, - foundation::{NSArray, NSData, NSFastEnumeration, NSString}, + appkit::{ + NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, + NSPasteboardTypeTIFF, + }, + base::{id, nil}, + foundation::{NSArray, NSData, NSFastEnumeration, NSString}, }; use objc::{msg_send, runtime::Object, sel, sel_impl}; use smallvec::SmallVec; @@ -16,516 +16,516 @@ use strum::IntoEnumIterator as _; use crate::ns_string; use gpui::{ - ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash, + ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash, }; pub struct Pasteboard { - inner: id, - text_hash_type: id, - metadata_type: id, + inner: id, + text_hash_type: id, + metadata_type: id, } impl Pasteboard { - pub fn general() -> Self { - unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) } - } + pub fn general() -> Self { + unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) } + } - pub fn find() -> Self { - unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) } - } + pub fn find() -> Self { + unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) } + } - #[cfg(test)] - pub fn unique() -> Self { - unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) } - } + #[cfg(test)] + pub fn unique() -> Self { + unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) } + } - unsafe fn new(inner: id) -> Self { - Self { - inner, - text_hash_type: unsafe { ns_string("zed-text-hash") }, - metadata_type: unsafe { ns_string("zed-metadata") }, - } - } + unsafe fn new(inner: id) -> Self { + Self { + inner, + text_hash_type: unsafe { ns_string("zed-text-hash") }, + metadata_type: unsafe { ns_string("zed-metadata") }, + } + } - pub fn read(&self) -> Option { - unsafe { - // Check for file paths first - let filenames = NSPasteboard::propertyListForType(self.inner, NSFilenamesPboardType); - if filenames != nil && NSArray::count(filenames) > 0 { - let mut paths = SmallVec::new(); - for file in filenames.iter() { - let f = NSString::UTF8String(file); - let path = CStr::from_ptr(f).to_string_lossy().into_owned(); - paths.push(PathBuf::from(path)); - } - if !paths.is_empty() { - let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))]; + pub fn read(&self) -> Option { + unsafe { + // Check for file paths first + let filenames = NSPasteboard::propertyListForType(self.inner, NSFilenamesPboardType); + if filenames != nil && NSArray::count(filenames) > 0 { + let mut paths = SmallVec::new(); + for file in filenames.iter() { + let f = NSString::UTF8String(file); + let path = CStr::from_ptr(f).to_string_lossy().into_owned(); + paths.push(PathBuf::from(path)); + } + if !paths.is_empty() { + let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))]; - // Also include the string representation so text editors can - // paste the path as text. - if let Some(string_item) = self.read_string_from_pasteboard() { - entries.push(string_item); - } + // Also include the string representation so text editors can + // paste the path as text. + if let Some(string_item) = self.read_string_from_pasteboard() { + entries.push(string_item); + } - return Some(ClipboardItem { entries }); - } - } + return Some(ClipboardItem { entries }); + } + } - // Next, check for a plain string. - if let Some(string_entry) = self.read_string_from_pasteboard() { - return Some(ClipboardItem { - entries: vec![string_entry], - }); - } + // Next, check for a plain string. + if let Some(string_entry) = self.read_string_from_pasteboard() { + return Some(ClipboardItem { + entries: vec![string_entry], + }); + } - // Finally, try the various supported image types. - for format in ImageFormat::iter() { - if let Some(item) = self.read_image(format) { - return Some(item); - } - } - } + // Finally, try the various supported image types. + for format in ImageFormat::iter() { + if let Some(item) = self.read_image(format) { + return Some(item); + } + } + } - None - } + None + } - fn read_image(&self, format: ImageFormat) -> Option { - let ut_type: UTType = format.into(); + fn read_image(&self, format: ImageFormat) -> Option { + let ut_type: UTType = format.into(); - unsafe { - let types: id = self.inner.types(); - if msg_send![types, containsObject: ut_type.inner()] { - self.data_for_type(ut_type.inner_mut()).map(|bytes| { - let bytes = bytes.to_vec(); - let id = hash(&bytes); + unsafe { + let types: id = self.inner.types(); + if msg_send![types, containsObject: ut_type.inner()] { + self.data_for_type(ut_type.inner_mut()).map(|bytes| { + let bytes = bytes.to_vec(); + let id = hash(&bytes); - ClipboardItem { - entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], - } - }) - } else { - None - } - } - } + ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + } + }) + } else { + None + } + } + } - unsafe fn read_string_from_pasteboard(&self) -> Option { - unsafe { - let pasteboard_types: id = self.inner.types(); - let string_type: id = ns_string("public.utf8-plain-text"); + unsafe fn read_string_from_pasteboard(&self) -> Option { + unsafe { + let pasteboard_types: id = self.inner.types(); + let string_type: id = ns_string("public.utf8-plain-text"); - if !msg_send![pasteboard_types, containsObject: string_type] { - return None; - } + if !msg_send![pasteboard_types, containsObject: string_type] { + return None; + } - let data = self.inner.dataForType(string_type); - let text_bytes: &[u8] = if data == nil { - return None; - } else if data.bytes().is_null() { - // https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc - // "If the length of the NSData object is 0, this property returns nil." - &[] - } else { - slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize) - }; + let data = self.inner.dataForType(string_type); + let text_bytes: &[u8] = if data == nil { + return None; + } else if data.bytes().is_null() { + // https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc + // "If the length of the NSData object is 0, this property returns nil." + &[] + } else { + slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize) + }; - let text = String::from_utf8_lossy(text_bytes).to_string(); - let metadata = self - .data_for_type(self.text_hash_type) - .and_then(|hash_bytes| { - let hash_bytes = hash_bytes.try_into().ok()?; - let hash = u64::from_be_bytes(hash_bytes); - let metadata = self.data_for_type(self.metadata_type)?; + let text = String::from_utf8_lossy(text_bytes).to_string(); + let metadata = self + .data_for_type(self.text_hash_type) + .and_then(|hash_bytes| { + let hash_bytes = hash_bytes.try_into().ok()?; + let hash = u64::from_be_bytes(hash_bytes); + let metadata = self.data_for_type(self.metadata_type)?; - if hash == ClipboardString::text_hash(&text) { - String::from_utf8(metadata.to_vec()).ok() - } else { - None - } - }); + if hash == ClipboardString::text_hash(&text) { + String::from_utf8(metadata.to_vec()).ok() + } else { + None + } + }); - Some(ClipboardEntry::String(ClipboardString { text, metadata })) - } - } + Some(ClipboardEntry::String(ClipboardString { text, metadata })) + } + } - unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> { - unsafe { - let data = self.inner.dataForType(kind); - if data == nil { - None - } else { - Some(slice::from_raw_parts( - data.bytes() as *mut u8, - data.length() as usize, - )) - } - } - } + unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> { + unsafe { + let data = self.inner.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } + } - pub fn write(&self, item: ClipboardItem) { - unsafe { - match item.entries.as_slice() { - [] => { - // Writing an empty list of entries just clears the clipboard. - self.inner.clearContents(); - } - [ClipboardEntry::String(string)] => { - self.write_plaintext(string); - } - [ClipboardEntry::Image(image)] => { - self.write_image(image); - } - [ClipboardEntry::ExternalPaths(_)] => {} - _ => { - // Agus NB: We're currently only writing string entries to the clipboard when we have more than one. - // - // This was the existing behavior before I refactored the outer clipboard code: - // https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110 - // - // Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor. + pub fn write(&self, item: ClipboardItem) { + unsafe { + match item.entries.as_slice() { + [] => { + // Writing an empty list of entries just clears the clipboard. + self.inner.clearContents(); + } + [ClipboardEntry::String(string)] => { + self.write_plaintext(string); + } + [ClipboardEntry::Image(image)] => { + self.write_image(image); + } + [ClipboardEntry::ExternalPaths(_)] => {} + _ => { + // Agus NB: We're currently only writing string entries to the clipboard when we have more than one. + // + // This was the existing behavior before I refactored the outer clipboard code: + // https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110 + // + // Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor. - let mut combined = ClipboardString { - text: String::new(), - metadata: None, - }; + let mut combined = ClipboardString { + text: String::new(), + metadata: None, + }; - for entry in item.entries { - match entry { - ClipboardEntry::String(text) => { - combined.text.push_str(&text.text()); - if combined.metadata.is_none() { - combined.metadata = text.metadata; - } - } - _ => {} - } - } + for entry in item.entries { + match entry { + ClipboardEntry::String(text) => { + combined.text.push_str(&text.text()); + if combined.metadata.is_none() { + combined.metadata = text.metadata; + } + } + _ => {} + } + } - self.write_plaintext(&combined); - } - } - } - } + self.write_plaintext(&combined); + } + } + } + } - fn write_plaintext(&self, string: &ClipboardString) { - unsafe { - self.inner.clearContents(); + fn write_plaintext(&self, string: &ClipboardString) { + unsafe { + self.inner.clearContents(); - let text_bytes = NSData::dataWithBytes_length_( - nil, - string.text.as_ptr() as *const c_void, - string.text.len() as u64, - ); - self.inner - .setData_forType(text_bytes, NSPasteboardTypeString); + let text_bytes = NSData::dataWithBytes_length_( + nil, + string.text.as_ptr() as *const c_void, + string.text.len() as u64, + ); + self.inner + .setData_forType(text_bytes, NSPasteboardTypeString); - if let Some(metadata) = string.metadata.as_ref() { - let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes(); - let hash_bytes = NSData::dataWithBytes_length_( - nil, - hash_bytes.as_ptr() as *const c_void, - hash_bytes.len() as u64, - ); - self.inner.setData_forType(hash_bytes, self.text_hash_type); + if let Some(metadata) = string.metadata.as_ref() { + let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + self.inner.setData_forType(hash_bytes, self.text_hash_type); - let metadata_bytes = NSData::dataWithBytes_length_( - nil, - metadata.as_ptr() as *const c_void, - metadata.len() as u64, - ); - self.inner - .setData_forType(metadata_bytes, self.metadata_type); - } - } - } + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + self.inner + .setData_forType(metadata_bytes, self.metadata_type); + } + } + } - unsafe fn write_image(&self, image: &Image) { - unsafe { - self.inner.clearContents(); + unsafe fn write_image(&self, image: &Image) { + unsafe { + self.inner.clearContents(); - let bytes = NSData::dataWithBytes_length_( - nil, - image.bytes.as_ptr() as *const c_void, - image.bytes.len() as u64, - ); + let bytes = NSData::dataWithBytes_length_( + nil, + image.bytes.as_ptr() as *const c_void, + image.bytes.len() as u64, + ); - self.inner - .setData_forType(bytes, Into::::into(image.format).inner_mut()); - } - } + self.inner + .setData_forType(bytes, Into::::into(image.format).inner_mut()); + } + } } #[link(name = "AppKit", kind = "framework")] unsafe extern "C" { - /// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc) - pub static NSPasteboardNameFind: id; + /// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc) + pub static NSPasteboardNameFind: id; } impl From for UTType { - fn from(value: ImageFormat) -> Self { - match value { - ImageFormat::Png => Self::png(), - ImageFormat::Jpeg => Self::jpeg(), - ImageFormat::Tiff => Self::tiff(), - ImageFormat::Webp => Self::webp(), - ImageFormat::Gif => Self::gif(), - ImageFormat::Bmp => Self::bmp(), - ImageFormat::Svg => Self::svg(), - ImageFormat::Ico => Self::ico(), - ImageFormat::Pnm => Self::pnm(), - } - } + fn from(value: ImageFormat) -> Self { + match value { + ImageFormat::Png => Self::png(), + ImageFormat::Jpeg => Self::jpeg(), + ImageFormat::Tiff => Self::tiff(), + ImageFormat::Webp => Self::webp(), + ImageFormat::Gif => Self::gif(), + ImageFormat::Bmp => Self::bmp(), + ImageFormat::Svg => Self::svg(), + ImageFormat::Ico => Self::ico(), + ImageFormat::Pnm => Self::pnm(), + } + } } // See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ pub struct UTType(id); impl UTType { - pub fn png() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png - Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType - } + pub fn png() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png + Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType + } - pub fn jpeg() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg - Self(unsafe { ns_string("public.jpeg") }) - } + pub fn jpeg() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg + Self(unsafe { ns_string("public.jpeg") }) + } - pub fn gif() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif - Self(unsafe { ns_string("com.compuserve.gif") }) - } + pub fn gif() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif + Self(unsafe { ns_string("com.compuserve.gif") }) + } - pub fn webp() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp - Self(unsafe { ns_string("org.webmproject.webp") }) - } + pub fn webp() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp + Self(unsafe { ns_string("org.webmproject.webp") }) + } - pub fn bmp() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp - Self(unsafe { ns_string("com.microsoft.bmp") }) - } + pub fn bmp() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp + Self(unsafe { ns_string("com.microsoft.bmp") }) + } - pub fn svg() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg - Self(unsafe { ns_string("public.svg-image") }) - } + pub fn svg() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg + Self(unsafe { ns_string("public.svg-image") }) + } - pub fn ico() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico - Self(unsafe { ns_string("com.microsoft.ico") }) - } + pub fn ico() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico + Self(unsafe { ns_string("com.microsoft.ico") }) + } - pub fn tiff() -> Self { - // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff - Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType - } + pub fn tiff() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff + Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType + } - pub fn pnm() -> Self { - //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier - Self(unsafe { ns_string("public.pbm") }) - } + pub fn pnm() -> Self { + //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier + Self(unsafe { ns_string("public.pbm") }) + } - fn inner(&self) -> *const Object { - self.0 - } + fn inner(&self) -> *const Object { + self.0 + } - pub fn inner_mut(&self) -> *mut Object { - self.0 as *mut _ - } + pub fn inner_mut(&self) -> *mut Object { + self.0 as *mut _ + } } #[cfg(test)] mod tests { - use cocoa::{ - appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString}, - base::{id, nil}, - foundation::{NSArray, NSData}, - }; - use std::ffi::c_void; + use cocoa::{ + appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString}, + base::{id, nil}, + foundation::{NSArray, NSData}, + }; + use std::ffi::c_void; - use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat}; + use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat}; - use super::*; + use super::*; - unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) { - unsafe { - let ns_paths: Vec = paths.iter().map(|p| ns_string(p)).collect(); - let ns_array = NSArray::arrayWithObjects(nil, &ns_paths); + unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) { + unsafe { + let ns_paths: Vec = paths.iter().map(|p| ns_string(p)).collect(); + let ns_array = NSArray::arrayWithObjects(nil, &ns_paths); - let mut types = vec![NSFilenamesPboardType]; - types.push(NSPasteboardTypeString); + let mut types = vec![NSFilenamesPboardType]; + types.push(NSPasteboardTypeString); - let types_array = NSArray::arrayWithObjects(nil, &types); - pasteboard.inner.declareTypes_owner(types_array, nil); + let types_array = NSArray::arrayWithObjects(nil, &types); + pasteboard.inner.declareTypes_owner(types_array, nil); - pasteboard - .inner - .setPropertyList_forType(ns_array, NSFilenamesPboardType); + pasteboard + .inner + .setPropertyList_forType(ns_array, NSFilenamesPboardType); - let joined = paths.join("\n"); - let bytes = NSData::dataWithBytes_length_( - nil, - joined.as_ptr() as *const c_void, - joined.len() as u64, - ); - pasteboard - .inner - .setData_forType(bytes, NSPasteboardTypeString); - } - } + let joined = paths.join("\n"); + let bytes = NSData::dataWithBytes_length_( + nil, + joined.as_ptr() as *const c_void, + joined.len() as u64, + ); + pasteboard + .inner + .setData_forType(bytes, NSPasteboardTypeString); + } + } - #[test] - fn test_string() { - let pasteboard = Pasteboard::unique(); - assert_eq!(pasteboard.read(), None); + #[test] + fn test_string() { + let pasteboard = Pasteboard::unique(); + assert_eq!(pasteboard.read(), None); - let item = ClipboardItem::new_string("1".to_string()); - pasteboard.write(item.clone()); - assert_eq!(pasteboard.read(), Some(item)); + let item = ClipboardItem::new_string("1".to_string()); + pasteboard.write(item.clone()); + assert_eq!(pasteboard.read(), Some(item)); - let item = ClipboardItem { - entries: vec![ClipboardEntry::String( - ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]), - )], - }; - pasteboard.write(item.clone()); - assert_eq!(pasteboard.read(), Some(item)); + let item = ClipboardItem { + entries: vec![ClipboardEntry::String( + ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]), + )], + }; + pasteboard.write(item.clone()); + assert_eq!(pasteboard.read(), Some(item)); - let text_from_other_app = "text from other app"; - unsafe { - let bytes = NSData::dataWithBytes_length_( - nil, - text_from_other_app.as_ptr() as *const c_void, - text_from_other_app.len() as u64, - ); - pasteboard - .inner - .setData_forType(bytes, NSPasteboardTypeString); - } - assert_eq!( - pasteboard.read(), - Some(ClipboardItem::new_string(text_from_other_app.to_string())) - ); - } + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + pasteboard + .inner + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + pasteboard.read(), + Some(ClipboardItem::new_string(text_from_other_app.to_string())) + ); + } - #[test] - fn test_read_external_path() { - let pasteboard = Pasteboard::unique(); + #[test] + fn test_read_external_path() { + let pasteboard = Pasteboard::unique(); - unsafe { - simulate_external_file_copy(&pasteboard, &["/test.txt"]); - } + unsafe { + simulate_external_file_copy(&pasteboard, &["/test.txt"]); + } - let item = pasteboard.read().expect("should read clipboard item"); + let item = pasteboard.read().expect("should read clipboard item"); - // Test both ExternalPaths and String entries exist - assert_eq!(item.entries.len(), 2); + // Test both ExternalPaths and String entries exist + assert_eq!(item.entries.len(), 2); - // Test first entry is ExternalPaths - match &item.entries[0] { - ClipboardEntry::ExternalPaths(ep) => { - assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]); - } - other => panic!("expected ExternalPaths, got {:?}", other), - } + // Test first entry is ExternalPaths + match &item.entries[0] { + ClipboardEntry::ExternalPaths(ep) => { + assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]); + } + other => panic!("expected ExternalPaths, got {:?}", other), + } - // Test second entry is String - match &item.entries[1] { - ClipboardEntry::String(s) => { - assert_eq!(s.text(), "/test.txt"); - } - other => panic!("expected String, got {:?}", other), - } - } + // Test second entry is String + match &item.entries[1] { + ClipboardEntry::String(s) => { + assert_eq!(s.text(), "/test.txt"); + } + other => panic!("expected String, got {:?}", other), + } + } - #[test] - fn test_read_external_paths_with_spaces() { - let pasteboard = Pasteboard::unique(); - let paths = ["/some file with spaces.txt"]; + #[test] + fn test_read_external_paths_with_spaces() { + let pasteboard = Pasteboard::unique(); + let paths = ["/some file with spaces.txt"]; - unsafe { - simulate_external_file_copy(&pasteboard, &paths); - } + unsafe { + simulate_external_file_copy(&pasteboard, &paths); + } - let item = pasteboard.read().expect("should read clipboard item"); + let item = pasteboard.read().expect("should read clipboard item"); - match &item.entries[0] { - ClipboardEntry::ExternalPaths(ep) => { - assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]); - } - other => panic!("expected ExternalPaths, got {:?}", other), - } - } + match &item.entries[0] { + ClipboardEntry::ExternalPaths(ep) => { + assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]); + } + other => panic!("expected ExternalPaths, got {:?}", other), + } + } - #[test] - fn test_read_multiple_external_paths() { - let pasteboard = Pasteboard::unique(); - let paths = ["/file.txt", "/image.png"]; + #[test] + fn test_read_multiple_external_paths() { + let pasteboard = Pasteboard::unique(); + let paths = ["/file.txt", "/image.png"]; - unsafe { - simulate_external_file_copy(&pasteboard, &paths); - } + unsafe { + simulate_external_file_copy(&pasteboard, &paths); + } - let item = pasteboard.read().expect("should read clipboard item"); - assert_eq!(item.entries.len(), 2); + let item = pasteboard.read().expect("should read clipboard item"); + assert_eq!(item.entries.len(), 2); - // Test both ExternalPaths and String entries exist - match &item.entries[0] { - ClipboardEntry::ExternalPaths(ep) => { - assert_eq!( - ep.paths(), - &[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),] - ); - } - other => panic!("expected ExternalPaths, got {:?}", other), - } + // Test both ExternalPaths and String entries exist + match &item.entries[0] { + ClipboardEntry::ExternalPaths(ep) => { + assert_eq!( + ep.paths(), + &[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),] + ); + } + other => panic!("expected ExternalPaths, got {:?}", other), + } - match &item.entries[1] { - ClipboardEntry::String(s) => { - assert_eq!(s.text(), "/file.txt\n/image.png"); - assert_eq!(s.metadata, None); - } - other => panic!("expected String, got {:?}", other), - } - } + match &item.entries[1] { + ClipboardEntry::String(s) => { + assert_eq!(s.text(), "/file.txt\n/image.png"); + assert_eq!(s.metadata, None); + } + other => panic!("expected String, got {:?}", other), + } + } - #[test] - fn test_read_image() { - let pasteboard = Pasteboard::unique(); + #[test] + fn test_read_image() { + let pasteboard = Pasteboard::unique(); - // Smallest valid PNG: 1x1 transparent pixel - let png_bytes: &[u8] = &[ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00, - 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ]; + // Smallest valid PNG: 1x1 transparent pixel + let png_bytes: &[u8] = &[ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, + 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, + 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, + 0x9C, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + ]; - unsafe { - let ns_png_type = NSPasteboardTypePNG; - let types_array = NSArray::arrayWithObjects(nil, &[ns_png_type]); - pasteboard.inner.declareTypes_owner(types_array, nil); + unsafe { + let ns_png_type = NSPasteboardTypePNG; + let types_array = NSArray::arrayWithObjects(nil, &[ns_png_type]); + pasteboard.inner.declareTypes_owner(types_array, nil); - let data = NSData::dataWithBytes_length_( - nil, - png_bytes.as_ptr() as *const c_void, - png_bytes.len() as u64, - ); - pasteboard.inner.setData_forType(data, ns_png_type); - } + let data = NSData::dataWithBytes_length_( + nil, + png_bytes.as_ptr() as *const c_void, + png_bytes.len() as u64, + ); + pasteboard.inner.setData_forType(data, ns_png_type); + } - let item = pasteboard.read().expect("should read PNG image"); + let item = pasteboard.read().expect("should read PNG image"); - // Test Image entry exists - assert_eq!(item.entries.len(), 1); - match &item.entries[0] { - ClipboardEntry::Image(img) => { - assert_eq!(img.format, ImageFormat::Png); - assert_eq!(img.bytes, png_bytes); - } - other => panic!("expected Image, got {:?}", other), - } - } + // Test Image entry exists + assert_eq!(item.entries.len(), 1); + match &item.entries[0] { + ClipboardEntry::Image(img) => { + assert_eq!(img.format, ImageFormat::Png); + assert_eq!(img.bytes, png_bytes); + } + other => panic!("expected Image, got {:?}", other), + } + } } diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index 7b0fcef820..07b77b8fff 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -1,65 +1,65 @@ use crate::{ - BoolExt, MacDispatcher, MacDisplay, MacKeyboardLayout, MacKeyboardMapper, MacWindow, - events::key_to_native, haptic_feedback::MacHaptics, ns_string, pasteboard::Pasteboard, - renderer, set_active_window_cursor_style, + BoolExt, MacDispatcher, MacDisplay, MacKeyboardLayout, MacKeyboardMapper, MacWindow, + events::key_to_native, haptic_feedback::MacHaptics, ns_string, pasteboard::Pasteboard, + renderer, set_active_window_cursor_style, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; use cocoa::{ - appkit::{ - NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSControl as _, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, - NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow, - }, - base::{BOOL, NO, YES, id, nil, selector}, - foundation::{ - NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL, - }, + appkit::{ + NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, + NSControl as _, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, + NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow, + }, + base::{BOOL, NO, YES, id, nil, selector}, + foundation::{ + NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL, + }, }; use core_foundation::{ - base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType}, - boolean::CFBoolean, - data::CFData, - dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary}, - runloop::CFRunLoopRun, - string::{CFString, CFStringRef}, + base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType}, + boolean::CFBoolean, + data::CFData, + dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary}, + runloop::CFRunLoopRun, + string::{CFString, CFStringRef}, }; use ctor::ctor; use dispatch2::DispatchQueue; use futures::channel::oneshot; use gpui::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, - KeyContext, Keymap, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SystemMenuType, Task, ThermalState, WindowAppearance, WindowParams, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, + KeyContext, Keymap, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Result, SystemMenuType, Task, ThermalState, WindowAppearance, WindowParams, }; use itertools::Itertools; use objc::{ - class, - declare::ClassDecl, - msg_send, - runtime::{Class, Object, Sel}, - sel, sel_impl, + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, }; use parking_lot::Mutex; use ptr::null_mut; use semver::Version; use std::{ - cell::Cell, - ffi::{CStr, OsStr, c_void}, - os::{raw::c_char, unix::ffi::OsStrExt}, - path::{Path, PathBuf}, - ptr, - rc::Rc, - slice, str, - sync::{ - Arc, OnceLock, - atomic::{AtomicBool, Ordering}, - }, + cell::Cell, + ffi::{CStr, OsStr, c_void}, + os::{raw::c_char, unix::ffi::OsStrExt}, + path::{Path, PathBuf}, + ptr, + rc::Rc, + slice, str, + sync::{ + Arc, OnceLock, + atomic::{AtomicBool, Ordering}, + }, }; use util::{ - ResultExt, - command::{new_command, new_std_command}, + ResultExt, + command::{new_command, new_std_command}, }; #[allow(non_upper_case_globals)] @@ -71,1372 +71,1372 @@ static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); #[ctor(unsafe)] unsafe fn build_classes() { - unsafe { - APP_CLASS = { - let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); - decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); - decl.register() - } - }; - unsafe { - APP_DELEGATE_CLASS = { - let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); - decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); - decl.add_method( - sel!(applicationWillFinishLaunching:), - will_finish_launching as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(applicationDidFinishLaunching:), - did_finish_launching as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(applicationShouldHandleReopen:hasVisibleWindows:), - should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), - ); - decl.add_method( - sel!(applicationWillTerminate:), - will_terminate as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(handleGPUIMenuItem:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - // Add menu item handlers so that OS save panels have the correct key commands - decl.add_method( - sel!(cut:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(copy:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(paste:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(selectAll:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(undo:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(redo:), - handle_menu_item as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(validateMenuItem:), - validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, - ); - decl.add_method( - sel!(menuWillOpen:), - menu_will_open as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(applicationDockMenu:), - handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id, - ); - decl.add_method( - sel!(application:openURLs:), - open_urls as extern "C" fn(&mut Object, Sel, id, id), - ); + unsafe { + APP_CLASS = { + let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.register() + } + }; + unsafe { + APP_DELEGATE_CLASS = { + let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(applicationWillFinishLaunching:), + will_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationShouldHandleReopen:hasVisibleWindows:), + should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(handleGPUIMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + // Add menu item handlers so that OS save panels have the correct key commands + decl.add_method( + sel!(cut:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(copy:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(paste:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(selectAll:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(undo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(redo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(validateMenuItem:), + validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, + ); + decl.add_method( + sel!(menuWillOpen:), + menu_will_open as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDockMenu:), + handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id, + ); + decl.add_method( + sel!(application:openURLs:), + open_urls as extern "C" fn(&mut Object, Sel, id, id), + ); - decl.add_method( - sel!(onKeyboardLayoutChange:), - on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), - ); + decl.add_method( + sel!(onKeyboardLayoutChange:), + on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), + ); - decl.add_method( - sel!(onThermalStateChange:), - on_thermal_state_change as extern "C" fn(&mut Object, Sel, id), - ); + decl.add_method( + sel!(onThermalStateChange:), + on_thermal_state_change as extern "C" fn(&mut Object, Sel, id), + ); - decl.register() - } - } + decl.register() + } + } } pub struct MacPlatform(Mutex); pub(crate) struct MacPlatformState { - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - renderer_context: renderer::Context, - headless: bool, - general_pasteboard: Pasteboard, - find_pasteboard: Pasteboard, - reopen: Option>, - on_keyboard_layout_change: Option>, - on_thermal_state_change: Option>, - quit: Option>, - menu_command: Option>, - validate_menu_command: Option bool>>, - will_open_menu: Option>, - menu_actions: Vec>, - open_urls: Option)>>, - finish_launching: Option>, - dock_menu: Option, - menus: Option>, - keyboard_mapper: Rc, - /// Mirrors `[NSCursor setHiddenUntilMouseMoves:]` state, which AppKit doesn't expose. - cursor_visible: Arc, - /// Haptic feedback engine (macOS only, lazy-initialized on first use). - haptics: MacHaptics, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + renderer_context: renderer::Context, + headless: bool, + general_pasteboard: Pasteboard, + find_pasteboard: Pasteboard, + reopen: Option>, + on_keyboard_layout_change: Option>, + on_thermal_state_change: Option>, + quit: Option>, + menu_command: Option>, + validate_menu_command: Option bool>>, + will_open_menu: Option>, + menu_actions: Vec>, + open_urls: Option)>>, + finish_launching: Option>, + dock_menu: Option, + menus: Option>, + keyboard_mapper: Rc, + /// Mirrors `[NSCursor setHiddenUntilMouseMoves:]` state, which AppKit doesn't expose. + cursor_visible: Arc, + /// Haptic feedback engine (macOS only, lazy-initialized on first use). + haptics: MacHaptics, } impl MacPlatform { - pub fn new(headless: bool) -> Self { - let dispatcher = Arc::new(MacDispatcher::new()); + pub fn new(headless: bool) -> Self { + let dispatcher = Arc::new(MacDispatcher::new()); - #[cfg(feature = "font-kit")] - let text_system = Arc::new(crate::MacTextSystem::new()); + #[cfg(feature = "font-kit")] + let text_system = Arc::new(crate::MacTextSystem::new()); - #[cfg(not(feature = "font-kit"))] - let text_system = Arc::new(gpui::NoopTextSystem::new()); + #[cfg(not(feature = "font-kit"))] + let text_system = Arc::new(gpui::NoopTextSystem::new()); - let keyboard_layout = MacKeyboardLayout::new(); - let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); + let keyboard_layout = MacKeyboardLayout::new(); + let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { - headless, - text_system, - background_executor: BackgroundExecutor::new(dispatcher.clone()), - foreground_executor: ForegroundExecutor::new(dispatcher), - renderer_context: renderer::Context::default(), - general_pasteboard: Pasteboard::general(), - find_pasteboard: Pasteboard::find(), - reopen: None, - quit: None, - menu_command: None, - validate_menu_command: None, - will_open_menu: None, - menu_actions: Default::default(), - open_urls: None, - finish_launching: None, - dock_menu: None, - on_keyboard_layout_change: None, - on_thermal_state_change: None, - menus: None, - keyboard_mapper, - cursor_visible: Arc::new(AtomicBool::new(true)), - haptics: MacHaptics::new(headless), - })) - } + Self(Mutex::new(MacPlatformState { + headless, + text_system, + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), + renderer_context: renderer::Context::default(), + general_pasteboard: Pasteboard::general(), + find_pasteboard: Pasteboard::find(), + reopen: None, + quit: None, + menu_command: None, + validate_menu_command: None, + will_open_menu: None, + menu_actions: Default::default(), + open_urls: None, + finish_launching: None, + dock_menu: None, + on_keyboard_layout_change: None, + on_thermal_state_change: None, + menus: None, + keyboard_mapper, + cursor_visible: Arc::new(AtomicBool::new(true)), + haptics: MacHaptics::new(headless), + })) + } - unsafe fn create_menu_bar( - &self, - menus: &Vec, - delegate: id, - actions: &mut Vec>, - keymap: &Keymap, - ) -> id { - unsafe { - let application_menu = NSMenu::new(nil).autorelease(); - application_menu.setDelegate_(delegate); + unsafe fn create_menu_bar( + &self, + menus: &Vec, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + unsafe { + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); - for menu_config in menus { - let menu = NSMenu::new(nil).autorelease(); - let menu_title = ns_string(&menu_config.name); - menu.setTitle_(menu_title); - menu.setDelegate_(delegate); + for menu_config in menus { + let menu = NSMenu::new(nil).autorelease(); + let menu_title = ns_string(&menu_config.name); + menu.setTitle_(menu_title); + menu.setDelegate_(delegate); - for item_config in &menu_config.items { - menu.addItem_(Self::create_menu_item( - item_config, - delegate, - actions, - keymap, - )); - } + for item_config in &menu_config.items { + menu.addItem_(Self::create_menu_item( + item_config, + delegate, + actions, + keymap, + )); + } - let menu_item = NSMenuItem::new(nil).autorelease(); - menu_item.setTitle_(menu_title); - menu_item.setSubmenu_(menu); - application_menu.addItem_(menu_item); + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setTitle_(menu_title); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); - if menu_config.name == "Window" { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setWindowsMenu_(menu); - } - } + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } - application_menu - } - } + application_menu + } + } - unsafe fn create_dock_menu( - &self, - menu_items: Vec, - delegate: id, - actions: &mut Vec>, - keymap: &Keymap, - ) -> id { - unsafe { - let dock_menu = NSMenu::new(nil); - dock_menu.setDelegate_(delegate); - for item_config in menu_items { - dock_menu.addItem_(Self::create_menu_item( - &item_config, - delegate, - actions, - keymap, - )); - } + unsafe fn create_dock_menu( + &self, + menu_items: Vec, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + unsafe { + let dock_menu = NSMenu::new(nil); + dock_menu.setDelegate_(delegate); + for item_config in menu_items { + dock_menu.addItem_(Self::create_menu_item( + &item_config, + delegate, + actions, + keymap, + )); + } - dock_menu - } - } + dock_menu + } + } - unsafe fn create_menu_item( - item: &MenuItem, - delegate: id, - actions: &mut Vec>, - keymap: &Keymap, - ) -> id { - static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); + unsafe fn create_menu_item( + item: &MenuItem, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); - unsafe { - match item { - MenuItem::Separator => NSMenuItem::separatorItem(nil), - MenuItem::Action { - name, - action, - os_action, - checked, - disabled, - } => { - // Note that this is intentionally using earlier bindings, whereas typically - // later ones take display precedence. See the discussion on - // https://github.com/zed-industries/zed/issues/23621 - let keystrokes = keymap - .bindings_for_action(action.as_ref()) - .find_or_first(|binding| { - binding.predicate().is_none_or(|predicate| { - predicate.eval(DEFAULT_CONTEXT.get_or_init(|| { - let mut workspace_context = KeyContext::new_with_defaults(); - workspace_context.add("Workspace"); - let mut pane_context = KeyContext::new_with_defaults(); - pane_context.add("Pane"); - let mut editor_context = KeyContext::new_with_defaults(); - editor_context.add("Editor"); + unsafe { + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { + name, + action, + os_action, + checked, + disabled, + } => { + // Note that this is intentionally using earlier bindings, whereas typically + // later ones take display precedence. See the discussion on + // https://github.com/zed-industries/zed/issues/23621 + let keystrokes = keymap + .bindings_for_action(action.as_ref()) + .find_or_first(|binding| { + binding.predicate().is_none_or(|predicate| { + predicate.eval(DEFAULT_CONTEXT.get_or_init(|| { + let mut workspace_context = KeyContext::new_with_defaults(); + workspace_context.add("Workspace"); + let mut pane_context = KeyContext::new_with_defaults(); + pane_context.add("Pane"); + let mut editor_context = KeyContext::new_with_defaults(); + editor_context.add("Editor"); - pane_context.extend(&editor_context); - workspace_context.extend(&pane_context); - vec![workspace_context] - })) - }) - }) - .map(|binding| binding.keystrokes()); + pane_context.extend(&editor_context); + workspace_context.extend(&pane_context); + vec![workspace_context] + })) + }) + }) + .map(|binding| binding.keystrokes()); - let selector = match os_action { - Some(gpui::OsAction::Cut) => selector("cut:"), - Some(gpui::OsAction::Copy) => selector("copy:"), - Some(gpui::OsAction::Paste) => selector("paste:"), - Some(gpui::OsAction::SelectAll) => selector("selectAll:"), - // "undo:" and "redo:" are always disabled in our case, as - // we don't have a NSTextView/NSTextField to enable them on. - Some(gpui::OsAction::Undo) => selector("handleGPUIMenuItem:"), - Some(gpui::OsAction::Redo) => selector("handleGPUIMenuItem:"), - None => selector("handleGPUIMenuItem:"), - }; + let selector = match os_action { + Some(gpui::OsAction::Cut) => selector("cut:"), + Some(gpui::OsAction::Copy) => selector("copy:"), + Some(gpui::OsAction::Paste) => selector("paste:"), + Some(gpui::OsAction::SelectAll) => selector("selectAll:"), + // "undo:" and "redo:" are always disabled in our case, as + // we don't have a NSTextView/NSTextField to enable them on. + Some(gpui::OsAction::Undo) => selector("handleGPUIMenuItem:"), + Some(gpui::OsAction::Redo) => selector("handleGPUIMenuItem:"), + None => selector("handleGPUIMenuItem:"), + }; - let item; - if let Some(keystrokes) = keystrokes { - if keystrokes.len() == 1 { - let keystroke = &keystrokes[0]; - let mut mask = NSEventModifierFlags::empty(); - for (modifier, flag) in &[ - ( - keystroke.modifiers().platform, - NSEventModifierFlags::NSCommandKeyMask, - ), - ( - keystroke.modifiers().control, - NSEventModifierFlags::NSControlKeyMask, - ), - ( - keystroke.modifiers().alt, - NSEventModifierFlags::NSAlternateKeyMask, - ), - ( - keystroke.modifiers().shift, - NSEventModifierFlags::NSShiftKeyMask, - ), - ] { - if *modifier { - mask |= *flag; - } - } + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + ( + keystroke.modifiers().platform, + NSEventModifierFlags::NSCommandKeyMask, + ), + ( + keystroke.modifiers().control, + NSEventModifierFlags::NSControlKeyMask, + ), + ( + keystroke.modifiers().alt, + NSEventModifierFlags::NSAlternateKeyMask, + ), + ( + keystroke.modifiers().shift, + NSEventModifierFlags::NSShiftKeyMask, + ), + ] { + if *modifier { + mask |= *flag; + } + } - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector, - ns_string(key_to_native(keystroke.key()).as_ref()), - ) - .autorelease(); - if Self::os_version() >= Version::new(12, 0, 0) { - let _: () = msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; - } - item.setKeyEquivalentModifierMask_(mask); - } else { - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector, - ns_string(""), - ) - .autorelease(); - } - } else { - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector, - ns_string(""), - ) - .autorelease(); - } + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(key_to_native(keystroke.key()).as_ref()), + ) + .autorelease(); + if Self::os_version() >= Version::new(12, 0, 0) { + let _: () = msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + } + item.setKeyEquivalentModifierMask_(mask); + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } - if *checked { - item.setState_(NSVisualEffectState::Active); - } - item.setEnabled_(if *disabled { NO } else { YES }); + if *checked { + item.setState_(NSVisualEffectState::Active); + } + item.setEnabled_(if *disabled { NO } else { YES }); - let tag = actions.len() as NSInteger; - let _: () = msg_send![item, setTag: tag]; - actions.push(action.boxed_clone()); - item - } - MenuItem::Submenu(Menu { - name, - items, - disabled, - }) => { - let item = NSMenuItem::new(nil).autorelease(); - let submenu = NSMenu::new(nil).autorelease(); - submenu.setDelegate_(delegate); - for item in items { - submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); - } - item.setSubmenu_(submenu); - item.setEnabled_(if *disabled { NO } else { YES }); - item.setTitle_(ns_string(name)); - item - } - MenuItem::SystemMenu(OsMenu { name, menu_type }) => { - let item = NSMenuItem::new(nil).autorelease(); - let submenu = NSMenu::new(nil).autorelease(); - submenu.setDelegate_(delegate); - item.setSubmenu_(submenu); - item.setTitle_(ns_string(name)); + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action.boxed_clone()); + item + } + MenuItem::Submenu(Menu { + name, + items, + disabled, + }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); + } + item.setSubmenu_(submenu); + item.setEnabled_(if *disabled { NO } else { YES }); + item.setTitle_(ns_string(name)); + item + } + MenuItem::SystemMenu(OsMenu { name, menu_type }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); - match menu_type { - SystemMenuType::Services => { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setServicesMenu_(item); - } - } + match menu_type { + SystemMenuType::Services => { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setServicesMenu_(item); + } + } - item - } - } - } - } + item + } + } + } + } - fn os_version() -> Version { - let version = unsafe { - let process_info = NSProcessInfo::processInfo(nil); - process_info.operatingSystemVersion() - }; - Version::new( - version.majorVersion, - version.minorVersion, - version.patchVersion, - ) - } + fn os_version() -> Version { + let version = unsafe { + let process_info = NSProcessInfo::processInfo(nil); + process_info.operatingSystemVersion() + }; + Version::new( + version.majorVersion, + version.minorVersion, + version.patchVersion, + ) + } } impl Platform for MacPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.0.lock().background_executor.clone() - } + fn background_executor(&self) -> BackgroundExecutor { + self.0.lock().background_executor.clone() + } - fn foreground_executor(&self) -> gpui::ForegroundExecutor { - self.0.lock().foreground_executor.clone() - } + fn foreground_executor(&self) -> gpui::ForegroundExecutor { + self.0.lock().foreground_executor.clone() + } - fn text_system(&self) -> Arc { - self.0.lock().text_system.clone() - } + fn text_system(&self) -> Arc { + self.0.lock().text_system.clone() + } - fn run(&self, on_finish_launching: Box) { - let mut state = self.0.lock(); - if state.headless { - drop(state); - on_finish_launching(); - unsafe { CFRunLoopRun() }; - } else { - state.finish_launching = Some(on_finish_launching); - drop(state); - } + fn run(&self, on_finish_launching: Box) { + let mut state = self.0.lock(); + if state.headless { + drop(state); + on_finish_launching(); + unsafe { CFRunLoopRun() }; + } else { + state.finish_launching = Some(on_finish_launching); + drop(state); + } - unsafe { - let app: id = msg_send![APP_CLASS, sharedApplication]; - let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; - app.setDelegate_(app_delegate); + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; + app.setDelegate_(app_delegate); - let self_ptr = self as *const Self as *const c_void; - (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); - (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + let self_ptr = self as *const Self as *const c_void; + (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); - let pool = NSAutoreleasePool::new(nil); - app.run(); - pool.drain(); + let pool = NSAutoreleasePool::new(nil); + app.run(); + pool.drain(); - (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); - (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); - } - } + (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + } + } - fn quit(&self) { - // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks - // synchronously before this method terminates. If we call `Platform::quit` while holding a - // borrow of the app state (which most of the time we will do), we will end up - // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve - // this, we make quitting the application asynchronous so that we aren't holding borrows to - // the app state on the stack when we actually terminate the app. + fn quit(&self) { + // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks + // synchronously before this method terminates. If we call `Platform::quit` while holding a + // borrow of the app state (which most of the time we will do), we will end up + // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve + // this, we make quitting the application asynchronous so that we aren't holding borrows to + // the app state on the stack when we actually terminate the app. - unsafe { - DispatchQueue::main().exec_async_f(ptr::null_mut(), quit); - } + unsafe { + DispatchQueue::main().exec_async_f(ptr::null_mut(), quit); + } - extern "C" fn quit(_: *mut c_void) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, terminate: nil]; - } - } - } + extern "C" fn quit(_: *mut c_void) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, terminate: nil]; + } + } + } - fn restart(&self, binary_path: Option) { - use std::os::unix::process::CommandExt as _; + fn restart(&self, binary_path: Option) { + use std::os::unix::process::CommandExt as _; - let app_pid = std::process::id().to_string(); - let app_path = binary_path - .or_else(|| { - self.app_path() - .ok() - // When the app is not bundled, `app_path` returns the - // directory containing the executable. Disregard this - // and get the path to the executable itself. - .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) - }) - .unwrap_or_else(|| std::env::current_exe().unwrap()); + let app_pid = std::process::id().to_string(); + let app_path = binary_path + .or_else(|| { + self.app_path() + .ok() + // When the app is not bundled, `app_path` returns the + // directory containing the executable. Disregard this + // and get the path to the executable itself. + .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) + }) + .unwrap_or_else(|| std::env::current_exe().unwrap()); - // Wait until this process has exited and then re-open this path. - let script = r#" + // Wait until this process has exited and then re-open this path. + let script = r#" while kill -0 $0 2> /dev/null; do sleep 0.1 done open "$1" "#; - #[allow( - clippy::disallowed_methods, - reason = "We are restarting ourselves, using std command thus is fine" - )] - let restart_process = new_std_command("/bin/bash") - .arg("-c") - .arg(script) - .arg(app_pid) - .arg(app_path) - .process_group(0) - .spawn(); - - match restart_process { - Ok(_) => self.quit(), - Err(e) => log::error!("failed to spawn restart script: {:?}", e), - } - } - - fn activate(&self, ignoring_other_apps: bool) { - unsafe { - let app = NSApplication::sharedApplication(nil); - app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); - } - } - - fn hide(&self) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, hide: nil]; - } - } - - fn hide_other_apps(&self) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, hideOtherApplications: nil]; - } - } - - fn unhide_other_apps(&self) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, unhideAllApplications: nil]; - } - } - - fn primary_display(&self) -> Option> { - Some(Rc::new(MacDisplay::primary())) - } - - fn displays(&self) -> Vec> { - MacDisplay::all() - .map(|screen| Rc::new(screen) as Rc<_>) - .collect() - } - - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - let min_version = cocoa::foundation::NSOperatingSystemVersion::new(12, 3, 0); - crate::is_macos_version_at_least(min_version) - } - - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - crate::screen_capture::get_sources() - } - - fn active_window(&self) -> Option { - MacWindow::active_window() - } - - // Returns the windows ordered front-to-back, meaning that the active - // window is the first one in the returned vec. - fn window_stack(&self) -> Option> { - Some(MacWindow::ordered_windows()) - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> Result> { - let (cursor_visible, foreground_executor, background_executor, renderer_context) = { - let guard = self.0.lock(); - ( - guard.cursor_visible.clone(), - guard.foreground_executor.clone(), - guard.background_executor.clone(), - guard.renderer_context.clone(), - ) - }; - - Ok(Box::new(MacWindow::open( - handle, - options, - cursor_visible, - foreground_executor, - background_executor, - renderer_context, - ))) - } - - fn window_appearance(&self) -> WindowAppearance { - unsafe { - let app = NSApplication::sharedApplication(nil); - let appearance: id = msg_send![app, effectiveAppearance]; - crate::window_appearance::window_appearance_from_native(appearance) - } - } - - fn open_url(&self, url: &str) { - unsafe { - let ns_url = NSURL::alloc(nil).initWithString_(ns_string(url)); - if ns_url.is_null() { - log::error!("Failed to create NSURL from string: {}", url); - return; - } - let url = ns_url.autorelease(); - let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; - msg_send![workspace, openURL: url] - } - } - - fn register_url_scheme(&self, scheme: &str) -> Task> { - // API only available post Monterey - // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl - let (done_tx, done_rx) = oneshot::channel(); - if Self::os_version() < Version::new(12, 0, 0) { - return Task::ready(Err(anyhow!( - "macOS 12.0 or later is required to register URL schemes" - ))); - } - - let bundle_id = unsafe { - let bundle: id = msg_send![class!(NSBundle), mainBundle]; - let bundle_id: id = msg_send![bundle, bundleIdentifier]; - if bundle_id == nil { - return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); - } - bundle_id - }; - - unsafe { - let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; - let scheme: id = ns_string(scheme); - let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; - if app == nil { - return Task::ready(Err(anyhow!( - "Cannot register URL scheme until app is installed" - ))); - } - let done_tx = Cell::new(Some(done_tx)); - let block = ConcreteBlock::new(move |error: id| { - let result = if error == nil { - Ok(()) - } else { - let msg: id = msg_send![error, localizedDescription]; - Err(anyhow!("Failed to register: {msg:?}")) - }; - - if let Some(done_tx) = done_tx.take() { - let _ = done_tx.send(result); - } - }); - let block = block.copy(); - let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; - } - - self.background_executor() - .spawn(async { done_rx.await.map_err(|e| anyhow!(e))? }) - } - - fn on_open_urls(&self, callback: Box)>) { - self.0.lock().open_urls = Some(callback); - } - - fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> oneshot::Receiver>>> { - let (done_tx, done_rx) = oneshot::channel(); - self.foreground_executor() - .spawn(async move { - unsafe { - let panel = NSOpenPanel::openPanel(nil); - panel.setCanChooseDirectories_(options.directories.to_objc()); - panel.setCanChooseFiles_(options.files.to_objc()); - panel.setAllowsMultipleSelection_(options.multiple.to_objc()); - - panel.setCanCreateDirectories(true.to_objc()); - panel.setResolvesAliases_(false.to_objc()); - let done_tx = Cell::new(Some(done_tx)); - let block = ConcreteBlock::new(move |response: NSModalResponse| { - let result = if response == NSModalResponse::NSModalResponseOk { - let mut result = Vec::new(); - let urls = panel.URLs(); - for i in 0..urls.count() { - let url = urls.objectAtIndex(i); - if url.isFileURL() == YES - && let Ok(path) = ns_url_to_path(url) - { - result.push(path) - } - } - Some(result) - } else { - None - }; - - if let Some(done_tx) = done_tx.take() { - let _ = done_tx.send(Ok(result)); - } - }); - let block = block.copy(); - - if let Some(prompt) = options.prompt { - let _: () = msg_send![panel, setPrompt: ns_string(&prompt)]; - } - - let _: () = msg_send![panel, beginWithCompletionHandler: block]; - } - }) - .detach(); - done_rx - } - - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let directory = directory.to_owned(); - let suggested_name = suggested_name.map(|s| s.to_owned()); - let (done_tx, done_rx) = oneshot::channel(); - self.foreground_executor() - .spawn(async move { - unsafe { - let panel = NSSavePanel::savePanel(nil); - let path = ns_string(directory.to_string_lossy().as_ref()); - let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); - panel.setDirectoryURL(url); - - if let Some(suggested_name) = suggested_name { - let name_string = ns_string(&suggested_name); - let _: () = msg_send![panel, setNameFieldStringValue: name_string]; - } - - let done_tx = Cell::new(Some(done_tx)); - let block = ConcreteBlock::new(move |response: NSModalResponse| { - let mut result = None; - if response == NSModalResponse::NSModalResponseOk { - let url = panel.URL(); - if url.isFileURL() == YES { - result = ns_url_to_path(panel.URL()).ok().map(|mut result| { - let Some(filename) = result.file_name() else { - return result; - }; - let chunks = filename - .as_bytes() - .split(|&b| b == b'.') - .collect::>(); - - // https://github.com/zed-industries/zed/issues/16969 - // Workaround a bug in macOS Sequoia that adds an extra file-extension - // sometimes. e.g. `a.sql` becomes `a.sql.s` or `a.txtx` becomes `a.txtx.txt` - // - // This is conditional on OS version because I'd like to get rid of it, so that - // you can manually create a file called `a.sql.s`. That said it seems better - // to break that use-case than breaking `a.sql`. - if chunks.len() == 3 - && chunks[1].starts_with(chunks[2]) - && Self::os_version() >= Version::new(15, 0, 0) - { - let new_filename = OsStr::from_bytes( - &filename.as_bytes() - [..chunks[0].len() + 1 + chunks[1].len()], - ) - .to_owned(); - result.set_file_name(&new_filename); - } - result - }) - } - } - - if let Some(done_tx) = done_tx.take() { - let _ = done_tx.send(Ok(result)); - } - }); - let block = block.copy(); - let _: () = msg_send![panel, beginWithCompletionHandler: block]; - } - }) - .detach(); - - done_rx - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - true - } - - fn reveal_path(&self, path: &Path) { - unsafe { - let path = path.to_path_buf(); - self.0 - .lock() - .background_executor - .spawn(async move { - let full_path = ns_string(path.to_str().unwrap_or("")); - let root_full_path = ns_string(""); - let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; - let _: BOOL = msg_send![ - workspace, - selectFile: full_path - inFileViewerRootedAtPath: root_full_path - ]; - }) - .detach(); - } - } - - fn open_with_system(&self, path: &Path) { - let path = path.to_owned(); - self.0 - .lock() - .background_executor - .spawn(async move { - if let Some(mut child) = new_command("open") - .arg("--") - .arg(path) - .spawn() - .context("invoking open command") - .log_err() - { - child.status().await.log_err(); - } - }) - .detach(); - } - - fn on_quit(&self, callback: Box) { - self.0.lock().quit = Some(callback); - } - - fn on_reopen(&self, callback: Box) { - self.0.lock().reopen = Some(callback); - } - - fn on_keyboard_layout_change(&self, callback: Box) { - self.0.lock().on_keyboard_layout_change = Some(callback); - } - - fn on_app_menu_action(&self, callback: Box) { - self.0.lock().menu_command = Some(callback); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.0.lock().will_open_menu = Some(callback); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.0.lock().validate_menu_command = Some(callback); - } - - fn on_thermal_state_change(&self, callback: Box) { - self.0.lock().on_thermal_state_change = Some(callback); - } - - fn thermal_state(&self) -> ThermalState { - unsafe { - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - let state: NSInteger = msg_send![process_info, thermalState]; - match state { - 0 => ThermalState::Nominal, - 1 => ThermalState::Fair, - 2 => ThermalState::Serious, - 3 => ThermalState::Critical, - _ => ThermalState::Nominal, - } - } - } - - fn keyboard_layout(&self) -> Box { - Box::new(MacKeyboardLayout::new()) - } - - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - - fn app_path(&self) -> Result { - unsafe { - let bundle: id = NSBundle::mainBundle(); - anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); - Ok(path_from_objc(msg_send![bundle, bundlePath])) - } - } - - fn set_menus(&self, menus: Vec, keymap: &Keymap) { - unsafe { - let app: id = msg_send![APP_CLASS, sharedApplication]; - let mut state = self.0.lock(); - let actions = &mut state.menu_actions; - let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap); - drop(state); - app.setMainMenu_(menu); - } - self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect()); - } - - fn get_menus(&self) -> Option> { - self.0.lock().menus.clone() - } - - fn set_dock_menu(&self, menu: Vec, keymap: &Keymap) { - unsafe { - let app: id = msg_send![APP_CLASS, sharedApplication]; - let mut state = self.0.lock(); - let actions = &mut state.menu_actions; - let new = self.create_dock_menu(menu, NSWindow::delegate(app), actions, keymap); - if let Some(old) = state.dock_menu.replace(new) { - CFRelease(old as _) - } - } - } - - fn add_recent_document(&self, path: &Path) { - if let Some(path_str) = path.to_str() { - unsafe { - let document_controller: id = - msg_send![class!(NSDocumentController), sharedDocumentController]; - let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str)); - let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url]; - } - } - } - - fn path_for_auxiliary_executable(&self, name: &str) -> Result { - unsafe { - let bundle: id = NSBundle::mainBundle(); - anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); - let name = ns_string(name); - let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; - anyhow::ensure!(!url.is_null(), "resource not found"); - ns_url_to_path(url) - } - } - - /// Match cursor style to one of the styles available - /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). - fn set_cursor_style(&self, style: CursorStyle) { - unsafe { - set_active_window_cursor_style(style); - } - } - - fn hide_cursor_until_mouse_moves(&self) { - let cursor_visible = self.0.lock().cursor_visible.clone(); - if !cursor_visible.swap(false, Ordering::Relaxed) { - return; - } - unsafe { - let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves: YES]; - } - } - - fn is_cursor_visible(&self) -> bool { - self.0.lock().cursor_visible.load(Ordering::Relaxed) - } - - fn should_auto_hide_scrollbars(&self) -> bool { - #[allow(non_upper_case_globals)] - const NSScrollerStyleOverlay: NSInteger = 1; - - unsafe { - let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; - style == NSScrollerStyleOverlay - } - } - - fn read_from_clipboard(&self) -> Option { - let state = self.0.lock(); - state.general_pasteboard.read() - } - - fn write_to_clipboard(&self, item: ClipboardItem) { - let state = self.0.lock(); - state.general_pasteboard.write(item); - } - - fn read_from_find_pasteboard(&self) -> Option { - let state = self.0.lock(); - state.find_pasteboard.read() - } - - fn write_to_find_pasteboard(&self, item: ClipboardItem) { - let state = self.0.lock(); - state.find_pasteboard.write(item); - } - - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { - let url = url.to_string(); - let username = username.to_string(); - let password = password.to_vec(); - self.background_executor().spawn(async move { - unsafe { - use security::*; - - let url = CFString::from(url.as_str()); - let username = CFString::from(username.as_str()); - let password = CFData::from_buffer(&password); - - // First, check if there are already credentials for the given server. If so, then - // update the username and password. - let mut verb = "updating"; - let mut query_attrs = CFMutableDictionary::with_capacity(2); - query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); - query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - - let mut attrs = CFMutableDictionary::with_capacity(4); - attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); - attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); - attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); - - let mut status = SecItemUpdate( - query_attrs.as_concrete_TypeRef(), - attrs.as_concrete_TypeRef(), - ); - - // If there were no existing credentials for the given server, then create them. - if status == errSecItemNotFound { - verb = "creating"; - status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); - } - anyhow::ensure!(status == errSecSuccess, "{verb} password failed: {status}"); - } - Ok(()) - }) - } - - fn read_credentials(&self, url: &str) -> Task)>>> { - let url = url.to_string(); - self.background_executor().spawn(async move { - let url = CFString::from(url.as_str()); - let cf_true = CFBoolean::true_value().as_CFTypeRef(); - - unsafe { - use security::*; - - // Find any credentials for the given server URL. - let mut attrs = CFMutableDictionary::with_capacity(5); - attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); - attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - attrs.set(kSecReturnAttributes as *const _, cf_true); - attrs.set(kSecReturnData as *const _, cf_true); - - let mut result = CFTypeRef::from(ptr::null()); - let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); - match status { - security::errSecSuccess => {} - security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), - _ => anyhow::bail!("reading password failed: {status}"), - } - - let result = CFType::wrap_under_create_rule(result) - .downcast::() - .context("keychain item was not a dictionary")?; - let username = result - .find(kSecAttrAccount as *const _) - .context("account was missing from keychain item")?; - let username = CFType::wrap_under_get_rule(*username) - .downcast::() - .context("account was not a string")?; - let password = result - .find(kSecValueData as *const _) - .context("password was missing from keychain item")?; - let password = CFType::wrap_under_get_rule(*password) - .downcast::() - .context("password was not a string")?; - - Ok(Some((username.to_string(), password.bytes().to_vec()))) - } - }) - } - - fn delete_credentials(&self, url: &str) -> Task> { - let url = url.to_string(); - - self.background_executor().spawn(async move { - unsafe { - use security::*; - - let url = CFString::from(url.as_str()); - let mut query_attrs = CFMutableDictionary::with_capacity(2); - query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); - query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - - let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); - anyhow::ensure!(status == errSecSuccess, "delete password failed: {status}"); - } - Ok(()) - }) - } - - fn supports_haptic_feedback(&self) -> bool { - self.0.lock().haptics.supported() - } - - fn play_haptic_feedback(&self, style: gpui::HapticFeedbackStyle) { - self.0.lock().haptics.play(style) - } + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = new_std_command("/bin/bash") + .arg("-c") + .arg(script) + .arg(app_pid) + .arg(app_path) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, ignoring_other_apps: bool) { + unsafe { + let app = NSApplication::sharedApplication(nil); + app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); + } + } + + fn hide(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hide: nil]; + } + } + + fn hide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hideOtherApplications: nil]; + } + } + + fn unhide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, unhideAllApplications: nil]; + } + } + + fn primary_display(&self) -> Option> { + Some(Rc::new(MacDisplay::primary())) + } + + fn displays(&self) -> Vec> { + MacDisplay::all() + .map(|screen| Rc::new(screen) as Rc<_>) + .collect() + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + let min_version = cocoa::foundation::NSOperatingSystemVersion::new(12, 3, 0); + crate::is_macos_version_at_least(min_version) + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + crate::screen_capture::get_sources() + } + + fn active_window(&self) -> Option { + MacWindow::active_window() + } + + // Returns the windows ordered front-to-back, meaning that the active + // window is the first one in the returned vec. + fn window_stack(&self) -> Option> { + Some(MacWindow::ordered_windows()) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Result> { + let (cursor_visible, foreground_executor, background_executor, renderer_context) = { + let guard = self.0.lock(); + ( + guard.cursor_visible.clone(), + guard.foreground_executor.clone(), + guard.background_executor.clone(), + guard.renderer_context.clone(), + ) + }; + + Ok(Box::new(MacWindow::open( + handle, + options, + cursor_visible, + foreground_executor, + background_executor, + renderer_context, + ))) + } + + fn window_appearance(&self) -> WindowAppearance { + unsafe { + let app = NSApplication::sharedApplication(nil); + let appearance: id = msg_send![app, effectiveAppearance]; + crate::window_appearance::window_appearance_from_native(appearance) + } + } + + fn open_url(&self, url: &str) { + unsafe { + let ns_url = NSURL::alloc(nil).initWithString_(ns_string(url)); + if ns_url.is_null() { + log::error!("Failed to create NSURL from string: {}", url); + return; + } + let url = ns_url.autorelease(); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + msg_send![workspace, openURL: url] + } + } + + fn register_url_scheme(&self, scheme: &str) -> Task> { + // API only available post Monterey + // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl + let (done_tx, done_rx) = oneshot::channel(); + if Self::os_version() < Version::new(12, 0, 0) { + return Task::ready(Err(anyhow!( + "macOS 12.0 or later is required to register URL schemes" + ))); + } + + let bundle_id = unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id == nil { + return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); + } + bundle_id + }; + + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let scheme: id = ns_string(scheme); + let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; + if app == nil { + return Task::ready(Err(anyhow!( + "Cannot register URL scheme until app is installed" + ))); + } + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |error: id| { + let result = if error == nil { + Ok(()) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {msg:?}")) + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; + } + + self.background_executor() + .spawn(async { done_rx.await.map_err(|e| anyhow!(e))? }) + } + + fn on_open_urls(&self, callback: Box)>) { + self.0.lock().open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSOpenPanel::openPanel(nil); + panel.setCanChooseDirectories_(options.directories.to_objc()); + panel.setCanChooseFiles_(options.files.to_objc()); + panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + + panel.setCanCreateDirectories(true.to_objc()); + panel.setResolvesAliases_(false.to_objc()); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + if url.isFileURL() == YES + && let Ok(path) = ns_url_to_path(url) + { + result.push(path) + } + } + Some(result) + } else { + None + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(Ok(result)); + } + }); + let block = block.copy(); + + if let Some(prompt) = options.prompt { + let _: () = msg_send![panel, setPrompt: ns_string(&prompt)]; + } + + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + done_rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSSavePanel::savePanel(nil); + let path = ns_string(directory.to_string_lossy().as_ref()); + let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); + panel.setDirectoryURL(url); + + if let Some(suggested_name) = suggested_name { + let name_string = ns_string(&suggested_name); + let _: () = msg_send![panel, setNameFieldStringValue: name_string]; + } + + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let mut result = None; + if response == NSModalResponse::NSModalResponseOk { + let url = panel.URL(); + if url.isFileURL() == YES { + result = ns_url_to_path(panel.URL()).ok().map(|mut result| { + let Some(filename) = result.file_name() else { + return result; + }; + let chunks = filename + .as_bytes() + .split(|&b| b == b'.') + .collect::>(); + + // https://github.com/zed-industries/zed/issues/16969 + // Workaround a bug in macOS Sequoia that adds an extra file-extension + // sometimes. e.g. `a.sql` becomes `a.sql.s` or `a.txtx` becomes `a.txtx.txt` + // + // This is conditional on OS version because I'd like to get rid of it, so that + // you can manually create a file called `a.sql.s`. That said it seems better + // to break that use-case than breaking `a.sql`. + if chunks.len() == 3 + && chunks[1].starts_with(chunks[2]) + && Self::os_version() >= Version::new(15, 0, 0) + { + let new_filename = OsStr::from_bytes( + &filename.as_bytes() + [..chunks[0].len() + 1 + chunks[1].len()], + ) + .to_owned(); + result.set_file_name(&new_filename); + } + result + }) + } + } + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(Ok(result)); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + + done_rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + + fn reveal_path(&self, path: &Path) { + unsafe { + let path = path.to_path_buf(); + self.0 + .lock() + .background_executor + .spawn(async move { + let full_path = ns_string(path.to_str().unwrap_or("")); + let root_full_path = ns_string(""); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let _: BOOL = msg_send![ + workspace, + selectFile: full_path + inFileViewerRootedAtPath: root_full_path + ]; + }) + .detach(); + } + } + + fn open_with_system(&self, path: &Path) { + let path = path.to_owned(); + self.0 + .lock() + .background_executor + .spawn(async move { + if let Some(mut child) = new_command("open") + .arg("--") + .arg(path) + .spawn() + .context("invoking open command") + .log_err() + { + child.status().await.log_err(); + } + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.0.lock().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.0.lock().reopen = Some(callback); + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.0.lock().on_keyboard_layout_change = Some(callback); + } + + fn on_app_menu_action(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } + + fn on_thermal_state_change(&self, callback: Box) { + self.0.lock().on_thermal_state_change = Some(callback); + } + + fn thermal_state(&self) -> ThermalState { + unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let state: NSInteger = msg_send![process_info, thermalState]; + match state { + 0 => ThermalState::Nominal, + 1 => ThermalState::Fair, + 2 => ThermalState::Serious, + 3 => ThermalState::Critical, + _ => ThermalState::Nominal, + } + } + } + + fn keyboard_layout(&self) -> Box { + Box::new(MacKeyboardLayout::new()) + } + + fn keyboard_mapper(&self) -> Rc { + self.0.lock().keyboard_mapper.clone() + } + + fn app_path(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); + Ok(path_from_objc(msg_send![bundle, bundlePath])) + } + } + + fn set_menus(&self, menus: Vec, keymap: &Keymap) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap); + drop(state); + app.setMainMenu_(menu); + } + self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect()); + } + + fn get_menus(&self) -> Option> { + self.0.lock().menus.clone() + } + + fn set_dock_menu(&self, menu: Vec, keymap: &Keymap) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + let new = self.create_dock_menu(menu, NSWindow::delegate(app), actions, keymap); + if let Some(old) = state.dock_menu.replace(new) { + CFRelease(old as _) + } + } + } + + fn add_recent_document(&self, path: &Path) { + if let Some(path_str) = path.to_str() { + unsafe { + let document_controller: id = + msg_send![class!(NSDocumentController), sharedDocumentController]; + let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str)); + let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url]; + } + } + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); + let name = ns_string(name); + let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; + anyhow::ensure!(!url.is_null(), "resource not found"); + ns_url_to_path(url) + } + } + + /// Match cursor style to one of the styles available + /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + set_active_window_cursor_style(style); + } + } + + fn hide_cursor_until_mouse_moves(&self) { + let cursor_visible = self.0.lock().cursor_visible.clone(); + if !cursor_visible.swap(false, Ordering::Relaxed) { + return; + } + unsafe { + let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves: YES]; + } + } + + fn is_cursor_visible(&self) -> bool { + self.0.lock().cursor_visible.load(Ordering::Relaxed) + } + + fn should_auto_hide_scrollbars(&self) -> bool { + #[allow(non_upper_case_globals)] + const NSScrollerStyleOverlay: NSInteger = 1; + + unsafe { + let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; + style == NSScrollerStyleOverlay + } + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.lock(); + state.general_pasteboard.read() + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + let state = self.0.lock(); + state.general_pasteboard.write(item); + } + + fn read_from_find_pasteboard(&self) -> Option { + let state = self.0.lock(); + state.find_pasteboard.read() + } + + fn write_to_find_pasteboard(&self, item: ClipboardItem) { + let state = self.0.lock(); + state.find_pasteboard.write(item); + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let username = CFString::from(username.as_str()); + let password = CFData::from_buffer(&password); + + // First, check if there are already credentials for the given server. If so, then + // update the username and password. + let mut verb = "updating"; + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let mut attrs = CFMutableDictionary::with_capacity(4); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); + attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); + + let mut status = SecItemUpdate( + query_attrs.as_concrete_TypeRef(), + attrs.as_concrete_TypeRef(), + ); + + // If there were no existing credentials for the given server, then create them. + if status == errSecItemNotFound { + verb = "creating"; + status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); + } + anyhow::ensure!(status == errSecSuccess, "{verb} password failed: {status}"); + } + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let url = CFString::from(url.as_str()); + let cf_true = CFBoolean::true_value().as_CFTypeRef(); + + unsafe { + use security::*; + + // Find any credentials for the given server URL. + let mut attrs = CFMutableDictionary::with_capacity(5); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecReturnAttributes as *const _, cf_true); + attrs.set(kSecReturnData as *const _, cf_true); + + let mut result = CFTypeRef::from(ptr::null()); + let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); + match status { + security::errSecSuccess => {} + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => anyhow::bail!("reading password failed: {status}"), + } + + let result = CFType::wrap_under_create_rule(result) + .downcast::() + .context("keychain item was not a dictionary")?; + let username = result + .find(kSecAttrAccount as *const _) + .context("account was missing from keychain item")?; + let username = CFType::wrap_under_get_rule(*username) + .downcast::() + .context("account was not a string")?; + let password = result + .find(kSecValueData as *const _) + .context("password was missing from keychain item")?; + let password = CFType::wrap_under_get_rule(*password) + .downcast::() + .context("password was not a string")?; + + Ok(Some((username.to_string(), password.bytes().to_vec()))) + } + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + anyhow::ensure!(status == errSecSuccess, "delete password failed: {status}"); + } + Ok(()) + }) + } + + fn supports_haptic_feedback(&self) -> bool { + self.0.lock().haptics.supported() + } + + fn play_haptic_feedback(&self, style: gpui::HapticFeedbackStyle) { + self.0.lock().haptics.play(style) + } } unsafe fn path_from_objc(path: id) -> PathBuf { - let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; - let bytes = unsafe { path.UTF8String() as *const u8 }; - let path = str::from_utf8(unsafe { slice::from_raw_parts(bytes, len) }).unwrap(); - PathBuf::from(path) + let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = unsafe { path.UTF8String() as *const u8 }; + let path = str::from_utf8(unsafe { slice::from_raw_parts(bytes, len) }).unwrap(); + PathBuf::from(path) } unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { - unsafe { - let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); - assert!(!platform_ptr.is_null()); - &*(platform_ptr as *const MacPlatform) - } + unsafe { + let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); + assert!(!platform_ptr.is_null()); + &*(platform_ptr as *const MacPlatform) + } } extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) { - unsafe { - let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; + unsafe { + let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; - // The autofill heuristic controller causes slowdown and high CPU usage. - // We don't know exactly why. This disables the full heuristic controller. - // - // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625 - let name = ns_string("NSAutoFillHeuristicControllerEnabled"); - let existing_value: id = msg_send![user_defaults, objectForKey: name]; - if existing_value == nil { - let false_value: id = msg_send![class!(NSNumber), numberWithBool:false]; - let _: () = msg_send![user_defaults, setObject: false_value forKey: name]; - } - } + // The autofill heuristic controller causes slowdown and high CPU usage. + // We don't know exactly why. This disables the full heuristic controller. + // + // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625 + let name = ns_string("NSAutoFillHeuristicControllerEnabled"); + let existing_value: id = msg_send![user_defaults, objectForKey: name]; + if existing_value == nil { + let false_value: id = msg_send![class!(NSNumber), numberWithBool:false]; + let _: () = msg_send![user_defaults, setObject: false_value forKey: name]; + } + } } extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { - unsafe { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - let notification_center: *mut Object = - msg_send![class!(NSNotificationCenter), defaultCenter]; - let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification"); - let _: () = msg_send![notification_center, addObserver: this as id - selector: sel!(onKeyboardLayoutChange:) - name: name - object: nil - ]; + let notification_center: *mut Object = + msg_send![class!(NSNotificationCenter), defaultCenter]; + let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification"); + let _: () = msg_send![notification_center, addObserver: this as id + selector: sel!(onKeyboardLayoutChange:) + name: name + object: nil + ]; - let thermal_name = ns_string("NSProcessInfoThermalStateDidChangeNotification"); - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - let _: () = msg_send![notification_center, addObserver: this as id - selector: sel!(onThermalStateChange:) - name: thermal_name - object: process_info - ]; + let thermal_name = ns_string("NSProcessInfoThermalStateDidChangeNotification"); + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let _: () = msg_send![notification_center, addObserver: this as id + selector: sel!(onThermalStateChange:) + name: thermal_name + object: process_info + ]; - let platform = get_mac_platform(this); - let callback = platform.0.lock().finish_launching.take(); - if let Some(callback) = callback { - callback(); - } - } + let platform = get_mac_platform(this); + let callback = platform.0.lock().finish_launching.take(); + if let Some(callback) = callback { + callback(); + } + } } extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { - if !has_open_windows { - let platform = unsafe { get_mac_platform(this) }; - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.reopen.take() { - drop(lock); - callback(); - platform.0.lock().reopen.get_or_insert(callback); - } - } + if !has_open_windows { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.reopen.take() { + drop(lock); + callback(); + platform.0.lock().reopen.get_or_insert(callback); + } + } } extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_mac_platform(this) }; - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.quit.take() { - drop(lock); - callback(); - platform.0.lock().quit.get_or_insert(callback); - } + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.quit.take() { + drop(lock); + callback(); + platform.0.lock().quit.get_or_insert(callback); + } } extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_mac_platform(this) }; - let mut lock = platform.0.lock(); - let keyboard_layout = MacKeyboardLayout::new(); - lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - if let Some(mut callback) = lock.on_keyboard_layout_change.take() { - drop(lock); - callback(); - platform - .0 - .lock() - .on_keyboard_layout_change - .get_or_insert(callback); - } + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + let keyboard_layout = MacKeyboardLayout::new(); + lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); + if let Some(mut callback) = lock.on_keyboard_layout_change.take() { + drop(lock); + callback(); + platform + .0 + .lock() + .on_keyboard_layout_change + .get_or_insert(callback); + } } extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) { - // Defer to the next run loop iteration to avoid re-entrant borrows of the App RefCell, - // as NSNotificationCenter delivers this notification synchronously and it may fire while - // the App is already borrowed (same pattern as quit() above). - let platform = unsafe { get_mac_platform(this) }; - let platform_ptr = platform as *const MacPlatform as *mut c_void; - unsafe { - DispatchQueue::main().exec_async_f(platform_ptr, on_thermal_state_change); - } + // Defer to the next run loop iteration to avoid re-entrant borrows of the App RefCell, + // as NSNotificationCenter delivers this notification synchronously and it may fire while + // the App is already borrowed (same pattern as quit() above). + let platform = unsafe { get_mac_platform(this) }; + let platform_ptr = platform as *const MacPlatform as *mut c_void; + unsafe { + DispatchQueue::main().exec_async_f(platform_ptr, on_thermal_state_change); + } - extern "C" fn on_thermal_state_change(context: *mut c_void) { - let platform = unsafe { &*(context as *const MacPlatform) }; - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.on_thermal_state_change.take() { - drop(lock); - callback(); - platform - .0 - .lock() - .on_thermal_state_change - .get_or_insert(callback); - } - } + extern "C" fn on_thermal_state_change(context: *mut c_void) { + let platform = unsafe { &*(context as *const MacPlatform) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.on_thermal_state_change.take() { + drop(lock); + callback(); + platform + .0 + .lock() + .on_thermal_state_change + .get_or_insert(callback); + } + } } extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { - let urls = unsafe { - (0..urls.count()) - .filter_map(|i| { - let url = urls.objectAtIndex(i); - match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { - Ok(string) => Some(string.to_string()), - Err(err) => { - log::error!("error converting path to string: {}", err); - None - } - } - }) - .collect::>() - }; - let platform = unsafe { get_mac_platform(this) }; - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.open_urls.take() { - drop(lock); - callback(urls); - platform.0.lock().open_urls.get_or_insert(callback); - } + let urls = unsafe { + (0..urls.count()) + .filter_map(|i| { + let url = urls.objectAtIndex(i); + match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { + Ok(string) => Some(string.to_string()), + Err(err) => { + log::error!("error converting path to string: {}", err); + None + } + } + }) + .collect::>() + }; + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.open_urls.take() { + drop(lock); + callback(urls); + platform.0.lock().open_urls.get_or_insert(callback); + } } extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { - unsafe { - let platform = get_mac_platform(this); - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.menu_command.take() { - let tag: NSInteger = msg_send![item, tag]; - let index = tag as usize; - if let Some(action) = lock.menu_actions.get(index) { - let action = action.boxed_clone(); - drop(lock); - callback(&*action); - } - platform.0.lock().menu_command.get_or_insert(callback); - } - } + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + callback(&*action); + } + platform.0.lock().menu_command.get_or_insert(callback); + } + } } extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { - unsafe { - let mut result = false; - let platform = get_mac_platform(this); - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.validate_menu_command.take() { - let tag: NSInteger = msg_send![item, tag]; - let index = tag as usize; - if let Some(action) = lock.menu_actions.get(index) { - let action = action.boxed_clone(); - drop(lock); - result = callback(action.as_ref()); - } - platform - .0 - .lock() - .validate_menu_command - .get_or_insert(callback); - } - result - } + unsafe { + let mut result = false; + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.validate_menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + result = callback(action.as_ref()); + } + platform + .0 + .lock() + .validate_menu_command + .get_or_insert(callback); + } + result + } } extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { - unsafe { - let platform = get_mac_platform(this); - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.will_open_menu.take() { - drop(lock); - callback(); - platform.0.lock().will_open_menu.get_or_insert(callback); - } - } + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.will_open_menu.take() { + drop(lock); + callback(); + platform.0.lock().will_open_menu.get_or_insert(callback); + } + } } extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id { - unsafe { - let platform = get_mac_platform(this); - let state = platform.0.lock(); - if let Some(id) = state.dock_menu { - id - } else { - nil - } - } + unsafe { + let platform = get_mac_platform(this); + let state = platform.0.lock(); + if let Some(id) = state.dock_menu { + id + } else { + nil + } + } } unsafe fn ns_url_to_path(url: id) -> Result { - let path: *mut c_char = msg_send![url, fileSystemRepresentation]; - anyhow::ensure!(!path.is_null(), "url is not a file path: {}", unsafe { - CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() - }); - Ok(PathBuf::from(OsStr::from_bytes(unsafe { - CStr::from_ptr(path).to_bytes() - }))) + let path: *mut c_char = msg_send![url, fileSystemRepresentation]; + anyhow::ensure!(!path.is_null(), "url is not a file path: {}", unsafe { + CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() + }); + Ok(PathBuf::from(OsStr::from_bytes(unsafe { + CStr::from_ptr(path).to_bytes() + }))) } #[link(name = "Carbon", kind = "framework")] unsafe extern "C" { - pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object; - pub(super) fn TISCopyCurrentKeyboardInputSource() -> *mut Object; - pub(super) fn TISGetInputSourceProperty( - inputSource: *mut Object, - propertyKey: *const c_void, - ) -> *mut Object; + pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object; + pub(super) fn TISCopyCurrentKeyboardInputSource() -> *mut Object; + pub(super) fn TISGetInputSourceProperty( + inputSource: *mut Object, + propertyKey: *const c_void, + ) -> *mut Object; - pub(super) fn UCKeyTranslate( - keyLayoutPtr: *const ::std::os::raw::c_void, - virtualKeyCode: u16, - keyAction: u16, - modifierKeyState: u32, - keyboardType: u32, - keyTranslateOptions: u32, - deadKeyState: *mut u32, - maxStringLength: usize, - actualStringLength: *mut usize, - unicodeString: *mut u16, - ) -> u32; - pub(super) fn LMGetKbdType() -> u16; - pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef; - pub(super) static kTISPropertyInputSourceID: CFStringRef; - pub(super) static kTISPropertyLocalizedName: CFStringRef; - pub(super) static kTISPropertyInputSourceIsASCIICapable: CFStringRef; - pub(super) static kTISPropertyInputSourceType: CFStringRef; - pub(super) static kTISTypeKeyboardInputMode: CFStringRef; + pub(super) fn UCKeyTranslate( + keyLayoutPtr: *const ::std::os::raw::c_void, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: u32, + deadKeyState: *mut u32, + maxStringLength: usize, + actualStringLength: *mut usize, + unicodeString: *mut u16, + ) -> u32; + pub(super) fn LMGetKbdType() -> u16; + pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + pub(super) static kTISPropertyInputSourceID: CFStringRef; + pub(super) static kTISPropertyLocalizedName: CFStringRef; + pub(super) static kTISPropertyInputSourceIsASCIICapable: CFStringRef; + pub(super) static kTISPropertyInputSourceType: CFStringRef; + pub(super) static kTISTypeKeyboardInputMode: CFStringRef; } mod security { - #![allow(non_upper_case_globals)] - use super::*; + #![allow(non_upper_case_globals)] + use super::*; - #[link(name = "Security", kind = "framework")] - unsafe extern "C" { - pub static kSecClass: CFStringRef; - pub static kSecClassInternetPassword: CFStringRef; - pub static kSecAttrServer: CFStringRef; - pub static kSecAttrAccount: CFStringRef; - pub static kSecValueData: CFStringRef; - pub static kSecReturnAttributes: CFStringRef; - pub static kSecReturnData: CFStringRef; + #[link(name = "Security", kind = "framework")] + unsafe extern "C" { + pub static kSecClass: CFStringRef; + pub static kSecClassInternetPassword: CFStringRef; + pub static kSecAttrServer: CFStringRef; + pub static kSecAttrAccount: CFStringRef; + pub static kSecValueData: CFStringRef; + pub static kSecReturnAttributes: CFStringRef; + pub static kSecReturnData: CFStringRef; - pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; - pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; - pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; - pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; - } + pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; + pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; + pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + } - pub const errSecSuccess: OSStatus = 0; - pub const errSecUserCanceled: OSStatus = -128; - pub const errSecItemNotFound: OSStatus = -25300; + pub const errSecSuccess: OSStatus = 0; + pub const errSecUserCanceled: OSStatus = -128; + pub const errSecItemNotFound: OSStatus = -25300; } diff --git a/crates/gpui_macos/src/screen_capture.rs b/crates/gpui_macos/src/screen_capture.rs index 5ece21a34c..72bb2178cb 100644 --- a/crates/gpui_macos/src/screen_capture.rs +++ b/crates/gpui_macos/src/screen_capture.rs @@ -2,29 +2,29 @@ use crate::ns_string; use anyhow::{Result, anyhow}; use block::ConcreteBlock; use cocoa::{ - base::{YES, id, nil}, - foundation::NSArray, + base::{YES, id, nil}, + foundation::NSArray, }; use collections::HashMap; use core_foundation::base::TCFType; use core_graphics::display::{ - CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, - CGDisplayModeGetPixelWidth, CGDisplayModeRelease, + CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, + CGDisplayModeGetPixelWidth, CGDisplayModeRelease, }; use ctor::ctor; use futures::channel::oneshot; use gpui::{ - DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - SharedString, SourceMetadata, size, + DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SharedString, SourceMetadata, size, }; use media::core_media::{CMSampleBuffer, CMSampleBufferRef}; use metal::NSInteger; use objc::{ - class, - declare::ClassDecl, - msg_send, - runtime::{Class, Object, Sel}, - sel, sel_impl, + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, }; use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc}; @@ -32,14 +32,14 @@ use crate::NSStringExt; #[derive(Clone)] pub struct MacScreenCaptureSource { - sc_display: id, - meta: Option, + sc_display: id, + meta: Option, } pub struct MacScreenCaptureStream { - sc_stream: id, - sc_stream_output: id, - meta: SourceMetadata, + sc_stream: id, + sc_stream_output: id, + meta: SourceMetadata, } static mut DELEGATE_CLASS: *const Class = ptr::null(); @@ -50,268 +50,268 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback"; const SCStreamOutputTypeScreen: NSInteger = 0; impl ScreenCaptureSource for MacScreenCaptureSource { - fn metadata(&self) -> Result { - let (display_id, size) = unsafe { - let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; - let display_mode_ref = CGDisplayCopyDisplayMode(display_id); - let width = CGDisplayModeGetPixelWidth(display_mode_ref); - let height = CGDisplayModeGetPixelHeight(display_mode_ref); - CGDisplayModeRelease(display_mode_ref); + fn metadata(&self) -> Result { + let (display_id, size) = unsafe { + let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; + let display_mode_ref = CGDisplayCopyDisplayMode(display_id); + let width = CGDisplayModeGetPixelWidth(display_mode_ref); + let height = CGDisplayModeGetPixelHeight(display_mode_ref); + CGDisplayModeRelease(display_mode_ref); - ( - display_id, - size(DevicePixels(width as i32), DevicePixels(height as i32)), - ) - }; - let (label, is_main) = self - .meta - .clone() - .map(|meta| (meta.label, meta.is_main)) - .unzip(); + ( + display_id, + size(DevicePixels(width as i32), DevicePixels(height as i32)), + ) + }; + let (label, is_main) = self + .meta + .clone() + .map(|meta| (meta.label, meta.is_main)) + .unzip(); - Ok(SourceMetadata { - id: display_id as u64, - label, - is_main, - resolution: size, - }) - } + Ok(SourceMetadata { + id: display_id as u64, + label, + is_main, + resolution: size, + }) + } - fn stream( - &self, - _foreground_executor: &ForegroundExecutor, - frame_callback: Box, - ) -> oneshot::Receiver>> { - unsafe { - let stream: id = msg_send![class!(SCStream), alloc]; - let filter: id = msg_send![class!(SCContentFilter), alloc]; - let configuration: id = msg_send![class!(SCStreamConfiguration), alloc]; - let delegate: id = msg_send![DELEGATE_CLASS, alloc]; - let output: id = msg_send![OUTPUT_CLASS, alloc]; + fn stream( + &self, + _foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + unsafe { + let stream: id = msg_send![class!(SCStream), alloc]; + let filter: id = msg_send![class!(SCContentFilter), alloc]; + let configuration: id = msg_send![class!(SCStreamConfiguration), alloc]; + let delegate: id = msg_send![DELEGATE_CLASS, alloc]; + let output: id = msg_send![OUTPUT_CLASS, alloc]; - let excluded_windows = NSArray::array(nil); - let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows]; - let configuration: id = msg_send![configuration, init]; - let _: id = msg_send![configuration, setScalesToFit: true]; - let _: id = msg_send![configuration, setPixelFormat: 0x42475241]; - // let _: id = msg_send![configuration, setShowsCursor: false]; - // let _: id = msg_send![configuration, setCaptureResolution: 3]; - let delegate: id = msg_send![delegate, init]; - let output: id = msg_send![output, init]; + let excluded_windows = NSArray::array(nil); + let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows]; + let configuration: id = msg_send![configuration, init]; + let _: id = msg_send![configuration, setScalesToFit: true]; + let _: id = msg_send![configuration, setPixelFormat: 0x42475241]; + // let _: id = msg_send![configuration, setShowsCursor: false]; + // let _: id = msg_send![configuration, setCaptureResolution: 3]; + let delegate: id = msg_send![delegate, init]; + let output: id = msg_send![output, init]; - output.as_mut().unwrap().set_ivar( - FRAME_CALLBACK_IVAR, - Box::into_raw(Box::new(frame_callback)) as *mut c_void, - ); + output.as_mut().unwrap().set_ivar( + FRAME_CALLBACK_IVAR, + Box::into_raw(Box::new(frame_callback)) as *mut c_void, + ); - let meta = self.metadata().unwrap(); - let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; - let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; - let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; + let meta = self.metadata().unwrap(); + let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; + let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; + let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; - // Stream contains filter, configuration, and delegate internally so we release them here - // to prevent a memory leak when steam is dropped - let _: () = msg_send![filter, release]; - let _: () = msg_send![configuration, release]; - let _: () = msg_send![delegate, release]; + // Stream contains filter, configuration, and delegate internally so we release them here + // to prevent a memory leak when steam is dropped + let _: () = msg_send![filter, release]; + let _: () = msg_send![configuration, release]; + let _: () = msg_send![delegate, release]; - let (tx, rx) = oneshot::channel(); + let (tx, rx) = oneshot::channel(); - let mut error: id = nil; - let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id]; - if error != nil { - let message: id = msg_send![error, localizedDescription]; - let _: () = msg_send![stream, release]; - let _: () = msg_send![output, release]; - tx.send(Err(anyhow!("failed to add stream output {message:?}"))) - .ok(); - return rx; - } + let mut error: id = nil; + let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + let _: () = msg_send![stream, release]; + let _: () = msg_send![output, release]; + tx.send(Err(anyhow!("failed to add stream output {message:?}"))) + .ok(); + return rx; + } - let tx = Rc::new(RefCell::new(Some(tx))); - let handler = ConcreteBlock::new({ - move |error: id| { - let result = if error == nil { - let stream = MacScreenCaptureStream { - meta: meta.clone(), - sc_stream: stream, - sc_stream_output: output, - }; - Ok(Box::new(stream) as Box) - } else { - let _: () = msg_send![stream, release]; - let _: () = msg_send![output, release]; - let message: id = msg_send![error, localizedDescription]; - Err(anyhow!("failed to start screen capture stream {message:?}")) - }; - if let Some(tx) = tx.borrow_mut().take() { - tx.send(result).ok(); - } - } - }); - let handler = handler.copy(); - let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler]; - rx - } - } + let tx = Rc::new(RefCell::new(Some(tx))); + let handler = ConcreteBlock::new({ + move |error: id| { + let result = if error == nil { + let stream = MacScreenCaptureStream { + meta: meta.clone(), + sc_stream: stream, + sc_stream_output: output, + }; + Ok(Box::new(stream) as Box) + } else { + let _: () = msg_send![stream, release]; + let _: () = msg_send![output, release]; + let message: id = msg_send![error, localizedDescription]; + Err(anyhow!("failed to start screen capture stream {message:?}")) + }; + if let Some(tx) = tx.borrow_mut().take() { + tx.send(result).ok(); + } + } + }); + let handler = handler.copy(); + let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler]; + rx + } + } } impl Drop for MacScreenCaptureSource { - fn drop(&mut self) { - unsafe { - let _: () = msg_send![self.sc_display, release]; - } - } + fn drop(&mut self) { + unsafe { + let _: () = msg_send![self.sc_display, release]; + } + } } impl ScreenCaptureStream for MacScreenCaptureStream { - fn metadata(&self) -> Result { - Ok(self.meta.clone()) - } + fn metadata(&self) -> Result { + Ok(self.meta.clone()) + } } impl Drop for MacScreenCaptureStream { - fn drop(&mut self) { - unsafe { - let mut error: id = nil; - let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _]; - if error != nil { - let message: id = msg_send![error, localizedDescription]; - log::error!("failed to add stream output {message:?}"); - } + fn drop(&mut self) { + unsafe { + let mut error: id = nil; + let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to add stream output {message:?}"); + } - let handler = ConcreteBlock::new(move |error: id| { - if error != nil { - let message: id = msg_send![error, localizedDescription]; - log::error!("failed to stop screen capture stream {message:?}"); - } - }); - let block = handler.copy(); - let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block]; - let _: () = msg_send![self.sc_stream, release]; - let _: () = msg_send![self.sc_stream_output, release]; - } - } + let handler = ConcreteBlock::new(move |error: id| { + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to stop screen capture stream {message:?}"); + } + }); + let block = handler.copy(); + let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block]; + let _: () = msg_send![self.sc_stream, release]; + let _: () = msg_send![self.sc_stream_output, release]; + } + } } #[derive(Clone)] struct ScreenMeta { - label: SharedString, - // Is this the screen with menu bar? - is_main: bool, + label: SharedString, + // Is this the screen with menu bar? + is_main: bool, } unsafe fn screen_id_to_human_label() -> HashMap { - let screens: id = msg_send![class!(NSScreen), screens]; - let count: usize = msg_send![screens, count]; - let mut map = HashMap::default(); - let screen_number_key = unsafe { ns_string("NSScreenNumber") }; - for i in 0..count { - let screen: id = msg_send![screens, objectAtIndex: i]; - let device_desc: id = msg_send![screen, deviceDescription]; - if device_desc == nil { - continue; - } + let screens: id = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut map = HashMap::default(); + let screen_number_key = unsafe { ns_string("NSScreenNumber") }; + for i in 0..count { + let screen: id = msg_send![screens, objectAtIndex: i]; + let device_desc: id = msg_send![screen, deviceDescription]; + if device_desc == nil { + continue; + } - let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; - if nsnumber == nil { - continue; - } + let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; + if nsnumber == nil { + continue; + } - let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; + let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; - let name: id = msg_send![screen, localizedName]; - if name != nil { - let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; - let rust_str = unsafe { - std::ffi::CStr::from_ptr(cstr) - .to_string_lossy() - .into_owned() - }; - map.insert( - screen_id, - ScreenMeta { - label: rust_str.into(), - is_main: i == 0, - }, - ); - } - } - map + let name: id = msg_send![screen, localizedName]; + if name != nil { + let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let rust_str = unsafe { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + }; + map.insert( + screen_id, + ScreenMeta { + label: rust_str.into(), + is_main: i == 0, + }, + ); + } + } + map } pub(crate) fn get_sources() -> oneshot::Receiver>>> { - unsafe { - let (tx, rx) = oneshot::channel(); - let tx = Rc::new(RefCell::new(Some(tx))); - let screen_id_to_label = screen_id_to_human_label(); - let block = ConcreteBlock::new(move |shareable_content: id, error: id| { - let Some(tx) = tx.borrow_mut().take() else { - return; - }; + unsafe { + let (tx, rx) = oneshot::channel(); + let tx = Rc::new(RefCell::new(Some(tx))); + let screen_id_to_label = screen_id_to_human_label(); + let block = ConcreteBlock::new(move |shareable_content: id, error: id| { + let Some(tx) = tx.borrow_mut().take() else { + return; + }; - let result = if error == nil { - let displays: id = msg_send![shareable_content, displays]; - let mut result = Vec::new(); - for i in 0..displays.count() { - let display = displays.objectAtIndex(i); - let id: CGDirectDisplayID = msg_send![display, displayID]; - let meta = screen_id_to_label.get(&id).cloned(); - let source = MacScreenCaptureSource { - sc_display: msg_send![display, retain], - meta, - }; - result.push(Rc::new(source) as Rc); - } - Ok(result) - } else { - let msg: id = msg_send![error, localizedDescription]; - Err(anyhow!( - "Screen share failed: {:?}", - NSStringExt::to_str(&msg) - )) - }; - tx.send(result).ok(); - }); - let block = block.copy(); + let result = if error == nil { + let displays: id = msg_send![shareable_content, displays]; + let mut result = Vec::new(); + for i in 0..displays.count() { + let display = displays.objectAtIndex(i); + let id: CGDirectDisplayID = msg_send![display, displayID]; + let meta = screen_id_to_label.get(&id).cloned(); + let source = MacScreenCaptureSource { + sc_display: msg_send![display, retain], + meta, + }; + result.push(Rc::new(source) as Rc); + } + Ok(result) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!( + "Screen share failed: {:?}", + NSStringExt::to_str(&msg) + )) + }; + tx.send(result).ok(); + }); + let block = block.copy(); - let _: () = msg_send![ + let _: () = msg_send![ class!(SCShareableContent), getShareableContentExcludingDesktopWindows:YES onScreenWindowsOnly:YES completionHandler:block]; - rx - } + rx + } } #[ctor(unsafe)] unsafe fn build_classes() { - let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap(); - unsafe { - decl.add_method( - sel!(outputVideoEffectDidStartForStream:), - output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(outputVideoEffectDidStopForStream:), - output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(stream:didStopWithError:), - stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id), - ); - DELEGATE_CLASS = decl.register(); + let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap(); + unsafe { + decl.add_method( + sel!(outputVideoEffectDidStartForStream:), + output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(outputVideoEffectDidStopForStream:), + output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(stream:didStopWithError:), + stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id), + ); + DELEGATE_CLASS = decl.register(); - let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap(); - decl.add_method( - sel!(stream:didOutputSampleBuffer:ofType:), - stream_did_output_sample_buffer_of_type - as extern "C" fn(&Object, Sel, id, id, NSInteger), - ); - decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR); + let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap(); + decl.add_method( + sel!(stream:didOutputSampleBuffer:ofType:), + stream_did_output_sample_buffer_of_type + as extern "C" fn(&Object, Sel, id, id, NSInteger), + ); + decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR); - OUTPUT_CLASS = decl.register(); - } + OUTPUT_CLASS = decl.register(); + } } extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {} @@ -321,24 +321,24 @@ extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _s extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {} extern "C" fn stream_did_output_sample_buffer_of_type( - this: &Object, - _: Sel, - _stream: id, - sample_buffer: id, - buffer_type: NSInteger, + this: &Object, + _: Sel, + _stream: id, + sample_buffer: id, + buffer_type: NSInteger, ) { - if buffer_type != SCStreamOutputTypeScreen { - return; - } + if buffer_type != SCStreamOutputTypeScreen { + return; + } - unsafe { - let sample_buffer = sample_buffer as CMSampleBufferRef; - let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer); - if let Some(buffer) = sample_buffer.image_buffer() { - let callback: Box> = - Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _); - callback(ScreenCaptureFrame(buffer)); - mem::forget(callback); - } - } + unsafe { + let sample_buffer = sample_buffer as CMSampleBufferRef; + let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer); + if let Some(buffer) = sample_buffer.image_buffer() { + let callback: Box> = + Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _); + callback(ScreenCaptureFrame(buffer)); + mem::forget(callback); + } + } } diff --git a/crates/gpui_macos/src/text_system.rs b/crates/gpui_macos/src/text_system.rs index 4ba444591e..0322c78bbc 100644 --- a/crates/gpui_macos/src/text_system.rs +++ b/crates/gpui_macos/src/text_system.rs @@ -2,48 +2,48 @@ use anyhow::anyhow; use cocoa::appkit::CGFloat; use collections::HashMap; use core_foundation::{ - array::{CFArray, CFArrayRef}, - attributed_string::CFMutableAttributedString, - base::{CFRange, CFType, TCFType}, - number::CFNumber, - string::CFString, + array::{CFArray, CFArrayRef}, + attributed_string::CFMutableAttributedString, + base::{CFRange, CFType, TCFType}, + number::CFNumber, + string::CFString, }; use core_graphics::{ - base::{CGGlyph, kCGImageAlphaPremultipliedLast}, - color_space::CGColorSpace, - context::{CGContext, CGTextDrawingMode}, - display::CGPoint, + base::{CGGlyph, kCGImageAlphaPremultipliedLast}, + color_space::CGColorSpace, + context::{CGContext, CGTextDrawingMode}, + display::CGPoint, }; use core_text::{ - font::CTFont, - font_collection::CTFontCollectionRef, - font_descriptor::{ - CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, - kCTFontWidthTrait, - }, - line::CTLine, - string_attributes::{kCTFontAttributeName, kCTKernAttributeName}, + font::CTFont, + font_collection::CTFontCollectionRef, + font_descriptor::{ + CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, + kCTFontWidthTrait, + }, + line::CTLine, + string_attributes::{kCTFontAttributeName, kCTKernAttributeName}, }; use font_kit::{ - font::Font as FontKitFont, - handle::Handle, - hinting::HintingOptions, - metrics::Metrics, - properties::{Style as FontkitStyle, Weight as FontkitWeight}, - source::SystemSource, - sources::mem::MemSource, + font::Font as FontKitFont, + handle::Handle, + hinting::HintingOptions, + metrics::Metrics, + properties::{Style as FontkitStyle, Weight as FontkitWeight}, + source::SystemSource, + sources::mem::MemSource, }; use gpui::{ - Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, - FontStyle, FontWeight, GlyphId, Hsla, LineLayout, Pixels, PlatformTextSystem, - RenderGlyphParams, Result, Rgba, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, - Size, TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra, + Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, + FontStyle, FontWeight, GlyphId, Hsla, LineLayout, Pixels, PlatformTextSystem, + RenderGlyphParams, Result, Rgba, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, + Size, TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra, }; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use pathfinder_geometry::{ - rect::{RectF, RectI}, - transform2d::Transform2F, - vector::Vector2F, + rect::{RectF, RectI}, + transform2d::Transform2F, + vector::Vector2F, }; use smallvec::SmallVec; use std::{borrow::Cow, char, convert::TryFrom, sync::Arc, sync::OnceLock}; @@ -58,615 +58,612 @@ pub struct MacTextSystem(RwLock); #[derive(Clone, PartialEq, Eq, Hash)] struct FontKey { - font_family: SharedString, - font_features: FontFeatures, - font_fallbacks: Option, + font_family: SharedString, + font_features: FontFeatures, + font_fallbacks: Option, } struct MacTextSystemState { - memory_source: MemSource, - system_source: SystemSource, - fonts: Vec, - font_selections: HashMap, - font_ids_by_postscript_name: HashMap, - font_ids_by_font_key: HashMap>, - postscript_names_by_font_id: HashMap, + memory_source: MemSource, + system_source: SystemSource, + fonts: Vec, + font_selections: HashMap, + font_ids_by_postscript_name: HashMap, + font_ids_by_font_key: HashMap>, + postscript_names_by_font_id: HashMap, } impl MacTextSystem { - /// Create a new MacTextSystem. - pub fn new() -> Self { - Self(RwLock::new(MacTextSystemState { - memory_source: MemSource::empty(), - system_source: SystemSource::new(), - fonts: Vec::new(), - font_selections: HashMap::default(), - font_ids_by_postscript_name: HashMap::default(), - font_ids_by_font_key: HashMap::default(), - postscript_names_by_font_id: HashMap::default(), - })) - } + /// Create a new MacTextSystem. + pub fn new() -> Self { + Self(RwLock::new(MacTextSystemState { + memory_source: MemSource::empty(), + system_source: SystemSource::new(), + fonts: Vec::new(), + font_selections: HashMap::default(), + font_ids_by_postscript_name: HashMap::default(), + font_ids_by_font_key: HashMap::default(), + postscript_names_by_font_id: HashMap::default(), + })) + } } impl Default for MacTextSystem { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl PlatformTextSystem for MacTextSystem { - fn add_fonts(&self, fonts: Vec>) -> Result<()> { - self.0.write().add_fonts(fonts) - } + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } - fn all_font_names(&self) -> Vec { - let mut names = Vec::new(); - let collection = core_text::font_collection::create_for_all_families(); - // NOTE: We intentionally avoid using `collection.get_descriptors()` here because - // it has a memory leak bug in core-text v21.0.0. The upstream code uses - // `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors` - // follows the Create Rule (caller owns the result), so it should use - // `wrap_under_create_rule`. We call the function directly with correct memory management. - unsafe extern "C" { - fn CTFontCollectionCreateMatchingFontDescriptors( - collection: CTFontCollectionRef, - ) -> CFArrayRef; - } - let descriptors: Option> = unsafe { - let array_ref = - CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef()); - if array_ref.is_null() { - None - } else { - Some(CFArray::wrap_under_create_rule(array_ref)) - } - }; - let Some(descriptors) = descriptors else { - return names; - }; - for descriptor in descriptors.into_iter() { - names.extend(lenient_font_attributes::family_name(&descriptor)); - } - if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { - names.extend(fonts_in_memory); - } - names - } + fn all_font_names(&self) -> Vec { + let mut names = Vec::new(); + let collection = core_text::font_collection::create_for_all_families(); + // NOTE: We intentionally avoid using `collection.get_descriptors()` here because + // it has a memory leak bug in core-text v21.0.0. The upstream code uses + // `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors` + // follows the Create Rule (caller owns the result), so it should use + // `wrap_under_create_rule`. We call the function directly with correct memory management. + unsafe extern "C" { + fn CTFontCollectionCreateMatchingFontDescriptors( + collection: CTFontCollectionRef, + ) -> CFArrayRef; + } + let descriptors: Option> = unsafe { + let array_ref = + CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef()); + if array_ref.is_null() { + None + } else { + Some(CFArray::wrap_under_create_rule(array_ref)) + } + }; + let Some(descriptors) = descriptors else { + return names; + }; + for descriptor in descriptors.into_iter() { + names.extend(lenient_font_attributes::family_name(&descriptor)); + } + if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { + names.extend(fonts_in_memory); + } + names + } - fn font_id(&self, font: &Font) -> Result { - let lock = self.0.upgradable_read(); - if let Some(font_id) = lock.font_selections.get(font) { - Ok(*font_id) - } else { - let mut lock = RwLockUpgradableReadGuard::upgrade(lock); - let font_key = FontKey { - font_family: font.family.clone(), - font_features: font.features.clone(), - font_fallbacks: font.fallbacks.clone(), - }; - let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { - font_ids.as_slice() - } else { - let font_ids = - lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; - lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); - lock.font_ids_by_font_key[&font_key].as_ref() - }; + fn font_id(&self, font: &Font) -> Result { + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let font_key = FontKey { + font_family: font.family.clone(), + font_features: font.features.clone(), + font_fallbacks: font.fallbacks.clone(), + }; + let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { + font_ids.as_slice() + } else { + let font_ids = + lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; + lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); + lock.font_ids_by_font_key[&font_key].as_ref() + }; - let candidate_properties = candidates - .iter() - .map(|font_id| lock.fonts[font_id.0].properties()) - .collect::>(); + let candidate_properties = candidates + .iter() + .map(|font_id| lock.fonts[font_id.0].properties()) + .collect::>(); - let ix = font_kit::matching::find_best_match( - &candidate_properties, - &font_kit::properties::Properties { - style: fontkit_style(font.style), - weight: fontkit_weight(font.weight), - stretch: Default::default(), - }, - )?; + let ix = font_kit::matching::find_best_match( + &candidate_properties, + &font_kit::properties::Properties { + style: fontkit_style(font.style), + weight: fontkit_weight(font.weight), + stretch: Default::default(), + }, + )?; - let font_id = candidates[ix]; - lock.font_selections.insert(font.clone(), font_id); - Ok(font_id) - } - } + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } - fn font_metrics(&self, font_id: FontId) -> FontMetrics { - font_kit_metrics_to_metrics(self.0.read().fonts[font_id.0].metrics()) - } + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + font_kit_metrics_to_metrics(self.0.read().fonts[font_id.0].metrics()) + } - fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - Ok(bounds_from_rect( - self.0.read().fonts[font_id.0].typographic_bounds(glyph_id.0)?, - )) - } + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(bounds_from_rect( + self.0.read().fonts[font_id.0].typographic_bounds(glyph_id.0)?, + )) + } - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - self.0.read().advance(font_id, 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_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.read().raster_bounds(params) - } + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.read().raster_bounds(params) + } - fn rasterize_glyph( - &self, - glyph_id: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)> { - self.0.read().rasterize_glyph(glyph_id, raster_bounds) - } + fn rasterize_glyph( + &self, + glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.read().rasterize_glyph(glyph_id, raster_bounds) + } - fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { - self.0.write().layout_line(text, font_size, font_runs) - } + fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, font_runs) + } - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - TextRenderingMode::Grayscale - } + fn recommended_rendering_mode( + &self, + _font_id: FontId, + _font_size: Pixels, + ) -> TextRenderingMode { + TextRenderingMode::Grayscale + } - fn glyph_dilation_for_color(&self, color: Hsla) -> u8 { - // When font smoothing is enabled, CoreGraphics thickens glyph strokes by an amount that - // depends on the foreground color's luminance. We replicate the logic used by CoreGraphics - // to select between the different levels of dilation. - if !font_smoothing_allowed_by_user() { - return 0; - } - let rgba: Rgba = color.into(); - let luminance = 0.2126 * rgba.r + 0.7152 * rgba.g + 0.0722 * rgba.b; - let level = ((4.0 * luminance) + 0.5).floor() as i32; - level.clamp(0, 4) as u8 - } + fn glyph_dilation_for_color(&self, color: Hsla) -> u8 { + // When font smoothing is enabled, CoreGraphics thickens glyph strokes by an amount that + // depends on the foreground color's luminance. We replicate the logic used by CoreGraphics + // to select between the different levels of dilation. + if !font_smoothing_allowed_by_user() { + return 0; + } + let rgba: Rgba = color.into(); + let luminance = 0.2126 * rgba.r + 0.7152 * rgba.g + 0.0722 * rgba.b; + let level = ((4.0 * luminance) + 0.5).floor() as i32; + level.clamp(0, 4) as u8 + } } fn font_smoothing_allowed_by_user() -> bool { - static ALLOWED: OnceLock = OnceLock::new(); - *ALLOWED.get_or_init(|| { - use core_foundation_sys::preferences::{ - CFPreferencesCopyAppValue, kCFPreferencesCurrentApplication, - }; + static ALLOWED: OnceLock = OnceLock::new(); + *ALLOWED.get_or_init(|| { + use core_foundation_sys::preferences::{ + CFPreferencesCopyAppValue, kCFPreferencesCurrentApplication, + }; - let key = CFString::new("AppleFontSmoothing"); - let value_ref = unsafe { - CFPreferencesCopyAppValue(key.as_concrete_TypeRef(), kCFPreferencesCurrentApplication) - }; - if value_ref.is_null() { - return true; - } - let value = unsafe { CFType::wrap_under_create_rule(value_ref) }; - let Some(number) = value.downcast_into::() else { - return true; - }; - // Only an explicit value of `0` means that font smoothing is disabled. - number.to_i64() != Some(0) - }) + let key = CFString::new("AppleFontSmoothing"); + let value_ref = unsafe { + CFPreferencesCopyAppValue(key.as_concrete_TypeRef(), kCFPreferencesCurrentApplication) + }; + if value_ref.is_null() { + return true; + } + let value = unsafe { CFType::wrap_under_create_rule(value_ref) }; + let Some(number) = value.downcast_into::() else { + return true; + }; + // Only an explicit value of `0` means that font smoothing is disabled. + number.to_i64() != Some(0) + }) } impl MacTextSystemState { - fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { - let fonts = fonts - .into_iter() - .map(|bytes| match bytes { - Cow::Borrowed(embedded_font) => { - let data_provider = unsafe { - core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) - }; - let font = core_graphics::font::CGFont::from_data_provider(data_provider) - .map_err(|()| anyhow!("Could not load an embedded font."))?; - let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); - Ok(Handle::from_native(&font)) - } - Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), - }) - .collect::>>()?; - self.memory_source.add_fonts(fonts.into_iter())?; - Ok(()) - } + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let fonts = fonts + .into_iter() + .map(|bytes| match bytes { + Cow::Borrowed(embedded_font) => { + let data_provider = unsafe { + core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) + }; + let font = core_graphics::font::CGFont::from_data_provider(data_provider) + .map_err(|()| anyhow!("Could not load an embedded font."))?; + let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); + Ok(Handle::from_native(&font)) + } + Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), + }) + .collect::>>()?; + self.memory_source.add_fonts(fonts.into_iter())?; + Ok(()) + } - fn load_family( - &mut self, - name: &str, - features: &FontFeatures, - fallbacks: Option<&FontFallbacks>, - ) -> Result> { - let name = gpui::font_name_with_fallbacks(name, ".AppleSystemUIFont"); + fn load_family( + &mut self, + name: &str, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, + ) -> Result> { + let name = gpui::font_name_with_fallbacks(name, ".AppleSystemUIFont"); - let mut font_ids = SmallVec::new(); - let family = self - .memory_source - .select_family_by_name(name) - .or_else(|_| self.system_source.select_family_by_name(name))?; - for font in family.fonts() { - let mut font = font.load()?; + let mut font_ids = SmallVec::new(); + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { + let mut font = font.load()?; - apply_features_and_fallbacks(&mut font, features, fallbacks)?; - // This block contains a precautionary fix to guard against loading fonts - // that might cause panics due to `.unwrap()`s up the chain. - { - // We use the 'm' character for text measurements in various spots - // (e.g., the editor). However, at time of writing some of those usages - // will panic if the font has no 'm' glyph. - // - // Therefore, we check up front that the font has the necessary glyph. - let has_m_glyph = font.glyph_for_char('m').is_some(); + apply_features_and_fallbacks(&mut font, features, fallbacks)?; + // This block contains a precautionary fix to guard against loading fonts + // that might cause panics due to `.unwrap()`s up the chain. + { + // We use the 'm' character for text measurements in various spots + // (e.g., the editor). However, at time of writing some of those usages + // will panic if the font has no 'm' glyph. + // + // Therefore, we check up front that the font has the necessary glyph. + let has_m_glyph = font.glyph_for_char('m').is_some(); - // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, - // but we need to be able to load it for rendering Windows icons in - // the Storybook (on macOS). - let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; + // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, + // but we need to be able to load it for rendering Windows icons in + // the Storybook (on macOS). + let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; - if !has_m_glyph && !is_segoe_fluent_icons { - // I spent far too long trying to track down why a font missing the 'm' - // character wasn't loading. This log statement will hopefully save - // someone else from suffering the same fate. - log::warn!( - "font '{}' has no 'm' character and was not loaded", - font.full_name() - ); - continue; - } - } + if !has_m_glyph && !is_segoe_fluent_icons { + // I spent far too long trying to track down why a font missing the 'm' + // character wasn't loading. This log statement will hopefully save + // someone else from suffering the same fate. + log::warn!( + "font '{}' has no 'm' character and was not loaded", + font.full_name() + ); + continue; + } + } - // We've seen a number of panics in production caused by calling font.properties() - // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, - // and to try and identify the incalcitrant font. - let traits = font.native_font().all_traits(); - if unsafe { - !(traits - .get(kCTFontSymbolicTrait) - .downcast::() - .is_some() - && traits - .get(kCTFontWidthTrait) - .downcast::() - .is_some() - && traits - .get(kCTFontWeightTrait) - .downcast::() - .is_some() - && traits - .get(kCTFontSlantTrait) - .downcast::() - .is_some()) - } { - log::error!( - "Failed to read traits for font {:?}", - font.postscript_name().unwrap() - ); - continue; - } + // We've seen a number of panics in production caused by calling font.properties() + // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, + // and to try and identify the incalcitrant font. + let traits = font.native_font().all_traits(); + if unsafe { + !(traits + .get(kCTFontSymbolicTrait) + .downcast::() + .is_some() && traits + .get(kCTFontWidthTrait) + .downcast::() + .is_some() && traits + .get(kCTFontWeightTrait) + .downcast::() + .is_some() && traits + .get(kCTFontSlantTrait) + .downcast::() + .is_some()) + } { + log::error!( + "Failed to read traits for font {:?}", + font.postscript_name().unwrap() + ); + continue; + } - let font_id = FontId(self.fonts.len()); - font_ids.push(font_id); - let postscript_name = font.postscript_name().unwrap(); - self.font_ids_by_postscript_name - .insert(postscript_name.clone(), font_id); - self.postscript_names_by_font_id - .insert(font_id, postscript_name); - self.fonts.push(font); - } - Ok(font_ids) - } + let font_id = FontId(self.fonts.len()); + font_ids.push(font_id); + let postscript_name = font.postscript_name().unwrap(); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts.push(font); + } + Ok(font_ids) + } - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - Ok(size_from_vector2f( - self.fonts[font_id.0].advance(glyph_id.0)?, - )) - } + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(size_from_vector2f( + self.fonts[font_id.0].advance(glyph_id.0)?, + )) + } - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { - self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) - } + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) + } - fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { - let postscript_name = requested_font.postscript_name(); - if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { - *font_id - } else { - let font_id = FontId(self.fonts.len()); - self.font_ids_by_postscript_name - .insert(postscript_name.clone(), font_id); - self.postscript_names_by_font_id - .insert(font_id, postscript_name); - self.fonts - .push(font_kit::font::Font::from_core_graphics_font( - requested_font.copy_to_CGFont(), - )); - font_id - } - } + fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { + let postscript_name = requested_font.postscript_name(); + if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { + *font_id + } else { + let font_id = FontId(self.fonts.len()); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts + .push(font_kit::font::Font::from_core_graphics_font( + requested_font.copy_to_CGFont(), + )); + font_id + } + } - fn is_emoji(&self, font_id: FontId) -> bool { - self.postscript_names_by_font_id - .get(&font_id) - .is_some_and(|postscript_name| { - postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" - }) - } + fn is_emoji(&self, font_id: FontId) -> bool { + self.postscript_names_by_font_id + .get(&font_id) + .is_some_and(|postscript_name| { + postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" + }) + } - fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - let font = &self.fonts[params.font_id.0]; - let scale = Transform2F::from_scale(params.scale_factor); - let bounds: Bounds = bounds_from_rect_i(font.raster_bounds( - params.glyph_id.0, - params.font_size.into(), - scale, - HintingOptions::None, - font_kit::canvas::RasterizationOptions::GrayscaleAa, - )?); + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let font = &self.fonts[params.font_id.0]; + let scale = Transform2F::from_scale(params.scale_factor); + let bounds: Bounds = bounds_from_rect_i(font.raster_bounds( + params.glyph_id.0, + params.font_size.into(), + scale, + HintingOptions::None, + font_kit::canvas::RasterizationOptions::GrayscaleAa, + )?); - // Expand the bounds by 1 pixel on each side to give CG room for anti-aliasing. - Ok(bounds.dilate(DevicePixels(1))) - } + // Expand the bounds by 1 pixel on each side to give CG room for anti-aliasing. + Ok(bounds.dilate(DevicePixels(1))) + } - fn rasterize_glyph( - &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"); - } else { - // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. - let mut bitmap_size = glyph_bounds.size; - if params.subpixel_variant.x > 0 { - bitmap_size.width += DevicePixels(1); - } - if params.subpixel_variant.y > 0 { - bitmap_size.height += DevicePixels(1); - } - let bitmap_size = bitmap_size; + fn rasterize_glyph( + &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"); + } else { + // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. + let mut bitmap_size = glyph_bounds.size; + if params.subpixel_variant.x > 0 { + bitmap_size.width += DevicePixels(1); + } + if params.subpixel_variant.y > 0 { + bitmap_size.height += DevicePixels(1); + } + let bitmap_size = bitmap_size; - let mut bytes; - let cx; - if params.is_emoji { - bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; - cx = CGContext::create_bitmap_context( - Some(bytes.as_mut_ptr() as *mut _), - bitmap_size.width.0 as usize, - bitmap_size.height.0 as usize, - 8, - bitmap_size.width.0 as usize * 4, - &CGColorSpace::create_device_rgb(), - kCGImageAlphaPremultipliedLast, - ); - } else { - bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; - cx = CGContext::create_bitmap_context( - Some(bytes.as_mut_ptr() as *mut _), - bitmap_size.width.0 as usize, - bitmap_size.height.0 as usize, - 8, - bitmap_size.width.0 as usize, - &CGColorSpace::create_device_gray(), - kCGImageAlphaOnly, - ); - } + let mut bytes; + let cx; + if params.is_emoji { + bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaPremultipliedLast, + ); + } else { + bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize, + &CGColorSpace::create_device_gray(), + kCGImageAlphaOnly, + ); + } - // Move the origin to bottom left and account for scaling, this - // makes drawing text consistent with the font-kit's raster_bounds. - cx.translate( - -glyph_bounds.origin.x.0 as CGFloat, - (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, - ); - cx.scale( - params.scale_factor as CGFloat, - params.scale_factor as CGFloat, - ); + // Move the origin to bottom left and account for scaling, this + // makes drawing text consistent with the font-kit's raster_bounds. + cx.translate( + -glyph_bounds.origin.x.0 as CGFloat, + (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, + ); + cx.scale( + params.scale_factor as CGFloat, + params.scale_factor as CGFloat, + ); - let subpixel_shift = params - .subpixel_variant - .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); - cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); - cx.set_allows_antialiasing(true); - cx.set_should_antialias(true); - cx.set_allows_font_subpixel_positioning(true); - cx.set_should_subpixel_position_fonts(true); - cx.set_allows_font_subpixel_quantization(false); - cx.set_should_subpixel_quantize_fonts(false); + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); + cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); + cx.set_allows_antialiasing(true); + cx.set_should_antialias(true); + cx.set_allows_font_subpixel_positioning(true); + cx.set_should_subpixel_position_fonts(true); + cx.set_allows_font_subpixel_quantization(false); + cx.set_should_subpixel_quantize_fonts(false); - if params.dilation > 0 { - let luminance = params.dilation as f64 * 0.25; - cx.set_should_smooth_fonts(true); - cx.set_gray_fill_color(luminance, 1.0); - } else { - cx.set_gray_fill_color(0.0, 1.0); - } - self.fonts[params.font_id.0] - .native_font() - .clone_with_font_size(f32::from(params.font_size) as CGFloat) - .draw_glyphs( - &[params.glyph_id.0 as CGGlyph], - &[CGPoint::new( - (subpixel_shift.x / params.scale_factor) as CGFloat, - (subpixel_shift.y / params.scale_factor) as CGFloat, - )], - cx, - ); + if params.dilation > 0 { + let luminance = params.dilation as f64 * 0.25; + cx.set_should_smooth_fonts(true); + cx.set_gray_fill_color(luminance, 1.0); + } else { + cx.set_gray_fill_color(0.0, 1.0); + } + self.fonts[params.font_id.0] + .native_font() + .clone_with_font_size(f32::from(params.font_size) as CGFloat) + .draw_glyphs( + &[params.glyph_id.0 as CGGlyph], + &[CGPoint::new( + (subpixel_shift.x / params.scale_factor) as CGFloat, + (subpixel_shift.y / params.scale_factor) as CGFloat, + )], + cx, + ); - if params.is_emoji { - // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. - for pixel in bytes.chunks_exact_mut(4) { - swap_rgba_pa_to_bgra(pixel); - } - } + if params.is_emoji { + // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. + for pixel in bytes.chunks_exact_mut(4) { + swap_rgba_pa_to_bgra(pixel); + } + } - Ok((bitmap_size, bytes)) - } - } + Ok((bitmap_size, bytes)) + } + } - fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { - // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. - let mut string = CFMutableAttributedString::new(); - let mut max_ascent = 0.0f32; - let mut max_descent = 0.0f32; + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. + let mut string = CFMutableAttributedString::new(); + let mut max_ascent = 0.0f32; + let mut max_descent = 0.0f32; - { - let mut text = text; - let mut break_ligature = true; - for run in font_runs { - let text_run; - (text_run, text) = text.split_at(run.len); + { + let mut text = text; + let mut break_ligature = true; + for run in font_runs { + let text_run; + (text_run, text) = text.split_at(run.len); - let utf16_start = string.char_len(); // insert at end of string - // note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string) - string.replace_str(&CFString::new(text_run), CFRange::init(utf16_start, 0)); - let utf16_end = string.char_len(); + let utf16_start = string.char_len(); // insert at end of string + // note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string) + string.replace_str(&CFString::new(text_run), CFRange::init(utf16_start, 0)); + let utf16_end = string.char_len(); - let length = utf16_end - utf16_start; - let cf_range = CFRange::init(utf16_start, length); - let font = &self.fonts[run.font_id.0]; + let length = utf16_end - utf16_start; + let cf_range = CFRange::init(utf16_start, length); + let font = &self.fonts[run.font_id.0]; - let font_metrics = font.metrics(); - let font_scale = f32::from(font_size) / font_metrics.units_per_em as f32; - max_ascent = max_ascent.max(font_metrics.ascent * font_scale); - max_descent = max_descent.max(-font_metrics.descent * font_scale); + let font_metrics = font.metrics(); + let font_scale = f32::from(font_size) / font_metrics.units_per_em as f32; + max_ascent = max_ascent.max(font_metrics.ascent * font_scale); + max_descent = max_descent.max(-font_metrics.descent * font_scale); - let font_size = if break_ligature { - px(f32::from(font_size).next_up()) - } else { - font_size - }; - unsafe { - string.set_attribute( - cf_range, - kCTFontAttributeName, - &font.native_font().clone_with_font_size(font_size.into()), - ); - if let Some(spacing) = run.letter_spacing { - string.set_attribute( - cf_range, - kCTKernAttributeName, - &CFNumber::from(f64::from(spacing.as_f32())), - ); - } - } - break_ligature = !break_ligature; - } - } - // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. - let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); - let glyph_runs = line.glyph_runs(); - let mut runs = >::with_capacity(glyph_runs.len() as usize); - let mut ix_converter = StringIndexConverter::new(text); - for run in glyph_runs.into_iter() { - let attributes = run.attributes().unwrap(); - let font = unsafe { - attributes - .get(kCTFontAttributeName) - .downcast::() - .unwrap() - }; - let font_id = self.id_for_native_font(font); + let font_size = if break_ligature { + px(f32::from(font_size).next_up()) + } else { + font_size + }; + unsafe { + string.set_attribute( + cf_range, + kCTFontAttributeName, + &font.native_font().clone_with_font_size(font_size.into()), + ); + if let Some(spacing) = run.letter_spacing { + string.set_attribute( + cf_range, + kCTKernAttributeName, + &CFNumber::from(f64::from(spacing.as_f32())), + ); + } + } + break_ligature = !break_ligature; + } + } + // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. + let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); + let glyph_runs = line.glyph_runs(); + let mut runs = >::with_capacity(glyph_runs.len() as usize); + let mut ix_converter = StringIndexConverter::new(text); + for run in glyph_runs.into_iter() { + let attributes = run.attributes().unwrap(); + let font = unsafe { + attributes + .get(kCTFontAttributeName) + .downcast::() + .unwrap() + }; + let font_id = self.id_for_native_font(font); - let glyphs = match runs.last_mut() { - Some(run) if run.font_id == font_id => &mut run.glyphs, - _ => { - runs.push(ShapedRun { - font_id, - glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)), - }); - &mut runs.last_mut().unwrap().glyphs - } - }; - for ((&glyph_id, position), &glyph_utf16_ix) in run - .glyphs() - .iter() - .zip(run.positions().iter()) - .zip(run.string_indices().iter()) - { - let glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap(); - if ix_converter.utf16_ix > glyph_utf16_ix { - // We cannot reuse current index converter, as it can only seek forward. Restart the search. - ix_converter = StringIndexConverter::new(text); - } - ix_converter.advance_to_utf16_ix(glyph_utf16_ix); - glyphs.push(ShapedGlyph { - id: GlyphId(glyph_id as u32), - position: point(position.x as f32, position.y as f32).map(px), - index: ix_converter.utf8_ix, - is_emoji: self.is_emoji(font_id), - }); - } - } - let typographic_bounds = line.get_typographic_bounds(); - LineLayout { - runs, - font_size, - width: typographic_bounds.width.into(), - ascent: max_ascent.into(), - descent: max_descent.into(), - len: text.len(), - } - } + let glyphs = match runs.last_mut() { + Some(run) if run.font_id == font_id => &mut run.glyphs, + _ => { + runs.push(ShapedRun { + font_id, + glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)), + }); + &mut runs.last_mut().unwrap().glyphs + } + }; + for ((&glyph_id, position), &glyph_utf16_ix) in run + .glyphs() + .iter() + .zip(run.positions().iter()) + .zip(run.string_indices().iter()) + { + let glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap(); + if ix_converter.utf16_ix > glyph_utf16_ix { + // We cannot reuse current index converter, as it can only seek forward. Restart the search. + ix_converter = StringIndexConverter::new(text); + } + ix_converter.advance_to_utf16_ix(glyph_utf16_ix); + glyphs.push(ShapedGlyph { + id: GlyphId(glyph_id as u32), + position: point(position.x as f32, position.y as f32).map(px), + index: ix_converter.utf8_ix, + is_emoji: self.is_emoji(font_id), + }); + } + } + let typographic_bounds = line.get_typographic_bounds(); + LineLayout { + runs, + font_size, + width: typographic_bounds.width.into(), + ascent: max_ascent.into(), + descent: max_descent.into(), + len: text.len(), + } + } } #[derive(Debug, Clone)] struct StringIndexConverter<'a> { - text: &'a str, - /// Index in UTF-8 bytes - utf8_ix: usize, - /// Index in UTF-16 code units - utf16_ix: usize, + text: &'a str, + /// Index in UTF-8 bytes + utf8_ix: usize, + /// Index in UTF-16 code units + utf16_ix: usize, } impl<'a> StringIndexConverter<'a> { - fn new(text: &'a str) -> Self { - Self { - text, - utf8_ix: 0, - utf16_ix: 0, - } - } + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } - 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 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_kit_metrics_to_metrics(metrics: Metrics) -> FontMetrics { - FontMetrics { - units_per_em: metrics.units_per_em, - ascent: metrics.ascent, - descent: metrics.descent, - line_gap: metrics.line_gap, - underline_position: metrics.underline_position, - underline_thickness: metrics.underline_thickness, - cap_height: metrics.cap_height, - x_height: metrics.x_height, - bounding_box: bounds_from_rect(metrics.bounding_box), - } + FontMetrics { + units_per_em: metrics.units_per_em, + ascent: metrics.ascent, + descent: metrics.descent, + line_gap: metrics.line_gap, + underline_position: metrics.underline_position, + underline_thickness: metrics.underline_thickness, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + bounding_box: bounds_from_rect(metrics.bounding_box), + } } fn bounds_from_rect(rect: RectF) -> Bounds { - Bounds { - origin: point(rect.origin_x(), rect.origin_y()), - size: size(rect.width(), rect.height()), - } + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } } fn bounds_from_rect_i(rect: RectI) -> Bounds { - Bounds { - origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), - size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), - } + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } } // impl From for Size { @@ -691,229 +688,229 @@ fn bounds_from_rect_i(rect: RectI) -> Bounds { // } fn size_from_vector2f(vec: Vector2F) -> Size { - size(vec.x(), vec.y()) + size(vec.x(), vec.y()) } fn fontkit_weight(value: FontWeight) -> FontkitWeight { - FontkitWeight(value.0) + FontkitWeight(value.0) } fn fontkit_style(style: FontStyle) -> FontkitStyle { - match style { - FontStyle::Normal => FontkitStyle::Normal, - FontStyle::Italic => FontkitStyle::Italic, - FontStyle::Oblique => FontkitStyle::Oblique, - } + match style { + FontStyle::Normal => FontkitStyle::Normal, + FontStyle::Italic => FontkitStyle::Italic, + FontStyle::Oblique => FontkitStyle::Oblique, + } } // Some fonts may have no attributes despite `core_text` requiring them (and panicking). // This is the same version as `core_text` has without `expect` calls. mod lenient_font_attributes { - use core_foundation::{ - base::{CFRetain, CFType, TCFType}, - string::{CFString, CFStringRef}, - }; - use core_text::font_descriptor::{ - CTFontDescriptor, CTFontDescriptorCopyAttribute, kCTFontFamilyNameAttribute, - }; + use core_foundation::{ + base::{CFRetain, CFType, TCFType}, + string::{CFString, CFStringRef}, + }; + use core_text::font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCopyAttribute, kCTFontFamilyNameAttribute, + }; - pub fn family_name(descriptor: &CTFontDescriptor) -> Option { - unsafe { get_string_attribute(descriptor, kCTFontFamilyNameAttribute) } - } + pub fn family_name(descriptor: &CTFontDescriptor) -> Option { + unsafe { get_string_attribute(descriptor, kCTFontFamilyNameAttribute) } + } - fn get_string_attribute( - descriptor: &CTFontDescriptor, - attribute: CFStringRef, - ) -> Option { - unsafe { - let value = CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), attribute); - if value.is_null() { - return None; - } + fn get_string_attribute( + descriptor: &CTFontDescriptor, + attribute: CFStringRef, + ) -> Option { + unsafe { + let value = CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), attribute); + if value.is_null() { + return None; + } - let value = CFType::wrap_under_create_rule(value); - assert!(value.instance_of::()); - let s = wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef); - Some(s.to_string()) - } - } + let value = CFType::wrap_under_create_rule(value); + assert!(value.instance_of::()); + let s = wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef); + Some(s.to_string()) + } + } - unsafe fn wrap_under_get_rule(reference: CFStringRef) -> CFString { - unsafe { - assert!(!reference.is_null(), "Attempted to create a NULL object."); - let reference = CFRetain(reference as *const ::std::os::raw::c_void) as CFStringRef; - TCFType::wrap_under_create_rule(reference) - } - } + unsafe fn wrap_under_get_rule(reference: CFStringRef) -> CFString { + unsafe { + assert!(!reference.is_null(), "Attempted to create a NULL object."); + let reference = CFRetain(reference as *const ::std::os::raw::c_void) as CFStringRef; + TCFType::wrap_under_create_rule(reference) + } + } } #[cfg(test)] mod tests { - use crate::MacTextSystem; - use gpui::{FontRun, GlyphId, PlatformTextSystem, font, px}; + use crate::MacTextSystem; + use gpui::{FontRun, GlyphId, PlatformTextSystem, font, px}; - #[test] - fn test_layout_line_bom_char() { - let fonts = MacTextSystem::new(); - let font_id = fonts.font_id(&font("Helvetica")).unwrap(); - let line = "\u{feff}"; - let mut style = FontRun { - font_id, - len: line.len(), - letter_spacing: None, - }; + #[test] + fn test_layout_line_bom_char() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + let line = "\u{feff}"; + let mut style = FontRun { + font_id, + len: line.len(), + letter_spacing: None, + }; - let layout = fonts.layout_line(line, px(16.), &[style]); - assert_eq!(layout.len, line.len()); - assert!(layout.runs.is_empty()); + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert!(layout.runs.is_empty()); - let line = "a\u{feff}b"; - style.len = line.len(); - let layout = fonts.layout_line(line, px(16.), &[style]); - assert_eq!(layout.len, line.len()); - assert_eq!(layout.runs.len(), 1); - assert_eq!(layout.runs[0].glyphs.len(), 2); - assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a - // There's no glyph for \u{feff} - assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b + let line = "a\u{feff}b"; + style.len = line.len(); + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b - let line = "\u{feff}ab"; - let font_runs = &[ - FontRun { - len: "\u{feff}".len(), - font_id, - letter_spacing: None, - }, - FontRun { - len: "ab".len(), - font_id, - letter_spacing: None, - }, - ]; - let layout = fonts.layout_line(line, px(16.), font_runs); - assert_eq!(layout.len, line.len()); - assert_eq!(layout.runs.len(), 1); - assert_eq!(layout.runs[0].glyphs.len(), 2); - // There's no glyph for \u{feff} - assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a - assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b - } + let line = "\u{feff}ab"; + let font_runs = &[ + FontRun { + len: "\u{feff}".len(), + font_id, + letter_spacing: None, + }, + FontRun { + len: "ab".len(), + font_id, + letter_spacing: None, + }, + ]; + let layout = fonts.layout_line(line, px(16.), font_runs); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a + assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b + } - #[test] - fn test_layout_line_zwnj_insertion() { - let fonts = MacTextSystem::new(); - let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + #[test] + fn test_layout_line_zwnj_insertion() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); - let text = "hello world"; - let font_runs = &[ - FontRun { - font_id, - len: 5, - letter_spacing: None, - }, // "hello" - FontRun { - font_id, - len: 6, - letter_spacing: None, - }, // " world" - ]; + let text = "hello world"; + let font_runs = &[ + FontRun { + font_id, + len: 5, + letter_spacing: None, + }, // "hello" + FontRun { + font_id, + len: 6, + letter_spacing: None, + }, // " world" + ]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); - for run in &layout.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } - // Test with different font runs - should not insert ZWNJ - let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id); - let font_runs_different = &[ - FontRun { - font_id, - len: 5, - letter_spacing: None, - }, // "hello" - // " world" - FontRun { - font_id: font_id2, - len: 6, - letter_spacing: None, - }, - ]; + // Test with different font runs - should not insert ZWNJ + let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id); + let font_runs_different = &[ + FontRun { + font_id, + len: 5, + letter_spacing: None, + }, // "hello" + // " world" + FontRun { + font_id: font_id2, + len: 6, + letter_spacing: None, + }, + ]; - let layout2 = fonts.layout_line(text, px(16.), font_runs_different); - assert_eq!(layout2.len, text.len()); + let layout2 = fonts.layout_line(text, px(16.), font_runs_different); + assert_eq!(layout2.len, text.len()); - for run in &layout2.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } - } + for run in &layout2.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + } - #[test] - fn test_layout_line_zwnj_edge_cases() { - let fonts = MacTextSystem::new(); - let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + #[test] + fn test_layout_line_zwnj_edge_cases() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); - let text = "hello"; - let font_runs = &[FontRun { - font_id, - len: 5, - letter_spacing: None, - }]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); + let text = "hello"; + let font_runs = &[FontRun { + font_id, + len: 5, + letter_spacing: None, + }]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); - let text = "abc"; - let font_runs = &[ - FontRun { - font_id, - len: 1, - letter_spacing: None, - }, // "a" - FontRun { - font_id, - len: 1, - letter_spacing: None, - }, // "b" - FontRun { - font_id, - len: 1, - letter_spacing: None, - }, // "c" - ]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); + let text = "abc"; + let font_runs = &[ + FontRun { + font_id, + len: 1, + letter_spacing: None, + }, // "a" + FontRun { + font_id, + len: 1, + letter_spacing: None, + }, // "b" + FontRun { + font_id, + len: 1, + letter_spacing: None, + }, // "c" + ]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); - for run in &layout.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } - // Test with empty text - let text = ""; - let font_runs = &[]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, 0); - assert!(layout.runs.is_empty()); - } + // Test with empty text + let text = ""; + let font_runs = &[]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, 0); + assert!(layout.runs.is_empty()); + } } diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 1793917861..feba72ac38 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -1,37 +1,37 @@ use crate::{ - BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, TISCopyCurrentKeyboardInputSource, - TISGetInputSourceProperty, events::platform_input_from_native, - kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode, - ns_string, renderer, + BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, TISCopyCurrentKeyboardInputSource, + TISGetInputSourceProperty, events::platform_input_from_native, + kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode, + ns_string, renderer, }; #[cfg(any(test, feature = "test-support"))] use anyhow::Result; use block::ConcreteBlock; use cocoa::{ - appkit::{ - NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered, - NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, - NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial, - NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton, - NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode, - NSWindowStyleMask, NSWindowTitleVisibility, - }, - base::{id, nil}, - foundation::{ - NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound, - NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUInteger, - NSUserDefaults, - }, + appkit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered, + NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, + NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial, + NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton, + NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowStyleMask, NSWindowTitleVisibility, + }, + base::{id, nil}, + foundation::{ + NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound, + NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUInteger, + NSUserDefaults, + }, }; use dispatch2::DispatchQueue; use gpui::{ - AnyWindowHandle, BackgroundExecutor, Bounds, Capslock, CursorStyle, ExternalPaths, - FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, - PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton, - PromptLevel, RequestFrameOptions, SharedString, Size, SystemWindowTab, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, point, - px, size, + AnyWindowHandle, BackgroundExecutor, Bounds, Capslock, CursorStyle, ExternalPaths, + FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton, + PromptLevel, RequestFrameOptions, SharedString, Size, SystemWindowTab, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, point, + px, size, }; #[cfg(any(test, feature = "test-support"))] use image::RgbaImage; @@ -43,29 +43,29 @@ use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect}; use ctor::ctor; use futures::channel::oneshot; use objc::{ - class, - declare::ClassDecl, - msg_send, - runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, - sel, sel_impl, + class, + declare::ClassDecl, + msg_send, + runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, + sel, sel_impl, }; use objc2_app_kit::NSBeep; use parking_lot::Mutex; use raw_window_handle as rwh; use smallvec::SmallVec; use std::{ - cell::Cell, - ffi::{CStr, c_void}, - mem, - ops::Range, - path::PathBuf, - ptr::{self, NonNull}, - rc::Rc, - sync::{ - Arc, Weak, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, + cell::Cell, + ffi::{CStr, c_void}, + mem, + ops::Range, + path::PathBuf, + ptr::{self, NonNull}, + rc::Rc, + sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, }; use util::ResultExt; @@ -78,7 +78,7 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null(); #[allow(non_upper_case_globals)] const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask = - NSWindowStyleMask::from_bits_retain(1 << 7); + NSWindowStyleMask::from_bits_retain(1 << 7); // WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html #[allow(non_upper_case_globals)] const NSNormalWindowLevel: NSInteger = 0; @@ -106,211 +106,211 @@ const NSDragOperationNone: NSDragOperation = 0; const NSDragOperationCopy: NSDragOperation = 1; #[derive(PartialEq)] pub enum UserTabbingPreference { - Never, - Always, - InFullScreen, + Never, + Always, + InFullScreen, } #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { - // Widely used private APIs; Apple uses them for their Terminal.app. - fn CGSMainConnectionID() -> id; - fn CGSSetWindowBackgroundBlurRadius( - connection_id: id, - window_id: NSInteger, - radius: i64, - ) -> i32; + // Widely used private APIs; Apple uses them for their Terminal.app. + fn CGSMainConnectionID() -> id; + fn CGSSetWindowBackgroundBlurRadius( + connection_id: id, + window_id: NSInteger, + radius: i64, + ) -> i32; } #[ctor(unsafe)] unsafe fn build_classes() { - unsafe { - WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); - PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel)); - VIEW_CLASS = { - let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap(); - decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); - decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + unsafe { + WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); + PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel)); + VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(performKeyEquivalent:), - handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(keyDown:), - handle_key_down as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(keyUp:), - handle_key_up as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseDown:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseUp:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(rightMouseDown:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(rightMouseUp:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseDown:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseUp:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseMoved:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(resetCursorRects), - reset_cursor_rects as extern "C" fn(&Object, Sel), - ); - decl.add_method( - sel!(pressureChangeWithEvent:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseExited:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(magnifyWithEvent:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseDragged:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(rightMouseDragged:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseDragged:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(scrollWheel:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(swipeWithEvent:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(flagsChanged:), - handle_view_event as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(performKeyEquivalent:), + handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(keyUp:), + handle_key_up as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(resetCursorRects), + reset_cursor_rects as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(pressureChangeWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseExited:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(magnifyWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(swipeWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(flagsChanged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(makeBackingLayer), - make_backing_layer as extern "C" fn(&Object, Sel) -> id, - ); + decl.add_method( + sel!(makeBackingLayer), + make_backing_layer as extern "C" fn(&Object, Sel) -> id, + ); - decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); - decl.add_method( - sel!(viewDidChangeBackingProperties), - view_did_change_backing_properties as extern "C" fn(&Object, Sel), - ); - decl.add_method( - sel!(setFrameSize:), - set_frame_size as extern "C" fn(&Object, Sel, NSSize), - ); - decl.add_method( - sel!(displayLayer:), - display_layer as extern "C" fn(&Object, Sel, id), - ); + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(viewDidChangeBackingProperties), + view_did_change_backing_properties as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(setFrameSize:), + set_frame_size as extern "C" fn(&Object, Sel, NSSize), + ); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); - decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); - decl.add_method( - sel!(validAttributesForMarkedText), - valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, - ); - decl.add_method( - sel!(hasMarkedText), - has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(markedRange), - marked_range as extern "C" fn(&Object, Sel) -> NSRange, - ); - decl.add_method( - sel!(selectedRange), - selected_range as extern "C" fn(&Object, Sel) -> NSRange, - ); - decl.add_method( - sel!(firstRectForCharacterRange:actualRange:), - first_rect_for_character_range - as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, - ); - decl.add_method( - sel!(insertText:replacementRange:), - insert_text as extern "C" fn(&Object, Sel, id, NSRange), - ); - decl.add_method( - sel!(setMarkedText:selectedRange:replacementRange:), - set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), - ); - decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(attributedSubstringForProposedRange:actualRange:), - attributed_substring_for_proposed_range - as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, - ); - decl.add_method( - sel!(viewDidChangeEffectiveAppearance), - view_did_change_effective_appearance as extern "C" fn(&Object, Sel), - ); + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range + as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + decl.add_method( + sel!(viewDidChangeEffectiveAppearance), + view_did_change_effective_appearance as extern "C" fn(&Object, Sel), + ); - // Suppress beep on keystrokes with modifier keys. - decl.add_method( - sel!(doCommandBySelector:), - do_command_by_selector as extern "C" fn(&Object, Sel, Sel), - ); + // Suppress beep on keystrokes with modifier keys. + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); - decl.add_method( - sel!(acceptsFirstMouse:), - accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, - ); + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); - decl.add_method( - sel!(characterIndexForPoint:), - character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64, - ); - decl.register() - }; - BLURRED_VIEW_CLASS = { - let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap(); - decl.add_method( - sel!(initWithFrame:), - blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id, - ); - decl.add_method( - sel!(updateLayer), - blurred_view_update_layer as extern "C" fn(&Object, Sel), - ); - decl.register() - }; - } + decl.add_method( + sel!(characterIndexForPoint:), + character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64, + ); + decl.register() + }; + BLURRED_VIEW_CLASS = { + let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap(); + decl.add_method( + sel!(initWithFrame:), + blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id, + ); + decl.add_method( + sel!(updateLayer), + blurred_view_update_layer as extern "C" fn(&Object, Sel), + ); + decl.register() + }; + } } pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { - point( - px(position.x as f32), - // macOS screen coordinates are relative to bottom left - window_height - px(position.y as f32), - ) + point( + px(position.x as f32), + // macOS screen coordinates are relative to bottom left + window_height - px(position.y as f32), + ) } /// Stores the cursor style on the active GPUI window and invalidates its cursor rects. @@ -321,335 +321,335 @@ pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) - /// thread because it reads the active AppKit window and updates GPUI window state associated /// with Objective-C objects. pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) { - // SAFETY: The caller guarantees AppKit main-thread access. The class check ensures the - // window has our WINDOW_STATE_IVAR before reading it. - unsafe { - let app = NSApplication::sharedApplication(nil); - let key_window: id = msg_send![app, keyWindow]; - let main_window: id = msg_send![app, mainWindow]; - let active_window = if !key_window.is_null() - && msg_send![key_window, isKindOfClass: WINDOW_CLASS] - { - Some(key_window) - } else if !main_window.is_null() && msg_send![main_window, isKindOfClass: WINDOW_CLASS] { - Some(main_window) - } else { - None - }; + // SAFETY: The caller guarantees AppKit main-thread access. The class check ensures the + // window has our WINDOW_STATE_IVAR before reading it. + unsafe { + let app = NSApplication::sharedApplication(nil); + let key_window: id = msg_send![app, keyWindow]; + let main_window: id = msg_send![app, mainWindow]; + let active_window = if !key_window.is_null() + && msg_send![key_window, isKindOfClass: WINDOW_CLASS] + { + Some(key_window) + } else if !main_window.is_null() && msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + Some(main_window) + } else { + None + }; - let Some(active_window) = active_window else { - return; - }; + let Some(active_window) = active_window else { + return; + }; - let window_state = get_window_state(&*active_window); - let mut window_state = window_state.lock(); - if window_state.cursor_style != style { - window_state.cursor_style = style; - let _: () = msg_send![ - window_state.native_window, - invalidateCursorRectsForView: window_state.native_view.as_ptr() - ]; - } - } + let window_state = get_window_state(&*active_window); + let mut window_state = window_state.lock(); + if window_state.cursor_style != style { + window_state.cursor_style = style; + let _: () = msg_send![ + window_state.native_window, + invalidateCursorRectsForView: window_state.native_view.as_ptr() + ]; + } + } } unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class { - unsafe { - let mut decl = ClassDecl::new(name, superclass).unwrap(); - decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); - decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel)); + unsafe { + let mut decl = ClassDecl::new(name, superclass).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(canBecomeMainWindow), - yes as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(canBecomeKeyWindow), - yes as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(windowDidResize:), - window_did_resize as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidChangeOcclusionState:), - window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillEnterFullScreen:), - window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillExitFullScreen:), - window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidMove:), - window_did_move as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidChangeScreen:), - window_did_change_screen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidBecomeKey:), - window_did_change_key_status as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResignKey:), - window_did_change_key_status as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowShouldClose:), - window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - ); + decl.add_method( + sel!(canBecomeMainWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(canBecomeKeyWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeOcclusionState:), + window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillExitFullScreen:), + window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeScreen:), + window_did_change_screen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, + ); - decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); + decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation, - ); - decl.add_method( - sel!(draggingUpdated:), - dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation, - ); - decl.add_method( - sel!(draggingExited:), - dragging_exited as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(concludeDragOperation:), - conclude_drag_operation as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(addTitlebarAccessoryViewController:), - add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(addTitlebarAccessoryViewController:), + add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(moveTabToNewWindow:), - move_tab_to_new_window as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(moveTabToNewWindow:), + move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(mergeAllWindows:), - merge_all_windows as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(mergeAllWindows:), + merge_all_windows as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(selectNextTab:), - select_next_tab as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(selectNextTab:), + select_next_tab as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(selectPreviousTab:), - select_previous_tab as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(selectPreviousTab:), + select_previous_tab as extern "C" fn(&Object, Sel, id), + ); - decl.add_method( - sel!(toggleTabBar:), - toggle_tab_bar as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(toggleTabBar:), + toggle_tab_bar as extern "C" fn(&Object, Sel, id), + ); - decl.register() - } + decl.register() + } } struct MacWindowState { - handle: AnyWindowHandle, - foreground_executor: ForegroundExecutor, - background_executor: BackgroundExecutor, - native_window: id, - native_view: NonNull, - blurred_view: Option, - background_appearance: WindowBackgroundAppearance, - cursor_style: CursorStyle, - cursor_visible: Arc, - display_link: Option, - renderer: renderer::Renderer, - request_frame_callback: Option>, - event_callback: Option gpui::DispatchEventResult>>, - activate_callback: Option>, - resize_callback: Option, f32)>>, - moved_callback: Option>, - should_close_callback: Option bool>>, - close_callback: Option>, - appearance_changed_callback: Option>, - input_handler: Option, - last_key_equivalent: Option, - synthetic_drag_counter: usize, - traffic_light_position: Option>, - transparent_titlebar: bool, - previous_modifiers_changed_event: Option, - keystroke_for_do_command: Option, - do_command_handled: Option, - external_files_dragged: bool, - // Whether the next left-mouse click is also the focusing click. - first_mouse: bool, - fullscreen_restore_bounds: Bounds, - move_tab_to_new_window_callback: Option>, - merge_all_windows_callback: Option>, - select_next_tab_callback: Option>, - select_previous_tab_callback: Option>, - toggle_tab_bar_callback: Option>, - activated_least_once: bool, - closed: Arc, - accesskit_adapter: Option, - // The parent window if this window is a sheet (Dialog kind) - sheet_parent: Option, + handle: AnyWindowHandle, + foreground_executor: ForegroundExecutor, + background_executor: BackgroundExecutor, + native_window: id, + native_view: NonNull, + blurred_view: Option, + background_appearance: WindowBackgroundAppearance, + cursor_style: CursorStyle, + cursor_visible: Arc, + display_link: Option, + renderer: renderer::Renderer, + request_frame_callback: Option>, + event_callback: Option gpui::DispatchEventResult>>, + activate_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + should_close_callback: Option bool>>, + close_callback: Option>, + appearance_changed_callback: Option>, + input_handler: Option, + last_key_equivalent: Option, + synthetic_drag_counter: usize, + traffic_light_position: Option>, + transparent_titlebar: bool, + previous_modifiers_changed_event: Option, + keystroke_for_do_command: Option, + do_command_handled: Option, + external_files_dragged: bool, + // Whether the next left-mouse click is also the focusing click. + first_mouse: bool, + fullscreen_restore_bounds: Bounds, + move_tab_to_new_window_callback: Option>, + merge_all_windows_callback: Option>, + select_next_tab_callback: Option>, + select_previous_tab_callback: Option>, + toggle_tab_bar_callback: Option>, + activated_least_once: bool, + closed: Arc, + accesskit_adapter: Option, + // The parent window if this window is a sheet (Dialog kind) + sheet_parent: Option, } impl MacWindowState { - fn move_traffic_light(&self) { - if let Some(traffic_light_position) = self.traffic_light_position { - if self.is_fullscreen() { - // Moving traffic lights while fullscreen doesn't work, - // see https://github.com/zed-industries/zed/issues/4712 - return; - } + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + if self.is_fullscreen() { + // Moving traffic lights while fullscreen doesn't work, + // see https://github.com/zed-industries/zed/issues/4712 + return; + } - let titlebar_height = self.titlebar_height(); + let titlebar_height = self.titlebar_height(); - unsafe { - let close_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowCloseButton - ]; - let min_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton - ]; - let zoom_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowZoomButton - ]; + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; - let mut close_button_frame: CGRect = msg_send![close_button, frame]; - let mut min_button_frame: CGRect = msg_send![min_button, frame]; - let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; - let mut origin = point( - traffic_light_position.x, - titlebar_height - - traffic_light_position.y - - px(close_button_frame.size.height as f32), - ); - let button_spacing = - px((min_button_frame.origin.x - close_button_frame.origin.x) as f32); + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = point( + traffic_light_position.x, + titlebar_height + - traffic_light_position.y + - px(close_button_frame.size.height as f32), + ); + let button_spacing = + px((min_button_frame.origin.x - close_button_frame.origin.x) as f32); - close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); - let _: () = msg_send![close_button, setFrame: close_button_frame]; - origin.x += button_spacing; + close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.x += button_spacing; - min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); - let _: () = msg_send![min_button, setFrame: min_button_frame]; - origin.x += button_spacing; + min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.x += button_spacing; - zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); - let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; - origin.x += button_spacing; - } - } - } + zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + origin.x += button_spacing; + } + } + } - fn start_display_link(&mut self) { - self.stop_display_link(); - unsafe { - if !self - .native_window - .occlusionState() - .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) - { - return; - } - } - let display_id = unsafe { display_id_for_screen(self.native_window.screen()) }; - if let Some(mut display_link) = - DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err() - { - display_link.start().log_err(); - self.display_link = Some(display_link); - } - } + fn start_display_link(&mut self) { + self.stop_display_link(); + unsafe { + if !self + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + return; + } + } + let display_id = unsafe { display_id_for_screen(self.native_window.screen()) }; + if let Some(mut display_link) = + DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err() + { + display_link.start().log_err(); + self.display_link = Some(display_link); + } + } - fn stop_display_link(&mut self) { - self.display_link = None; - } + fn stop_display_link(&mut self) { + self.display_link = None; + } - fn is_maximized(&self) -> bool { - fn rect_to_size(rect: NSRect) -> Size { - let NSSize { width, height } = rect.size; - size(width.into(), height.into()) - } + fn is_maximized(&self) -> bool { + fn rect_to_size(rect: NSRect) -> Size { + let NSSize { width, height } = rect.size; + size(width.into(), height.into()) + } - unsafe { - let bounds = self.bounds(); - let screen_size = rect_to_size(self.native_window.screen().visibleFrame()); - bounds.size == screen_size - } - } + unsafe { + let bounds = self.bounds(); + let screen_size = rect_to_size(self.native_window.screen().visibleFrame()); + bounds.size == screen_size + } + } - fn is_fullscreen(&self) -> bool { - unsafe { - let style_mask = self.native_window.styleMask(); - style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) - } - } + fn is_fullscreen(&self) -> bool { + unsafe { + let style_mask = self.native_window.styleMask(); + style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } - fn bounds(&self) -> Bounds { - let mut window_frame = unsafe { NSWindow::frame(self.native_window) }; - let screen = unsafe { NSWindow::screen(self.native_window) }; - if screen == nil { - return Bounds::new(point(px(0.), px(0.)), gpui::DEFAULT_WINDOW_SIZE); - } - let screen_frame = unsafe { NSScreen::frame(screen) }; + fn bounds(&self) -> Bounds { + let mut window_frame = unsafe { NSWindow::frame(self.native_window) }; + let screen = unsafe { NSWindow::screen(self.native_window) }; + if screen == nil { + return Bounds::new(point(px(0.), px(0.)), gpui::DEFAULT_WINDOW_SIZE); + } + let screen_frame = unsafe { NSScreen::frame(screen) }; - // Flip the y coordinate to be top-left origin - window_frame.origin.y = - screen_frame.size.height - window_frame.origin.y - window_frame.size.height; + // Flip the y coordinate to be top-left origin + window_frame.origin.y = + screen_frame.size.height - window_frame.origin.y - window_frame.size.height; - Bounds::new( - point( - px((window_frame.origin.x - screen_frame.origin.x) as f32), - px((window_frame.origin.y + screen_frame.origin.y) as f32), - ), - size( - px(window_frame.size.width as f32), - px(window_frame.size.height as f32), - ), - ) - } + Bounds::new( + point( + px((window_frame.origin.x - screen_frame.origin.x) as f32), + px((window_frame.origin.y + screen_frame.origin.y) as f32), + ), + size( + px(window_frame.size.width as f32), + px(window_frame.size.height as f32), + ), + ) + } - fn content_size(&self) -> Size { - let NSSize { width, height, .. } = - unsafe { NSView::frame(self.native_window.contentView()) }.size; - size(px(width as f32), px(height as f32)) - } + fn content_size(&self) -> Size { + let NSSize { width, height, .. } = + unsafe { NSView::frame(self.native_window.contentView()) }.size; + size(px(width as f32), px(height as f32)) + } - fn scale_factor(&self) -> f32 { - get_scale_factor(self.native_window) - } + fn scale_factor(&self) -> f32 { + get_scale_factor(self.native_window) + } - fn titlebar_height(&self) -> Pixels { - unsafe { - let frame = NSWindow::frame(self.native_window); - let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; - px((frame.size.height - content_layout_rect.size.height) as f32) - } - } + fn titlebar_height(&self) -> Pixels { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + px((frame.size.height - content_layout_rect.size.height) as f32) + } + } - fn window_bounds(&self) -> WindowBounds { - if self.is_fullscreen() { - WindowBounds::Fullscreen(self.fullscreen_restore_bounds) - } else { - WindowBounds::Windowed(self.bounds()) - } - } + fn window_bounds(&self) -> WindowBounds { + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else { + WindowBounds::Windowed(self.bounds()) + } + } } unsafe impl Send for MacWindowState {} @@ -657,434 +657,434 @@ unsafe impl Send for MacWindowState {} pub(crate) struct MacWindow(Arc>); impl MacWindow { - pub fn open( - handle: AnyWindowHandle, - WindowParams { - bounds, - titlebar, - kind, - is_movable, - is_resizable, - is_minimizable, - focus, - show, - display_id, - window_min_size, - tabbing_identifier, - .. - }: WindowParams, - cursor_visible: Arc, - foreground_executor: ForegroundExecutor, - background_executor: BackgroundExecutor, - renderer_context: renderer::Context, - ) -> Self { - unsafe { - let pool = NSAutoreleasePool::new(nil); + pub fn open( + handle: AnyWindowHandle, + WindowParams { + bounds, + titlebar, + kind, + is_movable, + is_resizable, + is_minimizable, + focus, + show, + display_id, + window_min_size, + tabbing_identifier, + .. + }: WindowParams, + cursor_visible: Arc, + foreground_executor: ForegroundExecutor, + background_executor: BackgroundExecutor, + renderer_context: renderer::Context, + ) -> Self { + unsafe { + let pool = NSAutoreleasePool::new(nil); - let allows_automatic_window_tabbing = tabbing_identifier.is_some(); - if allows_automatic_window_tabbing { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; - } else { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; - } + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } - let mut style_mask; - if let Some(titlebar) = titlebar.as_ref() { - style_mask = - NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask; + let mut style_mask; + if let Some(titlebar) = titlebar.as_ref() { + style_mask = + NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask; - if is_resizable { - style_mask |= NSWindowStyleMask::NSResizableWindowMask; - } + if is_resizable { + style_mask |= NSWindowStyleMask::NSResizableWindowMask; + } - if is_minimizable { - style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask; - } + if is_minimizable { + style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask; + } - if titlebar.appears_transparent { - style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; - } - } else { - style_mask = NSWindowStyleMask::NSTitledWindowMask - | NSWindowStyleMask::NSFullSizeContentViewWindowMask; - } + if titlebar.appears_transparent { + style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + } else { + style_mask = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } - let native_window: id = match kind { - WindowKind::Normal => { - msg_send![WINDOW_CLASS, alloc] - } - WindowKind::PopUp => { - style_mask |= NSWindowStyleMaskNonactivatingPanel; - msg_send![PANEL_CLASS, alloc] - } - WindowKind::Floating | WindowKind::Dialog => { - msg_send![PANEL_CLASS, alloc] - } - }; + let native_window: id = match kind { + WindowKind::Normal => { + msg_send![WINDOW_CLASS, alloc] + } + WindowKind::PopUp => { + style_mask |= NSWindowStyleMaskNonactivatingPanel; + msg_send![PANEL_CLASS, alloc] + } + WindowKind::Floating | WindowKind::Dialog => { + msg_send![PANEL_CLASS, alloc] + } + }; - let display = display_id - .and_then(MacDisplay::find_by_id) - .unwrap_or_else(MacDisplay::primary); + let display = display_id + .and_then(MacDisplay::find_by_id) + .unwrap_or_else(MacDisplay::primary); - let mut target_screen = nil; - let mut screen_frame = None; + let mut target_screen = nil; + let mut screen_frame = None; - let screens = NSScreen::screens(nil); - let count: u64 = cocoa::foundation::NSArray::count(screens); - for i in 0..count { - let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i); - let frame = NSScreen::frame(screen); - let display_id = display_id_for_screen(screen); - if display_id == display.0 { - screen_frame = Some(frame); - target_screen = screen; - } - } + let screens = NSScreen::screens(nil); + let count: u64 = cocoa::foundation::NSArray::count(screens); + for i in 0..count { + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i); + let frame = NSScreen::frame(screen); + let display_id = display_id_for_screen(screen); + if display_id == display.0 { + screen_frame = Some(frame); + target_screen = screen; + } + } - let screen_frame = screen_frame.unwrap_or_else(|| { - let screen = NSScreen::mainScreen(nil); - target_screen = screen; - NSScreen::frame(screen) - }); + let screen_frame = screen_frame.unwrap_or_else(|| { + let screen = NSScreen::mainScreen(nil); + target_screen = screen; + NSScreen::frame(screen) + }); - let window_rect = NSRect::new( - NSPoint::new( - screen_frame.origin.x + bounds.origin.x.as_f32() as f64, - screen_frame.origin.y - + (display.bounds().size.height - bounds.origin.y).as_f32() as f64, - ), - NSSize::new( - bounds.size.width.as_f32() as f64, - bounds.size.height.as_f32() as f64, - ), - ); + let window_rect = NSRect::new( + NSPoint::new( + screen_frame.origin.x + bounds.origin.x.as_f32() as f64, + screen_frame.origin.y + + (display.bounds().size.height - bounds.origin.y).as_f32() as f64, + ), + NSSize::new( + bounds.size.width.as_f32() as f64, + bounds.size.height.as_f32() as f64, + ), + ); - let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( - window_rect, - style_mask, - NSBackingStoreBuffered, - NO, - target_screen, - ); - assert!(!native_window.is_null()); - let () = msg_send![ - native_window, - registerForDraggedTypes: - NSArray::arrayWithObject(nil, NSFilenamesPboardType) - ]; - let () = msg_send![ - native_window, - setReleasedWhenClosed: NO - ]; + let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( + window_rect, + style_mask, + NSBackingStoreBuffered, + NO, + target_screen, + ); + assert!(!native_window.is_null()); + let () = msg_send![ + native_window, + registerForDraggedTypes: + NSArray::arrayWithObject(nil, NSFilenamesPboardType) + ]; + let () = msg_send![ + native_window, + setReleasedWhenClosed: NO + ]; - let content_view = native_window.contentView(); - let native_view: id = msg_send![VIEW_CLASS, alloc]; - let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view)); - assert!(!native_view.is_null()); + let content_view = native_window.contentView(); + let native_view: id = msg_send![VIEW_CLASS, alloc]; + let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view)); + assert!(!native_view.is_null()); - let mut window = Self(Arc::new(Mutex::new(MacWindowState { - handle, - foreground_executor, - background_executor, - native_window, - native_view: NonNull::new_unchecked(native_view), - blurred_view: None, - background_appearance: WindowBackgroundAppearance::Opaque, - cursor_style: CursorStyle::Arrow, - cursor_visible, - display_link: None, - renderer: renderer::new_renderer( - renderer_context, - native_window as *mut _, - native_view as *mut _, - bounds.size.map(|pixels| pixels.as_f32()), - false, - ), - request_frame_callback: None, - event_callback: None, - activate_callback: None, - resize_callback: None, - moved_callback: None, - should_close_callback: None, - close_callback: None, - appearance_changed_callback: None, - input_handler: None, - last_key_equivalent: None, - synthetic_drag_counter: 0, - traffic_light_position: titlebar - .as_ref() - .and_then(|titlebar| titlebar.traffic_light_position), - transparent_titlebar: titlebar - .as_ref() - .is_none_or(|titlebar| titlebar.appears_transparent), - previous_modifiers_changed_event: None, - keystroke_for_do_command: None, - do_command_handled: None, - external_files_dragged: false, - first_mouse: false, - fullscreen_restore_bounds: Bounds::default(), - move_tab_to_new_window_callback: None, - merge_all_windows_callback: None, - select_next_tab_callback: None, - select_previous_tab_callback: None, - toggle_tab_bar_callback: None, - activated_least_once: false, - closed: Arc::new(AtomicBool::new(false)), - accesskit_adapter: None, - sheet_parent: None, - }))); + let mut window = Self(Arc::new(Mutex::new(MacWindowState { + handle, + foreground_executor, + background_executor, + native_window, + native_view: NonNull::new_unchecked(native_view), + blurred_view: None, + background_appearance: WindowBackgroundAppearance::Opaque, + cursor_style: CursorStyle::Arrow, + cursor_visible, + display_link: None, + renderer: renderer::new_renderer( + renderer_context, + native_window as *mut _, + native_view as *mut _, + bounds.size.map(|pixels| pixels.as_f32()), + false, + ), + request_frame_callback: None, + event_callback: None, + activate_callback: None, + resize_callback: None, + moved_callback: None, + should_close_callback: None, + close_callback: None, + appearance_changed_callback: None, + input_handler: None, + last_key_equivalent: None, + synthetic_drag_counter: 0, + traffic_light_position: titlebar + .as_ref() + .and_then(|titlebar| titlebar.traffic_light_position), + transparent_titlebar: titlebar + .as_ref() + .is_none_or(|titlebar| titlebar.appears_transparent), + previous_modifiers_changed_event: None, + keystroke_for_do_command: None, + do_command_handled: None, + external_files_dragged: false, + first_mouse: false, + fullscreen_restore_bounds: Bounds::default(), + move_tab_to_new_window_callback: None, + merge_all_windows_callback: None, + select_next_tab_callback: None, + select_previous_tab_callback: None, + toggle_tab_bar_callback: None, + activated_least_once: false, + closed: Arc::new(AtomicBool::new(false)), + accesskit_adapter: None, + sheet_parent: None, + }))); - (*native_window).set_ivar( - WINDOW_STATE_IVAR, - Arc::into_raw(window.0.clone()) as *const c_void, - ); - native_window.setDelegate_(native_window); - (*native_view).set_ivar( - WINDOW_STATE_IVAR, - Arc::into_raw(window.0.clone()) as *const c_void, - ); + (*native_window).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); + native_window.setDelegate_(native_window); + (*native_view).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); - if let Some(title) = titlebar - .as_ref() - .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) - { - window.set_title(title); - } + if let Some(title) = titlebar + .as_ref() + .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) + { + window.set_title(title); + } - native_window.setMovable_(is_movable as BOOL); + native_window.setMovable_(is_movable as BOOL); - if let Some(window_min_size) = window_min_size { - native_window.setContentMinSize_(NSSize { - width: window_min_size.width.to_f64(), - height: window_min_size.height.to_f64(), - }); - } + if let Some(window_min_size) = window_min_size { + native_window.setContentMinSize_(NSSize { + width: window_min_size.width.to_f64(), + height: window_min_size.height.to_f64(), + }); + } - if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { - native_window.setTitlebarAppearsTransparent_(YES); - native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); - } + if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { + native_window.setTitlebarAppearsTransparent_(YES); + native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); + } - native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); - native_view.setWantsBestResolutionOpenGLSurface_(YES); + native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + native_view.setWantsBestResolutionOpenGLSurface_(YES); - // From winit crate: On Mojave, views automatically become layer-backed shortly after - // being added to a native_window. Changing the layer-backedness of a view breaks the - // association between the view and its associated OpenGL context. To work around this, - // on we explicitly make the view layer-backed up front so that AppKit doesn't do it - // itself and break the association with its context. - native_view.setWantsLayer(YES); - let _: () = msg_send![ - native_view, - setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize - ]; + // From winit crate: On Mojave, views automatically become layer-backed shortly after + // being added to a native_window. Changing the layer-backedness of a view breaks the + // association between the view and its associated OpenGL context. To work around this, + // on we explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; - content_view.addSubview_(native_view.autorelease()); - native_window.makeFirstResponder_(native_view); + content_view.addSubview_(native_view.autorelease()); + native_window.makeFirstResponder_(native_view); - let app: id = NSApplication::sharedApplication(nil); - let main_window: id = msg_send![app, mainWindow]; - let mut sheet_parent = None; + let app: id = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + let mut sheet_parent = None; - match kind { - WindowKind::Normal | WindowKind::Floating => { - if kind == WindowKind::Floating { - // Let the window float keep above normal windows. - native_window.setLevel_(NSFloatingWindowLevel); - } else { - native_window.setLevel_(NSNormalWindowLevel); - } - native_window.setAcceptsMouseMovedEvents_(YES); + match kind { + WindowKind::Normal | WindowKind::Floating => { + if kind == WindowKind::Floating { + // Let the window float keep above normal windows. + native_window.setLevel_(NSFloatingWindowLevel); + } else { + native_window.setLevel_(NSNormalWindowLevel); + } + native_window.setAcceptsMouseMovedEvents_(YES); - if let Some(tabbing_identifier) = tabbing_identifier { - let tabbing_id = ns_string(tabbing_identifier.as_str()); - let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; - } else { - let _: () = msg_send![native_window, setTabbingIdentifier:nil]; - } - } - WindowKind::PopUp => { - // Use a tracking area to allow receiving MouseMoved events even when - // the window or application aren't active, which is often the case - // e.g. for notification windows. - let tracking_area: id = msg_send![class!(NSTrackingArea), alloc]; - let _: () = msg_send![ - tracking_area, - initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) - options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect - owner: native_view - userInfo: nil - ]; - let _: () = - msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = ns_string(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + WindowKind::PopUp => { + // Use a tracking area to allow receiving MouseMoved events even when + // the window or application aren't active, which is often the case + // e.g. for notification windows. + let tracking_area: id = msg_send![class!(NSTrackingArea), alloc]; + let _: () = msg_send![ + tracking_area, + initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) + options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect + owner: native_view + userInfo: nil + ]; + let _: () = + msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; - native_window.setLevel_(NSPopUpWindowLevel); - let _: () = msg_send![ - native_window, - setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow - ]; - native_window.setCollectionBehavior_( + native_window.setLevel_(NSPopUpWindowLevel); + let _: () = msg_send![ + native_window, + setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow + ]; + native_window.setCollectionBehavior_( NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary ); - } - WindowKind::Dialog => { - if !main_window.is_null() { - let parent = { - let active_sheet: id = msg_send![main_window, attachedSheet]; - if active_sheet.is_null() { - main_window - } else { - active_sheet - } - }; - let _: () = - msg_send![parent, beginSheet: native_window completionHandler: nil]; - sheet_parent = Some(parent); - } - } - } + } + WindowKind::Dialog => { + if !main_window.is_null() { + let parent = { + let active_sheet: id = msg_send![main_window, attachedSheet]; + if active_sheet.is_null() { + main_window + } else { + active_sheet + } + }; + let _: () = + msg_send![parent, beginSheet: native_window completionHandler: nil]; + sheet_parent = Some(parent); + } + } + } - if allows_automatic_window_tabbing - && !main_window.is_null() - && main_window != native_window - { - let main_window_is_fullscreen = main_window - .styleMask() - .contains(NSWindowStyleMask::NSFullScreenWindowMask); - let user_tabbing_preference = Self::get_user_tabbing_preference() - .unwrap_or(UserTabbingPreference::InFullScreen); - let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always - || user_tabbing_preference == UserTabbingPreference::InFullScreen - && main_window_is_fullscreen; + if allows_automatic_window_tabbing + && !main_window.is_null() + && main_window != native_window + { + let main_window_is_fullscreen = main_window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask); + let user_tabbing_preference = Self::get_user_tabbing_preference() + .unwrap_or(UserTabbingPreference::InFullScreen); + let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always + || user_tabbing_preference == UserTabbingPreference::InFullScreen + && main_window_is_fullscreen; - if should_add_as_tab { - let main_window_can_tab: BOOL = - msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; - let main_window_visible: BOOL = msg_send![main_window, isVisible]; + if should_add_as_tab { + let main_window_can_tab: BOOL = + msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; + let main_window_visible: BOOL = msg_send![main_window, isVisible]; - if main_window_can_tab == YES && main_window_visible == YES { - let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; + if main_window_can_tab == YES && main_window_visible == YES { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; - // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point. - // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen. - if !main_window_is_fullscreen { - let _: () = msg_send![native_window, orderFront: nil]; - } - } - } - } + // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point. + // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen. + if !main_window_is_fullscreen { + let _: () = msg_send![native_window, orderFront: nil]; + } + } + } + } - if focus && show { - native_window.makeKeyAndOrderFront_(nil); - } else if show { - native_window.orderFront_(nil); - } + if focus && show { + native_window.makeKeyAndOrderFront_(nil); + } else if show { + native_window.orderFront_(nil); + } - // Set the initial position of the window to the specified origin. - // Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`, - // the window position might be incorrect if the main screen (the screen that contains the window that has focus) - // is different from the primary screen. - NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin); - { - let mut window_state = window.0.lock(); - window_state.move_traffic_light(); - window_state.sheet_parent = sheet_parent; - } + // Set the initial position of the window to the specified origin. + // Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`, + // the window position might be incorrect if the main screen (the screen that contains the window that has focus) + // is different from the primary screen. + NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin); + { + let mut window_state = window.0.lock(); + window_state.move_traffic_light(); + window_state.sheet_parent = sheet_parent; + } - pool.drain(); + pool.drain(); - window - } - } + window + } + } - pub fn active_window() -> Option { - unsafe { - let app = NSApplication::sharedApplication(nil); - let main_window: id = msg_send![app, mainWindow]; - if main_window.is_null() { - return None; - } + pub fn active_window() -> Option { + unsafe { + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if main_window.is_null() { + return None; + } - if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { - let handle = get_window_state(&*main_window).lock().handle; - Some(handle) - } else { - None - } - } - } + if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*main_window).lock().handle; + Some(handle) + } else { + None + } + } + } - pub fn ordered_windows() -> Vec { - unsafe { - let app = NSApplication::sharedApplication(nil); - let windows: id = msg_send![app, orderedWindows]; - let count: NSUInteger = msg_send![windows, count]; + pub fn ordered_windows() -> Vec { + unsafe { + let app = NSApplication::sharedApplication(nil); + let windows: id = msg_send![app, orderedWindows]; + let count: NSUInteger = msg_send![windows, count]; - let mut window_handles = Vec::new(); - for i in 0..count { - let window: id = msg_send![windows, objectAtIndex:i]; - if msg_send![window, isKindOfClass: WINDOW_CLASS] { - let handle = get_window_state(&*window).lock().handle; - window_handles.push(handle); - } - } + let mut window_handles = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + window_handles.push(handle); + } + } - window_handles - } - } + window_handles + } + } - pub fn get_user_tabbing_preference() -> Option { - unsafe { - let defaults: id = NSUserDefaults::standardUserDefaults(); - let domain = ns_string("NSGlobalDomain"); - let key = ns_string("AppleWindowTabbingMode"); + pub fn get_user_tabbing_preference() -> Option { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = ns_string("NSGlobalDomain"); + let key = ns_string("AppleWindowTabbingMode"); - let dict: id = msg_send![defaults, persistentDomainForName: domain]; - let value: id = if !dict.is_null() { - msg_send![dict, objectForKey: key] - } else { - nil - }; + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let value: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; - let value_str = if !value.is_null() { - CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() - } else { - "".into() - }; + let value_str = if !value.is_null() { + CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() + } else { + "".into() + }; - match value_str.as_ref() { - "manual" => Some(UserTabbingPreference::Never), - "always" => Some(UserTabbingPreference::Always), - _ => Some(UserTabbingPreference::InFullScreen), - } - } - } + match value_str.as_ref() { + "manual" => Some(UserTabbingPreference::Never), + "always" => Some(UserTabbingPreference::Always), + _ => Some(UserTabbingPreference::InFullScreen), + } + } + } } impl Drop for MacWindow { - fn drop(&mut self) { - let mut this = self.0.lock(); - this.renderer.destroy(); - let window = this.native_window; - let sheet_parent = this.sheet_parent.take(); - this.display_link.take(); - unsafe { - this.native_window.setDelegate_(nil); - } - this.input_handler.take(); - this.foreground_executor - .spawn(async move { - unsafe { - if let Some(parent) = sheet_parent { - let _: () = msg_send![parent, endSheet: window]; - } - window.close(); - window.autorelease(); - } - }) - .detach(); - } + fn drop(&mut self) { + let mut this = self.0.lock(); + this.renderer.destroy(); + let window = this.native_window; + let sheet_parent = this.sheet_parent.take(); + this.display_link.take(); + unsafe { + this.native_window.setDelegate_(nil); + } + this.input_handler.take(); + this.foreground_executor + .spawn(async move { + unsafe { + if let Some(parent) = sheet_parent { + let _: () = msg_send![parent, endSheet: window]; + } + window.close(); + window.autorelease(); + } + }) + .detach(); + } } /// Calls `f` if the window is not closed. @@ -1093,831 +1093,831 @@ impl Drop for MacWindow { /// window, as some messages will end hard faulting if dispatched to no longer /// valid window handles. fn if_window_not_closed(closed: Arc, f: impl FnOnce()) { - if !closed.load(Ordering::Acquire) { - f(); - } + if !closed.load(Ordering::Acquire) { + f(); + } } impl PlatformWindow for MacWindow { - fn bounds(&self) -> Bounds { - self.0.as_ref().lock().bounds() - } - - fn window_bounds(&self) -> WindowBounds { - self.0.as_ref().lock().window_bounds() - } - - fn is_maximized(&self) -> bool { - self.0.as_ref().lock().is_maximized() - } - - fn content_size(&self) -> Size { - self.0.as_ref().lock().content_size() - } - - fn resize(&mut self, size: Size) { - let this = self.0.lock(); - let window = this.native_window; - let closed = this.closed.clone(); - this.foreground_executor - .spawn(async move { - if_window_not_closed(closed, || unsafe { - window.setContentSize_(NSSize { - width: size.width.as_f32() as f64, - height: size.height.as_f32() as f64, - }); - }) - }) - .detach(); - } - - fn merge_all_windows(&self) { - let native_window = self.0.lock().native_window; - extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) { - unsafe { - let native_window = context as id; - let _: () = msg_send![native_window, mergeAllWindows:nil]; - } - } - - unsafe { - DispatchQueue::main() - .exec_async_f(native_window as *mut std::ffi::c_void, merge_windows_async); - } - } - - fn move_tab_to_new_window(&self) { - let native_window = self.0.lock().native_window; - extern "C" fn move_tab_async(context: *mut std::ffi::c_void) { - unsafe { - let native_window = context as id; - let _: () = msg_send![native_window, moveTabToNewWindow:nil]; - let _: () = msg_send![native_window, makeKeyAndOrderFront: nil]; - } - } - - unsafe { - DispatchQueue::main() - .exec_async_f(native_window as *mut std::ffi::c_void, move_tab_async); - } - } - - fn toggle_window_tab_overview(&self) { - let native_window = self.0.lock().native_window; - unsafe { - let _: () = msg_send![native_window, toggleTabOverview:nil]; - } - } - - fn set_tabbing_identifier(&self, tabbing_identifier: Option) { - let native_window = self.0.lock().native_window; - unsafe { - let allows_automatic_window_tabbing = tabbing_identifier.is_some(); - if allows_automatic_window_tabbing { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; - } else { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; - } - - if let Some(tabbing_identifier) = tabbing_identifier { - let tabbing_id = ns_string(tabbing_identifier.as_str()); - let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; - } else { - let _: () = msg_send![native_window, setTabbingIdentifier:nil]; - } - } - } - - fn scale_factor(&self) -> f32 { - self.0.as_ref().lock().scale_factor() - } - - fn appearance(&self) -> WindowAppearance { - unsafe { - let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance]; - crate::window_appearance::window_appearance_from_native(appearance) - } - } - - fn display(&self) -> Option> { - unsafe { - let screen = self.0.lock().native_window.screen(); - if screen.is_null() { - return None; - } - let device_description: id = msg_send![screen, deviceDescription]; - let screen_number: id = - NSDictionary::valueForKey_(device_description, ns_string("NSScreenNumber")); - - let screen_number: u32 = msg_send![screen_number, unsignedIntValue]; - - Some(Rc::new(MacDisplay(screen_number))) - } - } - - fn mouse_position(&self) -> Point { - let position = unsafe { - self.0 - .lock() - .native_window - .mouseLocationOutsideOfEventStream() - }; - convert_mouse_position(position, self.content_size().height) - } - - fn modifiers(&self) -> Modifiers { - unsafe { - let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; - - let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - - Modifiers { - control, - alt, - shift, - platform: command, - function, - } - } - } - - fn capslock(&self) -> Capslock { - unsafe { - let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; - - Capslock { - on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask), - } - } - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.as_ref().lock().input_handler = Some(input_handler); - } - - fn take_input_handler(&mut self) -> Option { - self.0.as_ref().lock().input_handler.take() - } - - fn prompt( - &self, - level: PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option> { - // NSAlert's first button keeps Return and Cancel keeps Escape, but the keyboard - // focus (and therefore Space) defaults to Cancel, leaving the middle button of - // prompts like "Save / Don't Save / Cancel" unreachable from the keyboard. Move - // the initial focus onto the last non-cancel, non-default button instead. - let initial_focus_ix = answers - .iter() - .enumerate() - .rev() - .find(|(_, label)| !label.is_cancel()) - .map(|(ix, _)| ix) - .filter(|&ix| ix > 0); - - unsafe { - let alert: id = msg_send![class!(NSAlert), alloc]; - let alert: id = msg_send![alert, init]; - let alert_style = match level { - PromptLevel::Info => 1, - PromptLevel::Warning => 0, - PromptLevel::Critical => 2, - }; - let _: () = msg_send![alert, setAlertStyle: alert_style]; - let _: () = msg_send![alert, setMessageText: ns_string(msg)]; - if let Some(detail) = detail { - let _: () = msg_send![alert, setInformativeText: ns_string(detail)]; - } - - let mut initial_focus_button: Option = None; - for (ix, answer) in answers.iter().enumerate() { - let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())]; - let _: () = msg_send![button, setTag: ix as NSInteger]; - - if answer.is_cancel() { - if let Some(key) = std::char::from_u32(crate::events::ESCAPE_KEY as u32) { - let _: () = - msg_send![button, setKeyEquivalent: ns_string(&key.to_string())]; - } - } else if Some(ix) == initial_focus_ix { - initial_focus_button = Some(button); - } - } - - if let Some(button) = initial_focus_button { - let alert_window: id = msg_send![alert, window]; - let _: () = msg_send![alert_window, setInitialFirstResponder: button]; - } - - let (done_tx, done_rx) = oneshot::channel(); - let done_tx = Cell::new(Some(done_tx)); - let block = ConcreteBlock::new(move |answer: NSInteger| { - let _: () = msg_send![alert, release]; - if let Some(done_tx) = done_tx.take() { - let _ = done_tx.send(answer.try_into().unwrap()); - } - }); - let block = block.copy(); - let lock = self.0.lock(); - let native_window = lock.native_window; - let closed = lock.closed.clone(); - let executor = lock.foreground_executor.clone(); - executor - .spawn(async move { - if !closed.load(Ordering::Acquire) { - let _: () = msg_send![ - alert, - beginSheetModalForWindow: native_window - completionHandler: block - ]; - } else { - let _: () = msg_send![alert, release]; - } - }) - .detach(); - - Some(done_rx) - } - } - - fn activate(&self) { - let lock = self.0.lock(); - let window = lock.native_window; - let closed = lock.closed.clone(); - let executor = lock.foreground_executor.clone(); - executor - .spawn(async move { - if !closed.load(Ordering::Acquire) { - unsafe { - let _: () = msg_send![window, makeKeyAndOrderFront: nil]; - } - } - }) - .detach(); - } - - fn is_active(&self) -> bool { - unsafe { self.0.lock().native_window.isKeyWindow() == YES } - } - - // is_hovered is unused on macOS. See Window::is_window_hovered. - fn is_hovered(&self) -> bool { - false - } - - fn set_title(&mut self, title: &str) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let window = self.0.lock().native_window; - let title = ns_string(title); - let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; - let _: () = msg_send![window, setTitle: title]; - self.0.lock().move_traffic_light(); - } - } - - fn get_title(&self) -> String { - unsafe { - let title: id = msg_send![self.0.lock().native_window, title]; - if title.is_null() { - "".to_string() - } else { - title.to_str().to_string() - } - } - } - - fn set_app_id(&mut self, _app_id: &str) {} - - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - let mut this = self.0.as_ref().lock(); - this.background_appearance = background_appearance; - - let opaque = background_appearance == WindowBackgroundAppearance::Opaque; - this.renderer.update_transparency(!opaque); - - unsafe { - this.native_window.setOpaque_(opaque as BOOL); - let background_color = if opaque { - NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) - } else { - // Not using `+[NSColor clearColor]` to avoid broken shadow. - NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) - }; - this.native_window.setBackgroundColor_(background_color); - - if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 { - // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`. - // On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers - // but uses a `CAProxyLayer`. Use the legacy WindowServer API. - let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { - 80 - } else { - 0 - }; - - let window_number = this.native_window.windowNumber(); - CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); - } else { - // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it - // could have a better performance (it downsamples the backdrop) and more control - // over the effect layer. - if background_appearance != WindowBackgroundAppearance::Blurred { - if let Some(blur_view) = this.blurred_view { - NSView::removeFromSuperview(blur_view); - this.blurred_view = None; - } - } else if this.blurred_view.is_none() { - let content_view = this.native_window.contentView(); - let frame = NSView::bounds(content_view); - let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; - blur_view = NSView::initWithFrame_(blur_view, frame); - blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); - - let _: () = msg_send![ - content_view, - addSubview: blur_view - positioned: NSWindowOrderingMode::NSWindowBelow - relativeTo: nil - ]; - this.blurred_view = Some(blur_view.autorelease()); - } - } - } - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - self.0.as_ref().lock().background_appearance - } - - fn is_subpixel_rendering_supported(&self) -> bool { - false - } - - fn set_edited(&mut self, edited: bool) { - unsafe { - let window = self.0.lock().native_window; - msg_send![window, setDocumentEdited: edited as BOOL] - } - - // Changing the document edited state resets the traffic light position, - // so we have to move it again. - self.0.lock().move_traffic_light(); - } - - fn set_document_path(&self, path: Option<&std::path::Path>) { - unsafe { - let window = self.0.lock().native_window; - let filename = path.map_or(ns_string(""), |p| ns_string(&p.to_string_lossy())); - let _: () = msg_send![window, setRepresentedFilename: filename]; - } - - // Changing the document path state resets the traffic light position, - // so we have to move it again. - self.0.lock().move_traffic_light(); - } - - fn show_character_palette(&self) { - let this = self.0.lock(); - let window = this.native_window; - this.foreground_executor - .spawn(async move { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, orderFrontCharacterPalette: window]; - } - }) - .detach(); - } - - fn minimize(&self) { - let window = self.0.lock().native_window; - unsafe { - window.miniaturize_(nil); - } - } - - fn zoom(&self) { - let this = self.0.lock(); - let window = this.native_window; - let closed = this.closed.clone(); - this.foreground_executor - .spawn(async move { - if_window_not_closed(closed, || unsafe { - window.zoom_(nil); - }) - }) - .detach(); - } - - fn toggle_fullscreen(&self) { - let this = self.0.lock(); - let window = this.native_window; - let closed = this.closed.clone(); - this.foreground_executor - .spawn(async move { - if_window_not_closed(closed, || unsafe { - window.toggleFullScreen_(nil); - }) - }) - .detach(); - } - - fn is_fullscreen(&self) -> bool { - let this = self.0.lock(); - let window = this.native_window; - - unsafe { - window - .styleMask() - .contains(NSWindowStyleMask::NSFullScreenWindowMask) - } - } - - fn on_request_frame(&self, callback: Box) { - self.0.as_ref().lock().request_frame_callback = Some(callback); - } - - fn on_input(&self, callback: Box gpui::DispatchEventResult>) { - self.0.as_ref().lock().event_callback = Some(callback); - } - - fn on_active_status_change(&self, callback: Box) { - self.0.as_ref().lock().activate_callback = Some(callback); - } - - fn on_hover_status_change(&self, _: Box) {} - - fn on_resize(&self, callback: Box, f32)>) { - self.0.as_ref().lock().resize_callback = Some(callback); - } - - fn on_moved(&self, callback: Box) { - self.0.as_ref().lock().moved_callback = Some(callback); - } - - fn on_should_close(&self, callback: Box bool>) { - self.0.as_ref().lock().should_close_callback = Some(callback); - } - - fn on_close(&self, callback: Box) { - self.0.as_ref().lock().close_callback = Some(callback); - } - - fn on_hit_test_window_control(&self, _callback: Box Option>) { - } - - fn on_appearance_changed(&self, callback: Box) { - self.0.lock().appearance_changed_callback = Some(callback); - } - - fn tabbed_windows(&self) -> Option> { - unsafe { - let windows: id = msg_send![self.0.lock().native_window, tabbedWindows]; - if windows.is_null() { - return None; - } - - let count: NSUInteger = msg_send![windows, count]; - let mut result = Vec::new(); - for i in 0..count { - let window: id = msg_send![windows, objectAtIndex:i]; - if msg_send![window, isKindOfClass: WINDOW_CLASS] { - let handle = get_window_state(&*window).lock().handle; - let title: id = msg_send![window, title]; - let title = SharedString::from(title.to_str().to_string()); - - result.push(SystemWindowTab::new(title, handle)); - } - } - - Some(result) - } - } - - fn tab_bar_visible(&self) -> bool { - unsafe { - let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; - if tab_group.is_null() { - false - } else { - let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; - tab_bar_visible == YES - } - } - } - - fn on_move_tab_to_new_window(&self, callback: Box) { - self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); - } - - fn on_merge_all_windows(&self, callback: Box) { - self.0.as_ref().lock().merge_all_windows_callback = Some(callback); - } - - fn on_select_next_tab(&self, callback: Box) { - self.0.as_ref().lock().select_next_tab_callback = Some(callback); - } - - fn on_select_previous_tab(&self, callback: Box) { - self.0.as_ref().lock().select_previous_tab_callback = Some(callback); - } - - fn on_toggle_tab_bar(&self, callback: Box) { - self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback); - } - - fn draw(&self, scene: &gpui::Scene) { - let mut this = self.0.lock(); - this.renderer.draw(scene); - } - - fn sprite_atlas(&self) -> Arc { - self.0.lock().renderer.sprite_atlas().clone() - } - - fn gpu_specs(&self) -> Option { - None - } - - fn update_ime_position(&self, _bounds: Bounds) { - let executor = self.0.lock().foreground_executor.clone(); - executor - .spawn(async move { - unsafe { - let input_context: id = - msg_send![class!(NSTextInputContext), currentInputContext]; - if input_context.is_null() { - return; - } - let _: () = msg_send![input_context, invalidateCharacterCoordinates]; - } - }) - .detach() - } - - fn titlebar_double_click(&self) { - let this = self.0.lock(); - let window = this.native_window; - let closed = this.closed.clone(); - this.foreground_executor - .spawn(async move { - if_window_not_closed(closed, || { - unsafe { - let defaults: id = NSUserDefaults::standardUserDefaults(); - let domain = ns_string("NSGlobalDomain"); - let key = ns_string("AppleActionOnDoubleClick"); - - let dict: id = msg_send![defaults, persistentDomainForName: domain]; - let action: id = if !dict.is_null() { - msg_send![dict, objectForKey: key] - } else { - nil - }; - - let action_str = if !action.is_null() { - CStr::from_ptr(NSString::UTF8String(action)).to_string_lossy() - } else { - "".into() - }; - - match action_str.as_ref() { - "None" => { - // "Do Nothing" selected, so do no action - } - "Minimize" => { - window.miniaturize_(nil); - } - "Maximize" => { - window.zoom_(nil); - } - "Fill" => { - // There is no documented API for "Fill" action, so we'll just zoom the window - window.zoom_(nil); - } - _ => { - window.zoom_(nil); - } - } - } - }) - }) - .detach(); - } - - fn start_window_move(&self) { - let this = self.0.lock(); - let window = this.native_window; - - unsafe { - let app = NSApplication::sharedApplication(nil); - let event: id = msg_send![app, currentEvent]; - let _: () = msg_send![window, performWindowDragWithEvent: event]; - } - } - - fn play_system_bell(&self) { - NSBeep() - } - - #[cfg(any(test, feature = "test-support"))] - fn render_to_image(&self, scene: &gpui::Scene) -> Result { - let mut this = self.0.lock(); - this.renderer.render_to_image(scene) - } - - fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { - let mut lock = self.0.lock(); - - let activation_handler = A11yActivationHandler { - callback: callbacks.activation, - }; - let action_handler = A11yActionHandler(callbacks.action); - - let adapter = unsafe { - accesskit_macos::SubclassingAdapter::for_window( - lock.native_window as *mut c_void, - activation_handler, - action_handler, - ) - }; - - lock.accesskit_adapter = Some(adapter); - } - - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { - let events = { - let mut lock = self.0.lock(); - lock.accesskit_adapter - .as_mut() - .and_then(|adapter| adapter.update_if_active(|| tree_update)) - }; - if let Some(events) = events { - events.raise(); - } - } - - fn a11y_update_window_bounds(&self) { - // macOS handles window bounds tracking automatically via NSAccessibility. - } + fn bounds(&self) -> Bounds { + self.0.as_ref().lock().bounds() + } + + fn window_bounds(&self) -> WindowBounds { + self.0.as_ref().lock().window_bounds() + } + + fn is_maximized(&self) -> bool { + self.0.as_ref().lock().is_maximized() + } + + fn content_size(&self) -> Size { + self.0.as_ref().lock().content_size() + } + + fn resize(&mut self, size: Size) { + let this = self.0.lock(); + let window = this.native_window; + let closed = this.closed.clone(); + this.foreground_executor + .spawn(async move { + if_window_not_closed(closed, || unsafe { + window.setContentSize_(NSSize { + width: size.width.as_f32() as f64, + height: size.height.as_f32() as f64, + }); + }) + }) + .detach(); + } + + fn merge_all_windows(&self) { + let native_window = self.0.lock().native_window; + extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) { + unsafe { + let native_window = context as id; + let _: () = msg_send![native_window, mergeAllWindows:nil]; + } + } + + unsafe { + DispatchQueue::main() + .exec_async_f(native_window as *mut std::ffi::c_void, merge_windows_async); + } + } + + fn move_tab_to_new_window(&self) { + let native_window = self.0.lock().native_window; + extern "C" fn move_tab_async(context: *mut std::ffi::c_void) { + unsafe { + let native_window = context as id; + let _: () = msg_send![native_window, moveTabToNewWindow:nil]; + let _: () = msg_send![native_window, makeKeyAndOrderFront: nil]; + } + } + + unsafe { + DispatchQueue::main() + .exec_async_f(native_window as *mut std::ffi::c_void, move_tab_async); + } + } + + fn toggle_window_tab_overview(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, toggleTabOverview:nil]; + } + } + + fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + let native_window = self.0.lock().native_window; + unsafe { + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = ns_string(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + } + + fn scale_factor(&self) -> f32 { + self.0.as_ref().lock().scale_factor() + } + + fn appearance(&self) -> WindowAppearance { + unsafe { + let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance]; + crate::window_appearance::window_appearance_from_native(appearance) + } + } + + fn display(&self) -> Option> { + unsafe { + let screen = self.0.lock().native_window.screen(); + if screen.is_null() { + return None; + } + let device_description: id = msg_send![screen, deviceDescription]; + let screen_number: id = + NSDictionary::valueForKey_(device_description, ns_string("NSScreenNumber")); + + let screen_number: u32 = msg_send![screen_number, unsignedIntValue]; + + Some(Rc::new(MacDisplay(screen_number))) + } + } + + fn mouse_position(&self) -> Point { + let position = unsafe { + self.0 + .lock() + .native_window + .mouseLocationOutsideOfEventStream() + }; + convert_mouse_position(position, self.content_size().height) + } + + fn modifiers(&self) -> Modifiers { + unsafe { + let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + platform: command, + function, + } + } + } + + fn capslock(&self) -> Capslock { + unsafe { + let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; + + Capslock { + on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask), + } + } + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.as_ref().lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.as_ref().lock().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option> { + // NSAlert's first button keeps Return and Cancel keeps Escape, but the keyboard + // focus (and therefore Space) defaults to Cancel, leaving the middle button of + // prompts like "Save / Don't Save / Cancel" unreachable from the keyboard. Move + // the initial focus onto the last non-cancel, non-default button instead. + let initial_focus_ix = answers + .iter() + .enumerate() + .rev() + .find(|(_, label)| !label.is_cancel()) + .map(|(ix, _)| ix) + .filter(|&ix| ix > 0); + + unsafe { + let alert: id = msg_send![class!(NSAlert), alloc]; + let alert: id = msg_send![alert, init]; + let alert_style = match level { + PromptLevel::Info => 1, + PromptLevel::Warning => 0, + PromptLevel::Critical => 2, + }; + let _: () = msg_send![alert, setAlertStyle: alert_style]; + let _: () = msg_send![alert, setMessageText: ns_string(msg)]; + if let Some(detail) = detail { + let _: () = msg_send![alert, setInformativeText: ns_string(detail)]; + } + + let mut initial_focus_button: Option = None; + for (ix, answer) in answers.iter().enumerate() { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + + if answer.is_cancel() { + if let Some(key) = std::char::from_u32(crate::events::ESCAPE_KEY as u32) { + let _: () = + msg_send![button, setKeyEquivalent: ns_string(&key.to_string())]; + } + } else if Some(ix) == initial_focus_ix { + initial_focus_button = Some(button); + } + } + + if let Some(button) = initial_focus_button { + let alert_window: id = msg_send![alert, window]; + let _: () = msg_send![alert_window, setInitialFirstResponder: button]; + } + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |answer: NSInteger| { + let _: () = msg_send![alert, release]; + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(answer.try_into().unwrap()); + } + }); + let block = block.copy(); + let lock = self.0.lock(); + let native_window = lock.native_window; + let closed = lock.closed.clone(); + let executor = lock.foreground_executor.clone(); + executor + .spawn(async move { + if !closed.load(Ordering::Acquire) { + let _: () = msg_send![ + alert, + beginSheetModalForWindow: native_window + completionHandler: block + ]; + } else { + let _: () = msg_send![alert, release]; + } + }) + .detach(); + + Some(done_rx) + } + } + + fn activate(&self) { + let lock = self.0.lock(); + let window = lock.native_window; + let closed = lock.closed.clone(); + let executor = lock.foreground_executor.clone(); + executor + .spawn(async move { + if !closed.load(Ordering::Acquire) { + unsafe { + let _: () = msg_send![window, makeKeyAndOrderFront: nil]; + } + } + }) + .detach(); + } + + fn is_active(&self) -> bool { + unsafe { self.0.lock().native_window.isKeyWindow() == YES } + } + + // is_hovered is unused on macOS. See Window::is_window_hovered. + fn is_hovered(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.lock().native_window; + let title = ns_string(title); + let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; + let _: () = msg_send![window, setTitle: title]; + self.0.lock().move_traffic_light(); + } + } + + fn get_title(&self) -> String { + unsafe { + let title: id = msg_send![self.0.lock().native_window, title]; + if title.is_null() { + "".to_string() + } else { + title.to_str().to_string() + } + } + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut this = self.0.as_ref().lock(); + this.background_appearance = background_appearance; + + let opaque = background_appearance == WindowBackgroundAppearance::Opaque; + this.renderer.update_transparency(!opaque); + + unsafe { + this.native_window.setOpaque_(opaque as BOOL); + let background_color = if opaque { + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) + } else { + // Not using `+[NSColor clearColor]` to avoid broken shadow. + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) + }; + this.native_window.setBackgroundColor_(background_color); + + if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 { + // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`. + // On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers + // but uses a `CAProxyLayer`. Use the legacy WindowServer API. + let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { + 80 + } else { + 0 + }; + + let window_number = this.native_window.windowNumber(); + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); + } else { + // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it + // could have a better performance (it downsamples the backdrop) and more control + // over the effect layer. + if background_appearance != WindowBackgroundAppearance::Blurred { + if let Some(blur_view) = this.blurred_view { + NSView::removeFromSuperview(blur_view); + this.blurred_view = None; + } + } else if this.blurred_view.is_none() { + let content_view = this.native_window.contentView(); + let frame = NSView::bounds(content_view); + let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; + blur_view = NSView::initWithFrame_(blur_view, frame); + blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + + let _: () = msg_send![ + content_view, + addSubview: blur_view + positioned: NSWindowOrderingMode::NSWindowBelow + relativeTo: nil + ]; + this.blurred_view = Some(blur_view.autorelease()); + } + } + } + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.0.as_ref().lock().background_appearance + } + + fn is_subpixel_rendering_supported(&self) -> bool { + false + } + + fn set_edited(&mut self, edited: bool) { + unsafe { + let window = self.0.lock().native_window; + msg_send![window, setDocumentEdited: edited as BOOL] + } + + // Changing the document edited state resets the traffic light position, + // so we have to move it again. + self.0.lock().move_traffic_light(); + } + + fn set_document_path(&self, path: Option<&std::path::Path>) { + unsafe { + let window = self.0.lock().native_window; + let filename = path.map_or(ns_string(""), |p| ns_string(&p.to_string_lossy())); + let _: () = msg_send![window, setRepresentedFilename: filename]; + } + + // Changing the document path state resets the traffic light position, + // so we have to move it again. + self.0.lock().move_traffic_light(); + } + + fn show_character_palette(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.foreground_executor + .spawn(async move { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + }) + .detach(); + } + + fn minimize(&self) { + let window = self.0.lock().native_window; + unsafe { + window.miniaturize_(nil); + } + } + + fn zoom(&self) { + let this = self.0.lock(); + let window = this.native_window; + let closed = this.closed.clone(); + this.foreground_executor + .spawn(async move { + if_window_not_closed(closed, || unsafe { + window.zoom_(nil); + }) + }) + .detach(); + } + + fn toggle_fullscreen(&self) { + let this = self.0.lock(); + let window = this.native_window; + let closed = this.closed.clone(); + this.foreground_executor + .spawn(async move { + if_window_not_closed(closed, || unsafe { + window.toggleFullScreen_(nil); + }) + }) + .detach(); + } + + fn is_fullscreen(&self) -> bool { + let this = self.0.lock(); + let window = this.native_window; + + unsafe { + window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn on_request_frame(&self, callback: Box) { + self.0.as_ref().lock().request_frame_callback = Some(callback); + } + + fn on_input(&self, callback: Box gpui::DispatchEventResult>) { + self.0.as_ref().lock().event_callback = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.as_ref().lock().activate_callback = Some(callback); + } + + fn on_hover_status_change(&self, _: Box) {} + + fn on_resize(&self, callback: Box, f32)>) { + self.0.as_ref().lock().resize_callback = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.as_ref().lock().moved_callback = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.as_ref().lock().should_close_callback = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.as_ref().lock().close_callback = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.lock().appearance_changed_callback = Some(callback); + } + + fn tabbed_windows(&self) -> Option> { + unsafe { + let windows: id = msg_send![self.0.lock().native_window, tabbedWindows]; + if windows.is_null() { + return None; + } + + let count: NSUInteger = msg_send![windows, count]; + let mut result = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + let title: id = msg_send![window, title]; + let title = SharedString::from(title.to_str().to_string()); + + result.push(SystemWindowTab::new(title, handle)); + } + } + + Some(result) + } + } + + fn tab_bar_visible(&self) -> bool { + unsafe { + let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; + if tab_group.is_null() { + false + } else { + let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; + tab_bar_visible == YES + } + } + } + + fn on_move_tab_to_new_window(&self, callback: Box) { + self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); + } + + fn on_merge_all_windows(&self, callback: Box) { + self.0.as_ref().lock().merge_all_windows_callback = Some(callback); + } + + fn on_select_next_tab(&self, callback: Box) { + self.0.as_ref().lock().select_next_tab_callback = Some(callback); + } + + fn on_select_previous_tab(&self, callback: Box) { + self.0.as_ref().lock().select_previous_tab_callback = Some(callback); + } + + fn on_toggle_tab_bar(&self, callback: Box) { + self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback); + } + + fn draw(&self, scene: &gpui::Scene) { + let mut this = self.0.lock(); + this.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> Arc { + self.0.lock().renderer.sprite_atlas().clone() + } + + fn gpu_specs(&self) -> Option { + None + } + + fn update_ime_position(&self, _bounds: Bounds) { + let executor = self.0.lock().foreground_executor.clone(); + executor + .spawn(async move { + unsafe { + let input_context: id = + msg_send![class!(NSTextInputContext), currentInputContext]; + if input_context.is_null() { + return; + } + let _: () = msg_send![input_context, invalidateCharacterCoordinates]; + } + }) + .detach() + } + + fn titlebar_double_click(&self) { + let this = self.0.lock(); + let window = this.native_window; + let closed = this.closed.clone(); + this.foreground_executor + .spawn(async move { + if_window_not_closed(closed, || { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = ns_string("NSGlobalDomain"); + let key = ns_string("AppleActionOnDoubleClick"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let action: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let action_str = if !action.is_null() { + CStr::from_ptr(NSString::UTF8String(action)).to_string_lossy() + } else { + "".into() + }; + + match action_str.as_ref() { + "None" => { + // "Do Nothing" selected, so do no action + } + "Minimize" => { + window.miniaturize_(nil); + } + "Maximize" => { + window.zoom_(nil); + } + "Fill" => { + // There is no documented API for "Fill" action, so we'll just zoom the window + window.zoom_(nil); + } + _ => { + window.zoom_(nil); + } + } + } + }) + }) + .detach(); + } + + fn start_window_move(&self) { + let this = self.0.lock(); + let window = this.native_window; + + unsafe { + let app = NSApplication::sharedApplication(nil); + let event: id = msg_send![app, currentEvent]; + let _: () = msg_send![window, performWindowDragWithEvent: event]; + } + } + + fn play_system_bell(&self) { + NSBeep() + } + + #[cfg(any(test, feature = "test-support"))] + fn render_to_image(&self, scene: &gpui::Scene) -> Result { + let mut this = self.0.lock(); + this.renderer.render_to_image(scene) + } + + fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { + let mut lock = self.0.lock(); + + let activation_handler = A11yActivationHandler { + callback: callbacks.activation, + }; + let action_handler = A11yActionHandler(callbacks.action); + + let adapter = unsafe { + accesskit_macos::SubclassingAdapter::for_window( + lock.native_window as *mut c_void, + activation_handler, + action_handler, + ) + }; + + lock.accesskit_adapter = Some(adapter); + } + + fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + let events = { + let mut lock = self.0.lock(); + lock.accesskit_adapter + .as_mut() + .and_then(|adapter| adapter.update_if_active(|| tree_update)) + }; + if let Some(events) = events { + events.raise(); + } + } + + fn a11y_update_window_bounds(&self) { + // macOS handles window bounds tracking automatically via NSAccessibility. + } } struct A11yActivationHandler { - callback: Box Option + Send + 'static>, + callback: Box Option + Send + 'static>, } impl accesskit::ActivationHandler for A11yActivationHandler { - fn request_initial_tree(&mut self) -> Option { - (self.callback)() - } + 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); - } + fn do_action(&mut self, request: accesskit::ActionRequest) { + (self.0)(request); + } } impl rwh::HasWindowHandle for MacWindow { - fn window_handle(&self) -> Result, rwh::HandleError> { - // SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView - unsafe { - Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit( - rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()), - ))) - } - } + fn window_handle(&self) -> Result, rwh::HandleError> { + // SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView + unsafe { + Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit( + rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()), + ))) + } + } } impl rwh::HasDisplayHandle for MacWindow { - fn display_handle(&self) -> Result, rwh::HandleError> { - Ok(rwh::DisplayHandle::appkit()) - } + fn display_handle(&self) -> Result, rwh::HandleError> { + Ok(rwh::DisplayHandle::appkit()) + } } fn get_scale_factor(native_window: id) -> f32 { - let factor = unsafe { - let screen: id = msg_send![native_window, screen]; - if screen.is_null() { - return 2.0; - } - NSScreen::backingScaleFactor(screen) as f32 - }; + let factor = unsafe { + let screen: id = msg_send![native_window, screen]; + if screen.is_null() { + return 2.0; + } + NSScreen::backingScaleFactor(screen) as f32 + }; - // We are not certain what triggers this, but it seems that sometimes - // this method would return 0 (https://github.com/zed-industries/zed/issues/6412) - // It seems most likely that this would happen if the window has no screen - // (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before - // it was rendered for real. - // Regardless, attempt to avoid the issue here. - if factor == 0.0 { 2. } else { factor } + // We are not certain what triggers this, but it seems that sometimes + // this method would return 0 (https://github.com/zed-industries/zed/issues/6412) + // It seems most likely that this would happen if the window has no screen + // (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before + // it was rendered for real. + // Regardless, attempt to avoid the issue here. + if factor == 0.0 { 2. } else { factor } } unsafe fn get_window_state(object: &Object) -> Arc> { - unsafe { - let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); - let rc1 = Arc::from_raw(raw as *mut Mutex); - let rc2 = rc1.clone(); - mem::forget(rc1); - rc2 - } + unsafe { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + let rc1 = Arc::from_raw(raw as *mut Mutex); + let rc2 = rc1.clone(); + mem::forget(rc1); + rc2 + } } unsafe fn drop_window_state(object: &Object) { - unsafe { - let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); - Arc::from_raw(raw as *mut Mutex); - } + unsafe { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + Arc::from_raw(raw as *mut Mutex); + } } extern "C" fn yes(_: &Object, _: Sel) -> BOOL { - YES + YES } extern "C" fn dealloc_window(this: &Object, _: Sel) { - unsafe { - drop_window_state(this); - let _: () = msg_send![super(this, class!(NSWindow)), dealloc]; - } + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSWindow)), dealloc]; + } } extern "C" fn dealloc_view(this: &Object, _: Sel) { - unsafe { - drop_window_state(this); - let _: () = msg_send![super(this, class!(NSView)), dealloc]; - } + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } } extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { - // SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances, - // whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered - // below is a valid NSCursor. - unsafe { - let _: () = msg_send![super(this, class!(NSView)), resetCursorRects]; + // SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances, + // whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered + // below is a valid NSCursor. + unsafe { + let _: () = msg_send![super(this, class!(NSView)), resetCursorRects]; - let window_state = get_window_state(this); - let cursor_style = window_state.lock().cursor_style; + let window_state = get_window_state(this); + let cursor_style = window_state.lock().cursor_style; - let cursor: id = match cursor_style { - CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], - CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], - CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor], - CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], - CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], - CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], - CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], - CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], - CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], - CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], - CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], - CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], - CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], - CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], + let cursor: id = match cursor_style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], + CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor], + CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], + CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], + CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], + CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], + CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], - // Undocumented, private class methods: - // https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor - CursorStyle::ResizeUpLeftDownRight => { - msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor] - } - CursorStyle::ResizeUpRightDownLeft => { - msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor] - } + // Undocumented, private class methods: + // https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor + CursorStyle::ResizeUpLeftDownRight => { + msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor] + } + CursorStyle::ResizeUpRightDownLeft => { + msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor] + } - CursorStyle::IBeamCursorForVerticalLayout => { - msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] - } - CursorStyle::OperationNotAllowed => { - msg_send![class!(NSCursor), operationNotAllowedCursor] - } - CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], - CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], - CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], - }; + CursorStyle::IBeamCursorForVerticalLayout => { + msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] + } + CursorStyle::OperationNotAllowed => { + msg_send![class!(NSCursor), operationNotAllowedCursor] + } + CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], + CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], + CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], + }; - let bounds = NSView::bounds(this as *const Object as id); - let _: () = msg_send![this, addCursorRect: bounds cursor: cursor]; - } + let bounds = NSView::bounds(this as *const Object as id); + let _: () = msg_send![this, addCursorRect: bounds cursor: cursor]; + } } extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { - handle_key_event(this, native_event, true) + handle_key_event(this, native_event, true) } extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { - handle_key_event(this, native_event, false); + handle_key_event(this, native_event, false); } extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) { - handle_key_event(this, native_event, false); + handle_key_event(this, native_event, false); } // Things to test if you're modifying this method: @@ -1958,1088 +1958,1088 @@ extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) { /// 2. The source is not ASCII-capable, which excludes modes like Japanese Romaji that /// produce ASCII characters and should allow multi-stroke keybindings like `jj`. unsafe fn is_ime_input_source_active() -> bool { - unsafe { - let source = TISCopyCurrentKeyboardInputSource(); - if source.is_null() { - return false; - } + unsafe { + let source = TISCopyCurrentKeyboardInputSource(); + if source.is_null() { + return false; + } - let source_type = - TISGetInputSourceProperty(source, kTISPropertyInputSourceType as *const c_void); - let is_input_mode = !source_type.is_null() - && CFEqual( - source_type as CFTypeRef, - kTISTypeKeyboardInputMode as CFTypeRef, - ) != 0; + let source_type = + TISGetInputSourceProperty(source, kTISPropertyInputSourceType as *const c_void); + let is_input_mode = !source_type.is_null() + && CFEqual( + source_type as CFTypeRef, + kTISTypeKeyboardInputMode as CFTypeRef, + ) != 0; - let is_ascii = TISGetInputSourceProperty( - source, - kTISPropertyInputSourceIsASCIICapable as *const c_void, - ); - let is_ascii_capable = !is_ascii.is_null() && CFBooleanGetValue(is_ascii as CFBooleanRef); + let is_ascii = TISGetInputSourceProperty( + source, + kTISPropertyInputSourceIsASCIICapable as *const c_void, + ); + let is_ascii_capable = !is_ascii.is_null() && CFBooleanGetValue(is_ascii as CFBooleanRef); - CFRelease(source as CFTypeRef); + CFRelease(source as CFTypeRef); - is_input_mode && !is_ascii_capable - } + is_input_mode && !is_ascii_capable + } } extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); - let window_height = lock.content_size().height; - let event = unsafe { platform_input_from_native(native_event, Some(window_height)) }; + let window_height = lock.content_size().height; + let event = unsafe { platform_input_from_native(native_event, Some(window_height)) }; - let Some(event) = event else { - return NO; - }; + let Some(event) = event else { + return NO; + }; - let run_callback = |event: PlatformInput| -> BOOL { - let mut callback = window_state.as_ref().lock().event_callback.take(); - let handled: BOOL = if let Some(callback) = callback.as_mut() { - !callback(event).propagate as BOOL - } else { - NO - }; - window_state.as_ref().lock().event_callback = callback; - handled - }; + let run_callback = |event: PlatformInput| -> BOOL { + let mut callback = window_state.as_ref().lock().event_callback.take(); + let handled: BOOL = if let Some(callback) = callback.as_mut() { + !callback(event).propagate as BOOL + } else { + NO + }; + window_state.as_ref().lock().event_callback = callback; + handled + }; - match event { - PlatformInput::KeyDown(key_down_event) => { - // For certain keystrokes, macOS will first dispatch a "key equivalent" event. - // If that event isn't handled, it will then dispatch a "key down" event. GPUI - // makes no distinction between these two types of events, so we need to ignore - // the "key down" event if we've already just processed its "key equivalent" version. - if key_equivalent { - lock.last_key_equivalent = Some(key_down_event.clone()); - } else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_event) { - return NO; - } + match event { + PlatformInput::KeyDown(key_down_event) => { + // For certain keystrokes, macOS will first dispatch a "key equivalent" event. + // If that event isn't handled, it will then dispatch a "key down" event. GPUI + // makes no distinction between these two types of events, so we need to ignore + // the "key down" event if we've already just processed its "key equivalent" version. + if key_equivalent { + lock.last_key_equivalent = Some(key_down_event.clone()); + } else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_event) { + return NO; + } - drop(lock); + drop(lock); - let is_composing = - with_input_handler(this, |input_handler| input_handler.marked_text_range()) - .flatten() - .is_some(); + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); - // If we're composing, send the key to the input handler first; - // otherwise we only send to the input handler if we don't have a matching binding. - // The input handler may call `do_command_by_selector` if it doesn't know how to handle - // a key. If it does so, it will return YES so we won't send the key twice. - // We also do this for non-printing keys (like arrow keys and escape) as the IME menu - // may need them even if there is no marked text; - // however we skip keys with control or the input handler adds control-characters to the buffer. - // and keys with function, as the input handler swallows them. - // and keys with platform (Cmd), so that Cmd+key events (e.g. Cmd+`) are not - // consumed by the IME on non-QWERTY / dead-key layouts. - // We also send printable keys to the IME first when an IME input source (e.g. Japanese, - // Korean, Chinese) is active and the input handler accepts text input. This prevents - // multi-stroke keybindings like `jj` from intercepting keys that the IME should compose - // (e.g. typing 'ji' should produce 'じ', not 'jい'). If the IME doesn't handle the key, - // it calls `doCommandBySelector:` which routes it back to keybinding matching. - let is_ime_printable_key = !is_composing - && key_down_event - .keystroke - .key_char - .as_ref() - .is_some_and(|key_char| key_char.chars().all(|c| !c.is_control())) - && !key_down_event.keystroke.modifiers.control - && !key_down_event.keystroke.modifiers.function - && !key_down_event.keystroke.modifiers.platform - && unsafe { is_ime_input_source_active() } - && with_input_handler(this, |input_handler| { - input_handler.query_prefers_ime_for_printable_keys() - }) - .unwrap_or(false); + // If we're composing, send the key to the input handler first; + // otherwise we only send to the input handler if we don't have a matching binding. + // The input handler may call `do_command_by_selector` if it doesn't know how to handle + // a key. If it does so, it will return YES so we won't send the key twice. + // We also do this for non-printing keys (like arrow keys and escape) as the IME menu + // may need them even if there is no marked text; + // however we skip keys with control or the input handler adds control-characters to the buffer. + // and keys with function, as the input handler swallows them. + // and keys with platform (Cmd), so that Cmd+key events (e.g. Cmd+`) are not + // consumed by the IME on non-QWERTY / dead-key layouts. + // We also send printable keys to the IME first when an IME input source (e.g. Japanese, + // Korean, Chinese) is active and the input handler accepts text input. This prevents + // multi-stroke keybindings like `jj` from intercepting keys that the IME should compose + // (e.g. typing 'ji' should produce 'じ', not 'jい'). If the IME doesn't handle the key, + // it calls `doCommandBySelector:` which routes it back to keybinding matching. + let is_ime_printable_key = !is_composing + && key_down_event + .keystroke + .key_char + .as_ref() + .is_some_and(|key_char| key_char.chars().all(|c| !c.is_control())) + && !key_down_event.keystroke.modifiers.control + && !key_down_event.keystroke.modifiers.function + && !key_down_event.keystroke.modifiers.platform + && unsafe { is_ime_input_source_active() } + && with_input_handler(this, |input_handler| { + input_handler.query_prefers_ime_for_printable_keys() + }) + .unwrap_or(false); - if is_composing - || is_ime_printable_key - || (key_down_event.keystroke.key_char.is_none() - && !key_down_event.keystroke.modifiers.control - && !key_down_event.keystroke.modifiers.function - && !key_down_event.keystroke.modifiers.platform) - { - { - let mut lock = window_state.as_ref().lock(); - lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone()); - lock.do_command_handled.take(); - drop(lock); - } + if is_composing + || is_ime_printable_key + || (key_down_event.keystroke.key_char.is_none() + && !key_down_event.keystroke.modifiers.control + && !key_down_event.keystroke.modifiers.function + && !key_down_event.keystroke.modifiers.platform) + { + { + let mut lock = window_state.as_ref().lock(); + lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone()); + lock.do_command_handled.take(); + drop(lock); + } - let handled: BOOL = unsafe { - let input_context: id = msg_send![this, inputContext]; - msg_send![input_context, handleEvent: native_event] - }; - window_state.as_ref().lock().keystroke_for_do_command.take(); - if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() { - return handled as BOOL; - } else if handled == YES { - return YES; - } + let handled: BOOL = unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + }; + window_state.as_ref().lock().keystroke_for_do_command.take(); + if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() { + return handled as BOOL; + } else if handled == YES { + return YES; + } - let handled = run_callback(PlatformInput::KeyDown(key_down_event)); - return handled; - } + let handled = run_callback(PlatformInput::KeyDown(key_down_event)); + return handled; + } - let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone())); - if handled == YES { - return YES; - } + let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone())); + if handled == YES { + return YES; + } - if key_down_event.is_held - && let Some(key_char) = key_down_event.keystroke.key_char.as_ref() - { - let handled = with_input_handler(this, |input_handler| { - if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range(None, key_char); - return YES; - } - NO - }); - if handled == Some(YES) { - return YES; - } - } + if key_down_event.is_held + && let Some(key_char) = key_down_event.keystroke.key_char.as_ref() + { + let handled = with_input_handler(this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, key_char); + return YES; + } + NO + }); + if handled == Some(YES) { + return YES; + } + } - // Don't send key equivalents to the input handler if there are key modifiers other - // than Function key, or macOS shortcuts like cmd-` will stop working. - if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() { - return NO; - } + // Don't send key equivalents to the input handler if there are key modifiers other + // than Function key, or macOS shortcuts like cmd-` will stop working. + if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() { + return NO; + } - unsafe { - let input_context: id = msg_send![this, inputContext]; - msg_send![input_context, handleEvent: native_event] - } - } + unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + } + } - PlatformInput::KeyUp(_) => { - drop(lock); - run_callback(event) - } + PlatformInput::KeyUp(_) => { + drop(lock); + run_callback(event) + } - _ => NO, - } + _ => NO, + } } extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { - let window_state = unsafe { get_window_state(this) }; - let weak_window_state = Arc::downgrade(&window_state); - let mut lock = window_state.as_ref().lock(); - let window_height = lock.content_size().height; - let event = unsafe { platform_input_from_native(native_event, Some(window_height)) }; + let window_state = unsafe { get_window_state(this) }; + let weak_window_state = Arc::downgrade(&window_state); + let mut lock = window_state.as_ref().lock(); + let window_height = lock.content_size().height; + let event = unsafe { platform_input_from_native(native_event, Some(window_height)) }; - if let Some(mut event) = event { - // AppKit unhides the cursor on the next mouse movement; mirror that here. - if matches!( - event, - PlatformInput::MouseMove(_) - | PlatformInput::MouseDown(_) - | PlatformInput::MouseUp(_) - | PlatformInput::MousePressure(_) - | PlatformInput::MouseExited(_) - | PlatformInput::ScrollWheel(_) - | PlatformInput::Pinch(_) - ) { - lock.cursor_visible.store(true, Ordering::Relaxed); - } + if let Some(mut event) = event { + // AppKit unhides the cursor on the next mouse movement; mirror that here. + if matches!( + event, + PlatformInput::MouseMove(_) + | PlatformInput::MouseDown(_) + | PlatformInput::MouseUp(_) + | PlatformInput::MousePressure(_) + | PlatformInput::MouseExited(_) + | PlatformInput::ScrollWheel(_) + | PlatformInput::Pinch(_) + ) { + lock.cursor_visible.store(true, Ordering::Relaxed); + } - match &mut event { - PlatformInput::MouseDown( - event @ MouseDownEvent { - button: MouseButton::Left, - modifiers: Modifiers { control: true, .. }, - .. - }, - ) => { - // On mac, a ctrl-left click should be handled as a right click. - *event = MouseDownEvent { - button: MouseButton::Right, - modifiers: Modifiers { - control: false, - ..event.modifiers - }, - click_count: 1, - ..*event - }; - } + match &mut event { + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + // On mac, a ctrl-left click should be handled as a right click. + *event = MouseDownEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } - // Handles focusing click. - PlatformInput::MouseDown( - event @ MouseDownEvent { - button: MouseButton::Left, - .. - }, - ) if (lock.first_mouse) => { - *event = MouseDownEvent { - first_mouse: true, - ..*event - }; - lock.first_mouse = false; - } + // Handles focusing click. + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + .. + }, + ) if (lock.first_mouse) => { + *event = MouseDownEvent { + first_mouse: true, + ..*event + }; + lock.first_mouse = false; + } - // Because we map a ctrl-left_down to a right_down -> right_up let's ignore - // the ctrl-left_up to avoid having a mismatch in button down/up events if the - // user is still holding ctrl when releasing the left mouse button - PlatformInput::MouseUp( - event @ MouseUpEvent { - button: MouseButton::Left, - modifiers: Modifiers { control: true, .. }, - .. - }, - ) => { - *event = MouseUpEvent { - button: MouseButton::Right, - modifiers: Modifiers { - control: false, - ..event.modifiers - }, - click_count: 1, - ..*event - }; - } + // Because we map a ctrl-left_down to a right_down -> right_up let's ignore + // the ctrl-left_up to avoid having a mismatch in button down/up events if the + // user is still holding ctrl when releasing the left mouse button + PlatformInput::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } - _ => {} - }; + _ => {} + }; - match &event { - PlatformInput::MouseDown(_) => { - drop(lock); - unsafe { - let input_context: id = msg_send![this, inputContext]; - msg_send![input_context, handleEvent: native_event] - } - lock = window_state.as_ref().lock(); - } - PlatformInput::MouseMove( - event @ MouseMoveEvent { - pressed_button: Some(_), - .. - }, - ) => { - // Synthetic drag is used for selecting long buffer contents while buffer is being scrolled. - // External file drag and drop is able to emit its own synthetic mouse events which will conflict - // with these ones. - if !lock.external_files_dragged { - lock.synthetic_drag_counter += 1; - let executor = lock.foreground_executor.clone(); - executor - .spawn(synthetic_drag( - weak_window_state, - lock.synthetic_drag_counter, - event.clone(), - lock.background_executor.clone(), - )) - .detach(); - } - } + match &event { + PlatformInput::MouseDown(_) => { + drop(lock); + unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + } + lock = window_state.as_ref().lock(); + } + PlatformInput::MouseMove( + event @ MouseMoveEvent { + pressed_button: Some(_), + .. + }, + ) => { + // Synthetic drag is used for selecting long buffer contents while buffer is being scrolled. + // External file drag and drop is able to emit its own synthetic mouse events which will conflict + // with these ones. + if !lock.external_files_dragged { + lock.synthetic_drag_counter += 1; + let executor = lock.foreground_executor.clone(); + executor + .spawn(synthetic_drag( + weak_window_state, + lock.synthetic_drag_counter, + event.clone(), + lock.background_executor.clone(), + )) + .detach(); + } + } - PlatformInput::MouseUp(MouseUpEvent { .. }) => { - lock.synthetic_drag_counter += 1; - } + PlatformInput::MouseUp(MouseUpEvent { .. }) => { + lock.synthetic_drag_counter += 1; + } - PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - }) => { - // Only raise modifiers changed event when they have actually changed - if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers: prev_modifiers, - capslock: prev_capslock, - })) = &lock.previous_modifiers_changed_event - && prev_modifiers == modifiers - && prev_capslock == capslock - { - return; - } + PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + }) => { + // Only raise modifiers changed event when they have actually changed + if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: prev_modifiers, + capslock: prev_capslock, + })) = &lock.previous_modifiers_changed_event + && prev_modifiers == modifiers + && prev_capslock == capslock + { + return; + } - lock.previous_modifiers_changed_event = Some(event.clone()); - } + lock.previous_modifiers_changed_event = Some(event.clone()); + } - _ => {} - } + _ => {} + } - if let Some(mut callback) = lock.event_callback.take() { - drop(lock); - callback(event); - window_state.lock().event_callback = Some(callback); - } - } + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(event); + window_state.lock().event_callback = Some(callback); + } + } } extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let lock = &mut *window_state.lock(); - unsafe { - if lock - .native_window - .occlusionState() - .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) - { - lock.move_traffic_light(); - lock.start_display_link(); - } else { - lock.stop_display_link(); - } - } + let window_state = unsafe { get_window_state(this) }; + let lock = &mut *window_state.lock(); + unsafe { + if lock + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + lock.move_traffic_light(); + lock.start_display_link(); + } else { + lock.stop_display_link(); + } + } } extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - window_state.as_ref().lock().move_traffic_light(); + let window_state = unsafe { get_window_state(this) }; + window_state.as_ref().lock().move_traffic_light(); } extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - lock.fullscreen_restore_bounds = lock.bounds(); + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.fullscreen_restore_bounds = lock.bounds(); - let min_version = NSOperatingSystemVersion::new(15, 3, 0); + let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) { - unsafe { - lock.native_window.setTitlebarAppearsTransparent_(NO); - } - } + if is_macos_version_at_least(min_version) { + unsafe { + lock.native_window.setTitlebarAppearsTransparent_(NO); + } + } } extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let lock = window_state.as_ref().lock(); + let window_state = unsafe { get_window_state(this) }; + let lock = window_state.as_ref().lock(); - let min_version = NSOperatingSystemVersion::new(15, 3, 0); + let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) && lock.transparent_titlebar { - unsafe { - lock.native_window.setTitlebarAppearsTransparent_(YES); - } - } + if is_macos_version_at_least(min_version) && lock.transparent_titlebar { + unsafe { + lock.native_window.setTitlebarAppearsTransparent_(YES); + } + } } pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool { - unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) } + unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) } } extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.moved_callback.take() { - drop(lock); - callback(); - window_state.lock().moved_callback = Some(callback); - } + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.moved_callback.take() { + drop(lock); + callback(); + window_state.lock().moved_callback = Some(callback); + } } // Update the window scale factor and drawable size, and call the resize callback if any. fn update_window_scale_factor(window_state: &Arc>) { - let mut lock = window_state.as_ref().lock(); - let scale_factor = lock.scale_factor(); - let size = lock.content_size(); - let drawable_size = size.to_device_pixels(scale_factor); - if let Some(layer) = lock.renderer.layer() { - unsafe { - let _: () = msg_send![ - layer, - setContentsScale: scale_factor as f64 - ]; - } - } + let mut lock = window_state.as_ref().lock(); + let scale_factor = lock.scale_factor(); + let size = lock.content_size(); + let drawable_size = size.to_device_pixels(scale_factor); + if let Some(layer) = lock.renderer.layer() { + unsafe { + let _: () = msg_send![ + layer, + setContentsScale: scale_factor as f64 + ]; + } + } - lock.renderer.update_drawable_size(drawable_size); + lock.renderer.update_drawable_size(drawable_size); - if let Some(mut callback) = lock.resize_callback.take() { - let content_size = lock.content_size(); - let scale_factor = lock.scale_factor(); - drop(lock); - callback(content_size, scale_factor); - window_state.as_ref().lock().resize_callback = Some(callback); - }; + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.as_ref().lock().resize_callback = Some(callback); + }; } extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - lock.start_display_link(); - drop(lock); - update_window_scale_factor(&window_state); + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.start_display_link(); + drop(lock); + update_window_scale_factor(&window_state); } extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let lock = window_state.lock(); - let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; + let window_state = unsafe { get_window_state(this) }; + let lock = window_state.lock(); + let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; - // AppKit also unhides the cursor on activation changes, so mirror that here. - lock.cursor_visible.store(true, Ordering::Relaxed); + // AppKit also unhides the cursor on activation changes, so mirror that here. + lock.cursor_visible.store(true, Ordering::Relaxed); - // When opening a pop-up while the application isn't active, Cocoa sends a spurious - // `windowDidBecomeKey` message to the previous key window even though that window - // isn't actually key. This causes a bug if the application is later activated while - // the pop-up is still open, making it impossible to activate the previous key window - // even if the pop-up gets closed. The only way to activate it again is to de-activate - // the app and re-activate it, which is a pretty bad UX. - // The following code detects the spurious event and invokes `resignKeyWindow`: - // in theory, we're not supposed to invoke this method manually but it balances out - // the spurious `becomeKeyWindow` event and helps us work around that bug. - if selector == sel!(windowDidBecomeKey:) && !is_active { - let native_window = lock.native_window; - drop(lock); - unsafe { - let _: () = msg_send![native_window, resignKeyWindow]; - } - return; - } + // When opening a pop-up while the application isn't active, Cocoa sends a spurious + // `windowDidBecomeKey` message to the previous key window even though that window + // isn't actually key. This causes a bug if the application is later activated while + // the pop-up is still open, making it impossible to activate the previous key window + // even if the pop-up gets closed. The only way to activate it again is to de-activate + // the app and re-activate it, which is a pretty bad UX. + // The following code detects the spurious event and invokes `resignKeyWindow`: + // in theory, we're not supposed to invoke this method manually but it balances out + // the spurious `becomeKeyWindow` event and helps us work around that bug. + if selector == sel!(windowDidBecomeKey:) && !is_active { + let native_window = lock.native_window; + drop(lock); + unsafe { + let _: () = msg_send![native_window, resignKeyWindow]; + } + return; + } - let executor = lock.foreground_executor.clone(); - drop(lock); + let executor = lock.foreground_executor.clone(); + drop(lock); - let a11y_events = { - let mut lock = window_state.lock(); - lock.accesskit_adapter - .as_mut() - .and_then(|adapter| adapter.update_view_focus_state(is_active)) - }; - if let Some(events) = a11y_events { - events.raise(); - } + let a11y_events = { + let mut lock = window_state.lock(); + lock.accesskit_adapter + .as_mut() + .and_then(|adapter| adapter.update_view_focus_state(is_active)) + }; + if let Some(events) = a11y_events { + events.raise(); + } - // When a window becomes active, trigger an immediate synchronous frame request to prevent - // tab flicker when switching between windows in native tabs mode. - // - // This is only done on subsequent activations (not the first) to ensure the initial focus - // path is properly established. Without this guard, the focus state would remain unset until - // the first mouse click, causing keybindings to be non-functional. - if selector == sel!(windowDidBecomeKey:) && is_active { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.lock(); + // When a window becomes active, trigger an immediate synchronous frame request to prevent + // tab flicker when switching between windows in native tabs mode. + // + // This is only done on subsequent activations (not the first) to ensure the initial focus + // path is properly established. Without this guard, the focus state would remain unset until + // the first mouse click, causing keybindings to be non-functional. + if selector == sel!(windowDidBecomeKey:) && is_active { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); - if lock.activated_least_once { - if let Some(mut callback) = lock.request_frame_callback.take() { - lock.renderer.set_presents_with_transaction(true); - lock.stop_display_link(); - drop(lock); - callback(Default::default()); + if lock.activated_least_once { + if let Some(mut callback) = lock.request_frame_callback.take() { + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(Default::default()); - let mut lock = window_state.lock(); - lock.request_frame_callback = Some(callback); - lock.renderer.set_presents_with_transaction(false); - lock.start_display_link(); - } - } else { - lock.activated_least_once = true; - } - } + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } + } else { + lock.activated_least_once = true; + } + } - executor - .spawn(async move { - let mut lock = window_state.as_ref().lock(); - if is_active { - lock.move_traffic_light(); - } + executor + .spawn(async move { + let mut lock = window_state.as_ref().lock(); + if is_active { + lock.move_traffic_light(); + } - if let Some(mut callback) = lock.activate_callback.take() { - drop(lock); - callback(is_active); - window_state.lock().activate_callback = Some(callback); - }; - }) - .detach(); + if let Some(mut callback) = lock.activate_callback.take() { + drop(lock); + callback(is_active); + window_state.lock().activate_callback = Some(callback); + }; + }) + .detach(); } extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.should_close_callback.take() { - drop(lock); - let should_close = callback(); - window_state.lock().should_close_callback = Some(callback); - should_close as BOOL - } else { - YES - } + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.should_close_callback.take() { + drop(lock); + let should_close = callback(); + window_state.lock().should_close_callback = Some(callback); + should_close as BOOL + } else { + YES + } } extern "C" fn close_window(this: &Object, _: Sel) { - unsafe { - let close_callback = { - let window_state = get_window_state(this); - let mut lock = window_state.as_ref().lock(); - lock.closed.store(true, Ordering::Release); - lock.close_callback.take() - }; + unsafe { + let close_callback = { + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.closed.store(true, Ordering::Release); + lock.close_callback.take() + }; - if let Some(callback) = close_callback { - callback(); - } + if let Some(callback) = close_callback { + callback(); + } - let _: () = msg_send![super(this, class!(NSWindow)), close]; - } + let _: () = msg_send![super(this, class!(NSWindow)), close]; + } } extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { - let window_state = unsafe { get_window_state(this) }; - let window_state = window_state.as_ref().lock(); - window_state.renderer.layer_ptr() as id + let window_state = unsafe { get_window_state(this) }; + let window_state = window_state.as_ref().lock(); + window_state.renderer.layer_ptr() as id } extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { - let window_state = unsafe { get_window_state(this) }; - update_window_scale_factor(&window_state); + let window_state = unsafe { get_window_state(this) }; + update_window_scale_factor(&window_state); } extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { - fn convert(value: NSSize) -> Size { - Size { - width: px(value.width as f32), - height: px(value.height as f32), - } - } + fn convert(value: NSSize) -> Size { + Size { + width: px(value.width as f32), + height: px(value.height as f32), + } + } - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); - let new_size = convert(size); - let old_size = unsafe { - let old_frame: NSRect = msg_send![this, frame]; - convert(old_frame.size) - }; + let new_size = convert(size); + let old_size = unsafe { + let old_frame: NSRect = msg_send![this, frame]; + convert(old_frame.size) + }; - if old_size == new_size { - return; - } + if old_size == new_size { + return; + } - unsafe { - let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; - } + unsafe { + let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; + } - let scale_factor = lock.scale_factor(); - let drawable_size = new_size.to_device_pixels(scale_factor); - lock.renderer.update_drawable_size(drawable_size); + let scale_factor = lock.scale_factor(); + let drawable_size = new_size.to_device_pixels(scale_factor); + lock.renderer.update_drawable_size(drawable_size); - if let Some(mut callback) = lock.resize_callback.take() { - let content_size = lock.content_size(); - let scale_factor = lock.scale_factor(); - drop(lock); - callback(content_size, scale_factor); - window_state.lock().resize_callback = Some(callback); - }; + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.lock().resize_callback = Some(callback); + }; } extern "C" fn display_layer(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.lock(); - if let Some(mut callback) = lock.request_frame_callback.take() { - lock.renderer.set_presents_with_transaction(true); - lock.stop_display_link(); - drop(lock); - callback(Default::default()); + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.request_frame_callback.take() { + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(Default::default()); - let mut lock = window_state.lock(); - lock.request_frame_callback = Some(callback); - lock.renderer.set_presents_with_transaction(false); - lock.start_display_link(); - } + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } } extern "C" fn step(view: *mut c_void) { - let view = view as id; - let window_state = unsafe { get_window_state(&*view) }; - let mut lock = window_state.lock(); + let view = view as id; + let window_state = unsafe { get_window_state(&*view) }; + let mut lock = window_state.lock(); - if let Some(mut callback) = lock.request_frame_callback.take() { - drop(lock); - callback(Default::default()); - window_state.lock().request_frame_callback = Some(callback); - } + if let Some(mut callback) = lock.request_frame_callback.take() { + drop(lock); + callback(Default::default()); + window_state.lock().request_frame_callback = Some(callback); + } } extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { - unsafe { msg_send![class!(NSArray), array] } + unsafe { msg_send![class!(NSArray), array] } } extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { - let has_marked_text_result = - with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); + let has_marked_text_result = + with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); - has_marked_text_result.is_some() as BOOL + has_marked_text_result.is_some() as BOOL } extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { - let marked_range_result = - with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); + let marked_range_result = + with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); - marked_range_result.map_or(NSRange::invalid(), |range| range.into()) + marked_range_result.map_or(NSRange::invalid(), |range| range.into()) } extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { - let selected_range_result = with_input_handler(this, |input_handler| { - input_handler.selected_text_range(false) - }) - .flatten(); + let selected_range_result = with_input_handler(this, |input_handler| { + input_handler.selected_text_range(false) + }) + .flatten(); - selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into()) + selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into()) } extern "C" fn first_rect_for_character_range( - this: &Object, - _: Sel, - range: NSRange, - _: id, + this: &Object, + _: Sel, + range: NSRange, + _: id, ) -> NSRect { - let frame = get_frame(this); - with_input_handler(this, |input_handler| { - input_handler.bounds_for_range(range.to_range()?) - }) - .flatten() - .map_or( - NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), - |bounds| { - NSRect::new( - NSPoint::new( - frame.origin.x + bounds.origin.x.as_f32() as f64, - frame.origin.y + frame.size.height - - bounds.origin.y.as_f32() as f64 - - bounds.size.height.as_f32() as f64, - ), - NSSize::new( - bounds.size.width.as_f32() as f64, - bounds.size.height.as_f32() as f64, - ), - ) - }, - ) + let frame = get_frame(this); + with_input_handler(this, |input_handler| { + input_handler.bounds_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |bounds| { + NSRect::new( + NSPoint::new( + frame.origin.x + bounds.origin.x.as_f32() as f64, + frame.origin.y + frame.size.height + - bounds.origin.y.as_f32() as f64 + - bounds.size.height.as_f32() as f64, + ), + NSSize::new( + bounds.size.width.as_f32() as f64, + bounds.size.height.as_f32() as f64, + ), + ) + }, + ) } fn get_frame(this: &Object) -> NSRect { - unsafe { - let state = get_window_state(this); - let lock = state.lock(); - let mut frame = NSWindow::frame(lock.native_window); - let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect]; - let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask]; - if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) { - frame.origin.y -= frame.size.height - content_layout_rect.size.height; - } - frame - } + unsafe { + let state = get_window_state(this); + let lock = state.lock(); + let mut frame = NSWindow::frame(lock.native_window); + let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect]; + let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask]; + if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) { + frame.origin.y -= frame.size.height - content_layout_rect.size.height; + } + frame + } } extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { - unsafe { - let is_attributed_string: BOOL = - msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; - let text: id = if is_attributed_string == YES { - msg_send![text, string] - } else { - text - }; + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; - let text = text.to_str(); - let replacement_range = replacement_range.to_range(); - with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range, text) - }); - } + let text = text.to_str(); + let replacement_range = replacement_range.to_range(); + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } } extern "C" fn set_marked_text( - this: &Object, - _: Sel, - text: id, - selected_range: NSRange, - replacement_range: NSRange, + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, ) { - unsafe { - let is_attributed_string: BOOL = - msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; - let text: id = if is_attributed_string == YES { - msg_send![text, string] - } else { - text - }; - let selected_range = selected_range.to_range(); - let replacement_range = replacement_range.to_range(); - let text = text.to_str(); - with_input_handler(this, |input_handler| { - input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range) - }); - } + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = text.to_str(); + with_input_handler(this, |input_handler| { + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range) + }); + } } extern "C" fn unmark_text(this: &Object, _: Sel) { - with_input_handler(this, |input_handler| input_handler.unmark_text()); + with_input_handler(this, |input_handler| input_handler.unmark_text()); } extern "C" fn attributed_substring_for_proposed_range( - this: &Object, - _: Sel, - range: NSRange, - actual_range: *mut c_void, + this: &Object, + _: Sel, + range: NSRange, + actual_range: *mut c_void, ) -> id { - with_input_handler(this, |input_handler| { - let range = range.to_range()?; - if range.is_empty() { - return None; - } - let mut adjusted: Option> = None; + with_input_handler(this, |input_handler| { + let range = range.to_range()?; + if range.is_empty() { + return None; + } + let mut adjusted: Option> = None; - let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; - if let Some(adjusted) = adjusted - && adjusted != range - { - unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; - } - unsafe { - let string: id = msg_send![class!(NSAttributedString), alloc]; - let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; - Some(string) - } - }) - .flatten() - .unwrap_or(nil) + let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; + if let Some(adjusted) = adjusted + && adjusted != range + { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + } + unsafe { + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) } // We ignore which selector it asks us to do because the user may have // bound the shortcut to something else. extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) { - let state = unsafe { get_window_state(this) }; - let mut lock = state.as_ref().lock(); - let keystroke = lock.keystroke_for_do_command.take(); - let mut event_callback = lock.event_callback.take(); - drop(lock); + let state = unsafe { get_window_state(this) }; + let mut lock = state.as_ref().lock(); + let keystroke = lock.keystroke_for_do_command.take(); + let mut event_callback = lock.event_callback.take(); + drop(lock); - if let Some((keystroke, callback)) = keystroke.zip(event_callback.as_mut()) { - let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: false, - prefer_character_input: false, - })); - state.as_ref().lock().do_command_handled = Some(!handled.propagate); - } + if let Some((keystroke, callback)) = keystroke.zip(event_callback.as_mut()) { + let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: false, + prefer_character_input: false, + })); + state.as_ref().lock().do_command_handled = Some(!handled.propagate); + } - state.as_ref().lock().event_callback = event_callback; + state.as_ref().lock().event_callback = event_callback; } extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { - unsafe { - let state = get_window_state(this); - let mut lock = state.as_ref().lock(); - if let Some(mut callback) = lock.appearance_changed_callback.take() { - drop(lock); - callback(); - state.lock().appearance_changed_callback = Some(callback); - } - } + unsafe { + let state = get_window_state(this); + let mut lock = state.as_ref().lock(); + if let Some(mut callback) = lock.appearance_changed_callback.take() { + drop(lock); + callback(); + state.lock().appearance_changed_callback = Some(callback); + } + } } extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - lock.first_mouse = true; - YES + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.first_mouse = true; + YES } extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 { - let position = screen_point_to_gpui_point(this, position); - with_input_handler(this, |input_handler| { - input_handler.character_index_for_point(position) - }) - .flatten() - .map(|index| index as u64) - .unwrap_or(NSNotFound as u64) + let position = screen_point_to_gpui_point(this, position); + with_input_handler(this, |input_handler| { + input_handler.character_index_for_point(position) + }) + .flatten() + .map(|index| index as u64) + .unwrap_or(NSNotFound as u64) } fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point { - let frame = get_frame(this); - let window_x = position.x - frame.origin.x; - let window_y = frame.size.height - (position.y - frame.origin.y); + let frame = get_frame(this); + let window_x = position.x - frame.origin.x; + let window_y = frame.size.height - (position.y - frame.origin.y); - point(px(window_x as f32), px(window_y as f32)) + point(px(window_x as f32), px(window_y as f32)) } extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { - let window_state = unsafe { get_window_state(this) }; - let position = drag_event_position(&window_state, dragging_info); - let paths = external_paths_from_event(dragging_info); - if let Some(event) = paths.map(|paths| FileDropEvent::Entered { position, paths }) - && send_file_drop_event(window_state, event) - { - return NSDragOperationCopy; - } - NSDragOperationNone + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + let paths = external_paths_from_event(dragging_info); + if let Some(event) = paths.map(|paths| FileDropEvent::Entered { position, paths }) + && send_file_drop_event(window_state, event) + { + return NSDragOperationCopy; + } + NSDragOperationNone } extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { - let window_state = unsafe { get_window_state(this) }; - let position = drag_event_position(&window_state, dragging_info); - if send_file_drop_event(window_state, FileDropEvent::Pending { position }) { - NSDragOperationCopy - } else { - NSDragOperationNone - } + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + if send_file_drop_event(window_state, FileDropEvent::Pending { position }) { + NSDragOperationCopy + } else { + NSDragOperationNone + } } extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - send_file_drop_event(window_state, FileDropEvent::Exited); + let window_state = unsafe { get_window_state(this) }; + send_file_drop_event(window_state, FileDropEvent::Exited); } extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL { - let window_state = unsafe { get_window_state(this) }; - let position = drag_event_position(&window_state, dragging_info); - send_file_drop_event(window_state, FileDropEvent::Submit { position }).to_objc() + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + send_file_drop_event(window_state, FileDropEvent::Submit { position }).to_objc() } fn external_paths_from_event(dragging_info: *mut Object) -> Option { - let mut paths = SmallVec::new(); - let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] }; - let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) }; - if filenames == nil { - return None; - } - for file in unsafe { filenames.iter() } { - let path = unsafe { - let f = NSString::UTF8String(file); - CStr::from_ptr(f).to_string_lossy().into_owned() - }; - paths.push(PathBuf::from(path)) - } - Some(ExternalPaths(paths)) + let mut paths = SmallVec::new(); + let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] }; + let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) }; + if filenames == nil { + return None; + } + for file in unsafe { filenames.iter() } { + let path = unsafe { + let f = NSString::UTF8String(file); + CStr::from_ptr(f).to_string_lossy().into_owned() + }; + paths.push(PathBuf::from(path)) + } + Some(ExternalPaths(paths)) } extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - send_file_drop_event(window_state, FileDropEvent::Exited); + let window_state = unsafe { get_window_state(this) }; + send_file_drop_event(window_state, FileDropEvent::Exited); } async fn synthetic_drag( - window_state: Weak>, - drag_id: usize, - event: MouseMoveEvent, - executor: BackgroundExecutor, + window_state: Weak>, + drag_id: usize, + event: MouseMoveEvent, + executor: BackgroundExecutor, ) { - loop { - executor.timer(Duration::from_millis(16)).await; - if let Some(window_state) = window_state.upgrade() { - let mut lock = window_state.lock(); - if lock.synthetic_drag_counter == drag_id { - if let Some(mut callback) = lock.event_callback.take() { - drop(lock); - callback(PlatformInput::MouseMove(event.clone())); - window_state.lock().event_callback = Some(callback); - } - } else { - break; - } - } - } + loop { + executor.timer(Duration::from_millis(16)).await; + if let Some(window_state) = window_state.upgrade() { + let mut lock = window_state.lock(); + if lock.synthetic_drag_counter == drag_id { + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(PlatformInput::MouseMove(event.clone())); + window_state.lock().event_callback = Some(callback); + } + } else { + break; + } + } + } } /// Sends the specified FileDropEvent using `PlatformInput::FileDrop` to the window /// state and updates the window state according to the event passed. fn send_file_drop_event( - window_state: Arc>, - file_drop_event: FileDropEvent, + window_state: Arc>, + file_drop_event: FileDropEvent, ) -> bool { - let external_files_dragged = match file_drop_event { - FileDropEvent::Entered { .. } => Some(true), - FileDropEvent::Exited => Some(false), - _ => None, - }; + let external_files_dragged = match file_drop_event { + FileDropEvent::Entered { .. } => Some(true), + FileDropEvent::Exited => Some(false), + _ => None, + }; - let mut lock = window_state.lock(); - if let Some(mut callback) = lock.event_callback.take() { - drop(lock); - callback(PlatformInput::FileDrop(file_drop_event)); - let mut lock = window_state.lock(); - lock.event_callback = Some(callback); - if let Some(external_files_dragged) = external_files_dragged { - lock.external_files_dragged = external_files_dragged; - } - true - } else { - false - } + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(PlatformInput::FileDrop(file_drop_event)); + let mut lock = window_state.lock(); + lock.event_callback = Some(callback); + if let Some(external_files_dragged) = external_files_dragged { + lock.external_files_dragged = external_files_dragged; + } + true + } else { + false + } } fn drag_event_position(window_state: &Mutex, dragging_info: id) -> Point { - let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] }; - convert_mouse_position(drag_location, window_state.lock().content_size().height) + let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] }; + convert_mouse_position(drag_location, window_state.lock().content_size().height) } fn with_input_handler(window: &Object, f: F) -> Option where - F: FnOnce(&mut PlatformInputHandler) -> R, + F: FnOnce(&mut PlatformInputHandler) -> R, { - let window_state = unsafe { get_window_state(window) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut input_handler) = lock.input_handler.take() { - drop(lock); - let result = f(&mut input_handler); - window_state.lock().input_handler = Some(input_handler); - Some(result) - } else { - None - } + let window_state = unsafe { get_window_state(window) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut input_handler) = lock.input_handler.take() { + drop(lock); + let result = f(&mut input_handler); + window_state.lock().input_handler = Some(input_handler); + Some(result) + } else { + None + } } unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID { - unsafe { - let device_description = NSScreen::deviceDescription(screen); - let screen_number_key: id = ns_string("NSScreenNumber"); - let screen_number = device_description.objectForKey_(screen_number_key); - let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue]; - screen_number as CGDirectDisplayID - } + unsafe { + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = ns_string("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue]; + screen_number as CGDirectDisplayID + } } extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id { - unsafe { - let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame]; - // Use a colorless semantic material. The default value `AppearanceBased`, though not - // manually set, is deprecated. - NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection); - NSVisualEffectView::setState_(view, NSVisualEffectState::Active); - view - } + unsafe { + let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame]; + // Use a colorless semantic material. The default value `AppearanceBased`, though not + // manually set, is deprecated. + NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection); + NSVisualEffectView::setState_(view, NSVisualEffectState::Active); + view + } } extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) { - unsafe { - let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer]; - let layer: id = msg_send![this, layer]; - if !layer.is_null() { - remove_layer_background(layer); - } - } + unsafe { + let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer]; + let layer: id = msg_send![this, layer]; + if !layer.is_null() { + remove_layer_background(layer); + } + } } unsafe fn remove_layer_background(layer: id) { - unsafe { - let _: () = msg_send![layer, setBackgroundColor:nil]; + unsafe { + let _: () = msg_send![layer, setBackgroundColor:nil]; - let class_name: id = msg_send![layer, className]; - if class_name.isEqualToString("CAChameleonLayer") { - // Remove the desktop tinting effect. - let _: () = msg_send![layer, setHidden: YES]; - return; - } + let class_name: id = msg_send![layer, className]; + if class_name.isEqualToString("CAChameleonLayer") { + // Remove the desktop tinting effect. + let _: () = msg_send![layer, setHidden: YES]; + return; + } - let filters: id = msg_send![layer, filters]; - if !filters.is_null() { - // Remove the increased saturation. - // The effect of a `CAFilter` or `CIFilter` is determined by its name, and the - // `description` reflects its name and some parameters. Currently `NSVisualEffectView` - // uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the - // `description` will still contain "Saturat" ("... inputSaturation = ..."). - let test_string: id = ns_string("Saturat"); - let count = NSArray::count(filters); - for i in 0..count { - let description: id = msg_send![filters.objectAtIndex(i), description]; - let hit: BOOL = msg_send![description, containsString: test_string]; - if hit == NO { - continue; - } + let filters: id = msg_send![layer, filters]; + if !filters.is_null() { + // Remove the increased saturation. + // The effect of a `CAFilter` or `CIFilter` is determined by its name, and the + // `description` reflects its name and some parameters. Currently `NSVisualEffectView` + // uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the + // `description` will still contain "Saturat" ("... inputSaturation = ..."). + let test_string: id = ns_string("Saturat"); + let count = NSArray::count(filters); + for i in 0..count { + let description: id = msg_send![filters.objectAtIndex(i), description]; + let hit: BOOL = msg_send![description, containsString: test_string]; + if hit == NO { + continue; + } - let all_indices = NSRange { - location: 0, - length: count, - }; - let indices: id = msg_send![class!(NSMutableIndexSet), indexSet]; - let _: () = msg_send![indices, addIndexesInRange: all_indices]; - let _: () = msg_send![indices, removeIndex:i]; - let filtered: id = msg_send![filters, objectsAtIndexes: indices]; - let _: () = msg_send![layer, setFilters: filtered]; - break; - } - } + let all_indices = NSRange { + location: 0, + length: count, + }; + let indices: id = msg_send![class!(NSMutableIndexSet), indexSet]; + let _: () = msg_send![indices, addIndexesInRange: all_indices]; + let _: () = msg_send![indices, removeIndex:i]; + let filtered: id = msg_send![filters, objectsAtIndexes: indices]; + let _: () = msg_send![layer, setFilters: filtered]; + break; + } + } - let sublayers: id = msg_send![layer, sublayers]; - if !sublayers.is_null() { - let count = NSArray::count(sublayers); - for i in 0..count { - let sublayer = sublayers.objectAtIndex(i); - remove_layer_background(sublayer); - } - } - } + let sublayers: id = msg_send![layer, sublayers]; + if !sublayers.is_null() { + let count = NSArray::count(sublayers); + for i in 0..count { + let sublayer = sublayers.objectAtIndex(i); + remove_layer_background(sublayer); + } + } + } } extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; - // Hide the native tab bar and set its height to 0, since we render our own. - let accessory_view: id = msg_send![view_controller, view]; - let _: () = msg_send![accessory_view, setHidden: YES]; - let mut frame: NSRect = msg_send![accessory_view, frame]; - frame.size.height = 0.0; - let _: () = msg_send![accessory_view, setFrame: frame]; - } + // Hide the native tab bar and set its height to 0, since we render our own. + let accessory_view: id = msg_send![view_controller, view]; + let _: () = msg_send![accessory_view, setHidden: YES]; + let mut frame: NSRect = msg_send![accessory_view, frame]; + frame.size.height = 0.0; + let _: () = msg_send![accessory_view, setFrame: frame]; + } } extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; - let window_state = get_window_state(this); - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { - drop(lock); - callback(); - window_state.lock().move_tab_to_new_window_callback = Some(callback); - } - } + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { + drop(lock); + callback(); + window_state.lock().move_tab_to_new_window_callback = Some(callback); + } + } } extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; - let window_state = get_window_state(this); - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.merge_all_windows_callback.take() { - drop(lock); - callback(); - window_state.lock().merge_all_windows_callback = Some(callback); - } - } + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.merge_all_windows_callback.take() { + drop(lock); + callback(); + window_state.lock().merge_all_windows_callback = Some(callback); + } + } } extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.select_next_tab_callback.take() { - drop(lock); - callback(); - window_state.lock().select_next_tab_callback = Some(callback); - } + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_next_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_next_tab_callback = Some(callback); + } } extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.select_previous_tab_callback.take() { - drop(lock); - callback(); - window_state.lock().select_previous_tab_callback = Some(callback); - } + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_previous_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_previous_tab_callback = Some(callback); + } } extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; - let window_state = get_window_state(this); - let mut lock = window_state.as_ref().lock(); - lock.move_traffic_light(); + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.move_traffic_light(); - if let Some(mut callback) = lock.toggle_tab_bar_callback.take() { - drop(lock); - callback(); - window_state.lock().toggle_tab_bar_callback = Some(callback); - } - } + if let Some(mut callback) = lock.toggle_tab_bar_callback.take() { + drop(lock); + callback(); + window_state.lock().toggle_tab_bar_callback = Some(callback); + } + } } diff --git a/crates/gpui_macos/src/window_appearance.rs b/crates/gpui_macos/src/window_appearance.rs index 02704bb0fc..7473c92b7b 100644 --- a/crates/gpui_macos/src/window_appearance.rs +++ b/crates/gpui_macos/src/window_appearance.rs @@ -1,35 +1,35 @@ use cocoa::{ - appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, - base::id, - foundation::NSString, + appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, + base::id, + foundation::NSString, }; use gpui::WindowAppearance; use objc::{msg_send, sel, sel_impl}; use std::ffi::CStr; pub(crate) unsafe fn window_appearance_from_native(appearance: id) -> WindowAppearance { - let name: id = msg_send![appearance, name]; - unsafe { - if name == NSAppearanceNameVibrantLight { - WindowAppearance::VibrantLight - } else if name == NSAppearanceNameVibrantDark { - WindowAppearance::VibrantDark - } else if name == NSAppearanceNameAqua { - WindowAppearance::Light - } else if name == NSAppearanceNameDarkAqua { - WindowAppearance::Dark - } else { - println!( - "unknown appearance: {:?}", - CStr::from_ptr(name.UTF8String()) - ); - WindowAppearance::Light - } - } + let name: id = msg_send![appearance, name]; + unsafe { + if name == NSAppearanceNameVibrantLight { + WindowAppearance::VibrantLight + } else if name == NSAppearanceNameVibrantDark { + WindowAppearance::VibrantDark + } else if name == NSAppearanceNameAqua { + WindowAppearance::Light + } else if name == NSAppearanceNameDarkAqua { + WindowAppearance::Dark + } else { + println!( + "unknown appearance: {:?}", + CStr::from_ptr(name.UTF8String()) + ); + WindowAppearance::Light + } + } } #[link(name = "AppKit", kind = "framework")] unsafe extern "C" { - pub static NSAppearanceNameAqua: id; - pub static NSAppearanceNameDarkAqua: id; + pub static NSAppearanceNameAqua: id; + pub static NSAppearanceNameDarkAqua: id; } diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index 4e6c6277e4..1fadbc59dd 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -5,27 +5,27 @@ use quote::quote; use syn::{Data, DeriveInput, LitStr, Token, parse::ParseStream}; pub(crate) fn derive_action(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); + let input = syn::parse_macro_input!(input as DeriveInput); - let struct_name = &input.ident; - let mut name_argument = None; - let mut deprecated_aliases = Vec::new(); - let mut no_json = false; - let mut no_register = false; - let mut namespace = None; - let mut deprecated = None; - let mut doc_str: Option = None; + let struct_name = &input.ident; + let mut name_argument = None; + let mut deprecated_aliases = Vec::new(); + let mut no_json = false; + let mut no_register = false; + let mut namespace = None; + let mut deprecated = None; + let mut doc_str: Option = None; - /* - * - * #[action()] - * Struct Foo { - * bar: bool // is bar considered an attribute - } - */ - for attr in &input.attrs { - if attr.path().is_ident("action") { - attr.parse_nested_meta(|meta| { + /* + * + * #[action()] + * Struct Foo { + * bar: bool // is bar considered an attribute + } + */ + for attr in &input.attrs { + if attr.path().is_ident("action") { + attr.parse_nested_meta(|meta| { if meta.path.is_ident("name") { if name_argument.is_some() { return Err(meta.error("'name' argument specified multiple times")); @@ -82,130 +82,129 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { Ok(()) }) .unwrap_or_else(|e| panic!("in #[action] attribute: {}", e)); - } else if attr.path().is_ident("doc") { - use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue}; - if let Meta::NameValue(MetaNameValue { - value: - Lit(ExprLit { - lit: Str(ref lit_str), - .. - }), - .. - }) = attr.meta - { - let doc = lit_str.value(); - let doc_str = doc_str.get_or_insert_default(); - doc_str.push_str(doc.trim()); - doc_str.push('\n'); - } - } - } + } else if attr.path().is_ident("doc") { + use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue}; + if let Meta::NameValue(MetaNameValue { + value: Lit(ExprLit { + lit: Str(ref lit_str), + .. + }), + .. + }) = attr.meta + { + let doc = lit_str.value(); + let doc_str = doc_str.get_or_insert_default(); + doc_str.push_str(doc.trim()); + doc_str.push('\n'); + } + } + } - let name = name_argument.unwrap_or_else(|| struct_name.to_string()); + let name = name_argument.unwrap_or_else(|| struct_name.to_string()); - if name.contains("::") { - panic!( - "in #[action] attribute: `name = \"{name}\"` must not contain `::`, \ + if name.contains("::") { + panic!( + "in #[action] attribute: `name = \"{name}\"` must not contain `::`, \ also specify `namespace` instead" - ); - } + ); + } - let full_name = if let Some(namespace) = namespace { - format!("{namespace}::{name}") - } else { - name - }; + let full_name = if let Some(namespace) = namespace { + format!("{namespace}::{name}") + } else { + name + }; - let is_unit_struct = matches!(&input.data, Data::Struct(data) if data.fields.is_empty()); + let is_unit_struct = matches!(&input.data, Data::Struct(data) if data.fields.is_empty()); - let build_fn_body = if no_json { - let error_msg = format!("{} cannot be built from JSON", full_name); - quote! { Err(gpui::private::anyhow::anyhow!(#error_msg)) } - } else if is_unit_struct { - quote! { Ok(Box::new(Self)) } - } else { - quote! { Ok(Box::new(gpui::private::serde_json::from_value::(_value)?)) } - }; + let build_fn_body = if no_json { + let error_msg = format!("{} cannot be built from JSON", full_name); + quote! { Err(gpui::private::anyhow::anyhow!(#error_msg)) } + } else if is_unit_struct { + quote! { Ok(Box::new(Self)) } + } else { + quote! { Ok(Box::new(gpui::private::serde_json::from_value::(_value)?)) } + }; - let json_schema_fn_body = if no_json || is_unit_struct { - quote! { None } - } else { - quote! { Some(::json_schema(_generator)) } - }; + let json_schema_fn_body = if no_json || is_unit_struct { + quote! { None } + } else { + quote! { Some(::json_schema(_generator)) } + }; - let deprecated_aliases_fn_body = if deprecated_aliases.is_empty() { - quote! { &[] } - } else { - let aliases = deprecated_aliases.iter(); - quote! { &[#(#aliases),*] } - }; + let deprecated_aliases_fn_body = if deprecated_aliases.is_empty() { + quote! { &[] } + } else { + let aliases = deprecated_aliases.iter(); + quote! { &[#(#aliases),*] } + }; - let deprecation_fn_body = if let Some(message) = deprecated { - quote! { Some(#message) } - } else { - quote! { None } - }; + let deprecation_fn_body = if let Some(message) = deprecated { + quote! { Some(#message) } + } else { + quote! { None } + }; - let documentation_fn_body = if let Some(doc) = doc_str { - let doc = doc.trim(); - quote! { Some(#doc) } - } else { - quote! { None } - }; + let documentation_fn_body = if let Some(doc) = doc_str { + let doc = doc.trim(); + quote! { Some(#doc) } + } else { + quote! { None } + }; - let registration = if no_register { - quote! {} - } else { - generate_register_action(struct_name) - }; + let registration = if no_register { + quote! {} + } else { + generate_register_action(struct_name) + }; - TokenStream::from(quote! { - #registration + TokenStream::from(quote! { + #registration - impl gpui::Action for #struct_name { - fn name(&self) -> &'static str { - #full_name - } + impl gpui::Action for #struct_name { + fn name(&self) -> &'static str { + #full_name + } - fn name_for_type() -> &'static str - where - Self: Sized - { - #full_name - } + fn name_for_type() -> &'static str + where + Self: Sized + { + #full_name + } - fn partial_eq(&self, action: &dyn gpui::Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } + fn partial_eq(&self, action: &dyn gpui::Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } - fn build(_value: gpui::private::serde_json::Value) -> gpui::Result> { - #build_fn_body - } + fn build(_value: gpui::private::serde_json::Value) -> gpui::Result> { + #build_fn_body + } - fn action_json_schema( - _generator: &mut gpui::private::schemars::SchemaGenerator, - ) -> Option { - #json_schema_fn_body - } + fn action_json_schema( + _generator: &mut gpui::private::schemars::SchemaGenerator, + ) -> Option { + #json_schema_fn_body + } - fn deprecated_aliases() -> &'static [&'static str] { - #deprecated_aliases_fn_body - } + fn deprecated_aliases() -> &'static [&'static str] { + #deprecated_aliases_fn_body + } - fn deprecation_message() -> Option<&'static str> { - #deprecation_fn_body - } + fn deprecation_message() -> Option<&'static str> { + #deprecation_fn_body + } - fn documentation() -> Option<&'static str> { - #documentation_fn_body - } - } - }) + fn documentation() -> Option<&'static str> { + #documentation_fn_body + } + } + }) } diff --git a/crates/gpui_macros/src/derive_app_context.rs b/crates/gpui_macros/src/derive_app_context.rs index 451731479c..affd4b8a98 100644 --- a/crates/gpui_macros/src/derive_app_context.rs +++ b/crates/gpui_macros/src/derive_app_context.rs @@ -5,115 +5,115 @@ use syn::{DeriveInput, parse_macro_input}; use crate::get_simple_attribute_field; pub fn derive_app_context(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); + let ast = parse_macro_input!(input as DeriveInput); - let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { - return quote! { - compile_error!("Derive must have an #[app] attribute to detect the &mut App field"); - } - .into(); - }; + let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { + return quote! { + compile_error!("Derive must have an #[app] attribute to detect the &mut App field"); + } + .into(); + }; - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - let r#gen = quote! { - impl #impl_generics gpui::AppContext for #type_name #type_generics - #where_clause - { - fn new( - &mut self, - build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, - ) -> gpui::Entity { - self.#app_variable.new(build_entity) - } + let r#gen = quote! { + impl #impl_generics gpui::AppContext for #type_name #type_generics + #where_clause + { + fn new( + &mut self, + build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, + ) -> gpui::Entity { + self.#app_variable.new(build_entity) + } - fn reserve_entity(&mut self) -> gpui::Reservation { - self.#app_variable.reserve_entity() - } + fn reserve_entity(&mut self) -> gpui::Reservation { + self.#app_variable.reserve_entity() + } - fn insert_entity( - &mut self, - reservation: gpui::Reservation, - build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, - ) -> gpui::Entity { - self.#app_variable.insert_entity(reservation, build_entity) - } + fn insert_entity( + &mut self, + reservation: gpui::Reservation, + build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, + ) -> gpui::Entity { + self.#app_variable.insert_entity(reservation, build_entity) + } - fn update_entity( - &mut self, - handle: &gpui::Entity, - update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R, - ) -> R - where - T: 'static, - { - self.#app_variable.update_entity(handle, update) - } + fn update_entity( + &mut self, + handle: &gpui::Entity, + update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R, + ) -> R + where + T: 'static, + { + self.#app_variable.update_entity(handle, update) + } - fn as_mut<'y, 'z, T>( - &'y mut self, - handle: &'z gpui::Entity, - ) -> gpui::GpuiBorrow<'y, T> - where - T: 'static, - { - self.#app_variable.as_mut(handle) - } + fn as_mut<'y, 'z, T>( + &'y mut self, + handle: &'z gpui::Entity, + ) -> gpui::GpuiBorrow<'y, T> + where + T: 'static, + { + self.#app_variable.as_mut(handle) + } - fn read_entity( - &self, - handle: &gpui::Entity, - read: impl FnOnce(&T, &gpui::App) -> R, - ) -> R - where - T: 'static, - { - self.#app_variable.read_entity(handle, read) - } + fn read_entity( + &self, + handle: &gpui::Entity, + read: impl FnOnce(&T, &gpui::App) -> R, + ) -> R + where + T: 'static, + { + self.#app_variable.read_entity(handle, read) + } - fn update_window(&mut self, window: gpui::AnyWindowHandle, f: F) -> gpui::Result - where - F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut gpui::App) -> T, - { - self.#app_variable.update_window(window, f) - } + fn update_window(&mut self, window: gpui::AnyWindowHandle, f: F) -> gpui::Result + where + F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut gpui::App) -> T, + { + self.#app_variable.update_window(window, f) + } - fn with_window( - &mut self, - entity_id: gpui::EntityId, - f: impl FnOnce(&mut gpui::Window, &mut gpui::App) -> R, - ) -> Option - { - self.#app_variable.with_window(entity_id, f) - } + fn with_window( + &mut self, + entity_id: gpui::EntityId, + f: impl FnOnce(&mut gpui::Window, &mut gpui::App) -> R, + ) -> Option + { + self.#app_variable.with_window(entity_id, f) + } - fn read_window( - &self, - window: &gpui::WindowHandle, - read: impl FnOnce(gpui::Entity, &gpui::App) -> R, - ) -> gpui::Result - where - T: 'static, - { - self.#app_variable.read_window(window, read) - } + fn read_window( + &self, + window: &gpui::WindowHandle, + read: impl FnOnce(gpui::Entity, &gpui::App) -> R, + ) -> gpui::Result + where + T: 'static, + { + self.#app_variable.read_window(window, read) + } - fn background_spawn(&self, future: impl std::future::Future + Send + 'static) -> gpui::Task - where - R: Send + 'static, - { - self.#app_variable.background_spawn(future) - } + fn background_spawn(&self, future: impl std::future::Future + Send + 'static) -> gpui::Task + where + R: Send + 'static, + { + self.#app_variable.background_spawn(future) + } - fn read_global(&self, callback: impl FnOnce(&G, &gpui::App) -> R) -> R - where - G: gpui::Global, - { - self.#app_variable.read_global(callback) - } - } - }; + fn read_global(&self, callback: impl FnOnce(&G, &gpui::App) -> R) -> R + where + G: gpui::Global, + { + self.#app_variable.read_global(callback) + } + } + }; - r#gen.into() + r#gen.into() } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index 9c1cb503a8..745b2fa8e0 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -6,94 +6,94 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::{ - Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type, - parse_macro_input, parse_quote, - visit_mut::{self, VisitMut}, + Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type, + parse_macro_input, parse_quote, + visit_mut::{self, VisitMut}, }; pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { - let mut item = parse_macro_input!(input as Item); + let mut item = parse_macro_input!(input as Item); - // First, expand any macros in the trait - match &mut item { - Item::Trait(trait_item) => { - let mut expander = MacroExpander; - expander.visit_item_trait_mut(trait_item); - } - _ => { - return syn::Error::new_spanned( - quote!(#item), - "#[derive_inspector_reflection] can only be applied to traits", - ) - .to_compile_error() - .into(); - } - } + // First, expand any macros in the trait + match &mut item { + Item::Trait(trait_item) => { + let mut expander = MacroExpander; + expander.visit_item_trait_mut(trait_item); + } + _ => { + return syn::Error::new_spanned( + quote!(#item), + "#[derive_inspector_reflection] can only be applied to traits", + ) + .to_compile_error() + .into(); + } + } - // Now process the expanded trait - match item { - Item::Trait(trait_item) => generate_reflected_trait(trait_item), - _ => unreachable!(), - } + // Now process the expanded trait + match item { + Item::Trait(trait_item) => generate_reflected_trait(trait_item), + _ => unreachable!(), + } } fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream { - let trait_name = &trait_item.ident; - let vis = &trait_item.vis; + let trait_name = &trait_item.ident; + let vis = &trait_item.vis; - // Determine if we're being called from within the gpui crate - let call_site = Span::call_site(); - let inspector_reflection_path = if is_called_from_gpui_crate(call_site) { - quote! { crate::inspector_reflection } - } else { - quote! { ::gpui::inspector_reflection } - }; + // Determine if we're being called from within the gpui crate + let call_site = Span::call_site(); + let inspector_reflection_path = if is_called_from_gpui_crate(call_site) { + quote! { crate::inspector_reflection } + } else { + quote! { ::gpui::inspector_reflection } + }; - // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self - let mut method_infos = Vec::new(); + // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self + let mut method_infos = Vec::new(); - for item in &trait_item.items { - if let TraitItem::Fn(method) = item { - let method_name = &method.sig.ident; + for item in &trait_item.items { + if let TraitItem::Fn(method) = item { + let method_name = &method.sig.ident; - // Check if method has self or mut self receiver - let has_valid_self_receiver = method - .sig - .inputs - .iter() - .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none())); + // Check if method has self or mut self receiver + let has_valid_self_receiver = method + .sig + .inputs + .iter() + .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none())); - // Check if method returns Self - let returns_self = match &method.sig.output { - ReturnType::Type(_, ty) => { - matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self")) - } - ReturnType::Default => false, - }; + // Check if method returns Self + let returns_self = match &method.sig.output { + ReturnType::Type(_, ty) => { + matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self")) + } + ReturnType::Default => false, + }; - // Check if method has exactly one parameter (self or mut self) - let param_count = method.sig.inputs.len(); + // Check if method has exactly one parameter (self or mut self) + let param_count = method.sig.inputs.len(); - // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self - // This includes methods with default implementations - if has_valid_self_receiver && returns_self && param_count == 1 { - // Extract documentation and cfg attributes - let doc = extract_doc_comment(&method.attrs); - let cfg_attrs = extract_cfg_attributes(&method.attrs); - method_infos.push((method_name.clone(), doc, cfg_attrs)); - } - } - } + // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self + // This includes methods with default implementations + if has_valid_self_receiver && returns_self && param_count == 1 { + // Extract documentation and cfg attributes + let doc = extract_doc_comment(&method.attrs); + let cfg_attrs = extract_cfg_attributes(&method.attrs); + method_infos.push((method_name.clone(), doc, cfg_attrs)); + } + } + } - // Generate the reflection module name - let reflection_mod_name = Ident::new( - &format!("{}_reflection", trait_name.to_string().to_snake_case()), - trait_name.span(), - ); + // Generate the reflection module name + let reflection_mod_name = Ident::new( + &format!("{}_reflection", trait_name.to_string().to_snake_case()), + trait_name.span(), + ); - // Generate wrapper functions for each method - // These wrappers use type erasure to allow runtime invocation - let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| { + // Generate wrapper functions for each method + // These wrappers use type erasure to allow runtime invocation + let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| { let wrapper_name = Ident::new( &format!("__wrapper_{}", method_name), method_name.span(), @@ -110,196 +110,196 @@ fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream { } }); - // Generate method info entries - let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| { - let method_name_str = method_name.to_string(); - let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span()); - let doc_expr = match doc { - Some(doc_str) => quote! { Some(#doc_str) }, - None => quote! { None }, - }; - quote! { - #(#cfg_attrs)* - #inspector_reflection_path::FunctionReflection { - name: #method_name_str, - function: #wrapper_name::, - documentation: #doc_expr, - _type: ::std::marker::PhantomData, - } - } - }); + // Generate method info entries + let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| { + let method_name_str = method_name.to_string(); + let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span()); + let doc_expr = match doc { + Some(doc_str) => quote! { Some(#doc_str) }, + None => quote! { None }, + }; + quote! { + #(#cfg_attrs)* + #inspector_reflection_path::FunctionReflection { + name: #method_name_str, + function: #wrapper_name::, + documentation: #doc_expr, + _type: ::std::marker::PhantomData, + } + } + }); - // Generate the complete output - let output = quote! { - #trait_item + // Generate the complete output + let output = quote! { + #trait_item - /// Implements function reflection - #vis mod #reflection_mod_name { - use super::*; + /// Implements function reflection + #vis mod #reflection_mod_name { + use super::*; - #(#wrapper_functions)* + #(#wrapper_functions)* - /// Get all reflectable methods for a concrete type implementing the trait - pub fn methods() -> Vec<#inspector_reflection_path::FunctionReflection> { - vec![ - #(#method_info_entries),* - ] - } + /// Get all reflectable methods for a concrete type implementing the trait + pub fn methods() -> Vec<#inspector_reflection_path::FunctionReflection> { + vec![ + #(#method_info_entries),* + ] + } - /// Find a method by name for a concrete type implementing the trait - pub fn find_method(name: &str) -> Option<#inspector_reflection_path::FunctionReflection> { - methods::().into_iter().find(|m| m.name == name) - } - } - }; + /// Find a method by name for a concrete type implementing the trait + pub fn find_method(name: &str) -> Option<#inspector_reflection_path::FunctionReflection> { + methods::().into_iter().find(|m| m.name == name) + } + } + }; - TokenStream::from(output) + TokenStream::from(output) } fn extract_doc_comment(attrs: &[Attribute]) -> Option { - let mut doc_lines = Vec::new(); + let mut doc_lines = Vec::new(); - for attr in attrs { - if attr.path().is_ident("doc") - && let Meta::NameValue(meta) = &attr.meta - && let Expr::Lit(expr_lit) = &meta.value - && let Lit::Str(lit_str) = &expr_lit.lit - { - let line = lit_str.value(); - let line = line.strip_prefix(' ').unwrap_or(&line); - doc_lines.push(line.to_string()); - } - } + for attr in attrs { + if attr.path().is_ident("doc") + && let Meta::NameValue(meta) = &attr.meta + && let Expr::Lit(expr_lit) = &meta.value + && let Lit::Str(lit_str) = &expr_lit.lit + { + let line = lit_str.value(); + let line = line.strip_prefix(' ').unwrap_or(&line); + doc_lines.push(line.to_string()); + } + } - if doc_lines.is_empty() { - None - } else { - Some(doc_lines.join("\n")) - } + if doc_lines.is_empty() { + None + } else { + Some(doc_lines.join("\n")) + } } fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")) - .cloned() - .collect() + attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect() } fn is_called_from_gpui_crate(_span: Span) -> bool { - // Check if we're being called from within the gpui crate by examining the call site - // This is a heuristic approach - we check if the current crate name is "gpui" - std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui") + // Check if we're being called from within the gpui crate by examining the call site + // This is a heuristic approach - we check if the current crate name is "gpui" + std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui") } struct MacroExpander; impl VisitMut for MacroExpander { - fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) { - let mut expanded_items = Vec::new(); - let mut items_to_keep = Vec::new(); + fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) { + let mut expanded_items = Vec::new(); + let mut items_to_keep = Vec::new(); - for item in trait_item.items.drain(..) { - match item { - TraitItem::Macro(macro_item) => { - // Try to expand known macros - if let Some(expanded) = try_expand_macro(¯o_item) { - expanded_items.extend(expanded); - } else { - // Keep unknown macros as-is - items_to_keep.push(TraitItem::Macro(macro_item)); - } - } - other => { - items_to_keep.push(other); - } - } - } + for item in trait_item.items.drain(..) { + match item { + TraitItem::Macro(macro_item) => { + // Try to expand known macros + if let Some(expanded) = try_expand_macro(¯o_item) { + expanded_items.extend(expanded); + } else { + // Keep unknown macros as-is + items_to_keep.push(TraitItem::Macro(macro_item)); + } + } + other => { + items_to_keep.push(other); + } + } + } - // Rebuild the items list with expanded content first, then original items - trait_item.items = expanded_items; - trait_item.items.extend(items_to_keep); + // Rebuild the items list with expanded content first, then original items + trait_item.items = expanded_items; + trait_item.items.extend(items_to_keep); - // Continue visiting - visit_mut::visit_item_trait_mut(self, trait_item); - } + // Continue visiting + visit_mut::visit_item_trait_mut(self, trait_item); + } } fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option> { - let path = ¯o_item.mac.path; + let path = ¯o_item.mac.path; - // Check if this is one of our known style macros - let macro_name = path_to_string(path); + // Check if this is one of our known style macros + let macro_name = path_to_string(path); - // Handle the known macros by calling their implementations - match macro_name.as_str() { - "gpui_macros::style_helpers" | "style_helpers" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::style_helpers(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::visibility_style_methods" | "visibility_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::margin_style_methods" | "margin_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::padding_style_methods" | "padding_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::position_style_methods" | "position_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::position_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::overflow_style_methods" | "overflow_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::cursor_style_methods" | "cursor_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::border_style_methods" | "border_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::border_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - _ => None, - } + // Handle the known macros by calling their implementations + match macro_name.as_str() { + "gpui_macros::style_helpers" | "style_helpers" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::style_helpers(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::visibility_style_methods" | "visibility_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::margin_style_methods" | "margin_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::padding_style_methods" | "padding_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::position_style_methods" | "position_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::position_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::overflow_style_methods" | "overflow_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::cursor_style_methods" | "cursor_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::border_style_methods" | "border_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::border_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + _ => None, + } } fn path_to_string(path: &Path) -> String { - path.segments - .iter() - .map(|seg| seg.ident.to_string()) - .collect::>() - .join("::") + path.segments + .iter() + .map(|seg| seg.ident.to_string()) + .collect::>() + .join("::") } fn parse_expanded_items(expanded: TokenStream) -> Option> { - let tokens = TokenStream2::from(expanded); + let tokens = TokenStream2::from(expanded); - // Try to parse the expanded tokens as trait items - // We need to wrap them in a dummy trait to parse properly - let dummy_trait: ItemTrait = parse_quote! { - trait Dummy { - #tokens - } - }; + // Try to parse the expanded tokens as trait items + // We need to wrap them in a dummy trait to parse properly + let dummy_trait: ItemTrait = parse_quote! { + trait Dummy { + #tokens + } + }; - Some(dummy_trait.items) + Some(dummy_trait.items) } diff --git a/crates/gpui_macros/src/derive_into_element.rs b/crates/gpui_macros/src/derive_into_element.rs index 89d609ae65..94414cf01b 100644 --- a/crates/gpui_macros/src/derive_into_element.rs +++ b/crates/gpui_macros/src/derive_into_element.rs @@ -3,22 +3,22 @@ use quote::quote; use syn::{DeriveInput, parse_macro_input}; pub fn derive_into_element(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - let r#gen = quote! { - impl #impl_generics gpui::IntoElement for #type_name #type_generics - #where_clause - { - type Element = gpui::Component; + let r#gen = quote! { + impl #impl_generics gpui::IntoElement for #type_name #type_generics + #where_clause + { + type Element = gpui::Component; - #[track_caller] - fn into_element(self) -> Self::Element { - gpui::Component::new(self) - } - } - }; + #[track_caller] + fn into_element(self) -> Self::Element { + gpui::Component::new(self) + } + } + }; - r#gen.into() + r#gen.into() } diff --git a/crates/gpui_macros/src/derive_render.rs b/crates/gpui_macros/src/derive_render.rs index 3e0dcbc993..e6fc2b8752 100644 --- a/crates/gpui_macros/src/derive_render.rs +++ b/crates/gpui_macros/src/derive_render.rs @@ -3,19 +3,19 @@ use quote::quote; use syn::{DeriveInput, parse_macro_input}; pub fn derive_render(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - let r#gen = quote! { - impl #impl_generics gpui::Render for #type_name #type_generics - #where_clause - { - fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context) -> impl gpui::Element { - gpui::Empty - } - } - }; + let r#gen = quote! { + impl #impl_generics gpui::Render for #type_name #type_generics + #where_clause + { + fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context) -> impl gpui::Element { + gpui::Empty + } + } + }; - r#gen.into() + r#gen.into() } diff --git a/crates/gpui_macros/src/derive_visual_context.rs b/crates/gpui_macros/src/derive_visual_context.rs index a639b6d2d6..62aa8b0723 100644 --- a/crates/gpui_macros/src/derive_visual_context.rs +++ b/crates/gpui_macros/src/derive_visual_context.rs @@ -5,69 +5,69 @@ use syn::{DeriveInput, parse_macro_input}; use super::get_simple_attribute_field; pub fn derive_visual_context(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); + let ast = parse_macro_input!(input as DeriveInput); - let Some(window_variable) = get_simple_attribute_field(&ast, "window") else { - return quote! { - compile_error!("Derive must have a #[window] attribute to detect the &mut Window field"); - } - .into(); - }; + let Some(window_variable) = get_simple_attribute_field(&ast, "window") else { + return quote! { + compile_error!("Derive must have a #[window] attribute to detect the &mut Window field"); + } + .into(); + }; - let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { - return quote! { - compile_error!("Derive must have a #[app] attribute to detect the &mut App field"); - } - .into(); - }; + let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { + return quote! { + compile_error!("Derive must have a #[app] attribute to detect the &mut App field"); + } + .into(); + }; - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - let r#gen = quote! { - impl #impl_generics gpui::VisualContext for #type_name #type_generics - #where_clause - { - type Result = T; + let r#gen = quote! { + impl #impl_generics gpui::VisualContext for #type_name #type_generics + #where_clause + { + type Result = T; - fn window_handle(&self) -> gpui::AnyWindowHandle { - self.#window_variable.window_handle() - } + fn window_handle(&self) -> gpui::AnyWindowHandle { + self.#window_variable.window_handle() + } - fn update_window_entity( - &mut self, - entity: &gpui::Entity, - update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context) -> R, - ) -> R { - gpui::AppContext::update_entity(self.#app_variable, entity, |entity, cx| update(entity, self.#window_variable, cx)) - } + fn update_window_entity( + &mut self, + entity: &gpui::Entity, + update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context) -> R, + ) -> R { + gpui::AppContext::update_entity(self.#app_variable, entity, |entity, cx| update(entity, self.#window_variable, cx)) + } - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T, - ) -> gpui::Entity { - gpui::AppContext::new(self.#app_variable, |cx| build_entity(self.#window_variable, cx)) - } + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T, + ) -> gpui::Entity { + gpui::AppContext::new(self.#app_variable, |cx| build_entity(self.#window_variable, cx)) + } - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context) -> V, - ) -> gpui::Entity - where - V: 'static + gpui::Render, - { - self.#window_variable.replace_root(self.#app_variable, build_view) - } + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context) -> V, + ) -> gpui::Entity + where + V: 'static + gpui::Render, + { + self.#window_variable.replace_root(self.#app_variable, build_view) + } - fn focus(&mut self, entity: &gpui::Entity) - where - V: gpui::Focusable, - { - let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable); - self.#window_variable.focus(&focus_handle, self.#app_variable); - } - } - }; + fn focus(&mut self, entity: &gpui::Entity) + where + V: gpui::Focusable, + { + let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable); + self.#window_variable.focus(&focus_handle, self.#app_variable); + } + } + }; - r#gen.into() + r#gen.into() } diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index e2e174a1b2..83da2cbcff 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -17,7 +17,7 @@ use syn::{DeriveInput, Ident}; /// `Action` derive macro - see the trait documentation for details. #[proc_macro_derive(Action, attributes(action))] pub fn derive_action(input: TokenStream) -> TokenStream { - derive_action::derive_action(input) + derive_action::derive_action(input) } /// This can be used to register an action with the GPUI runtime when you want to manually implement @@ -25,20 +25,20 @@ pub fn derive_action(input: TokenStream) -> TokenStream { /// instead. #[proc_macro] pub fn register_action(ident: TokenStream) -> TokenStream { - register_action::register_action(ident) + register_action::register_action(ident) } /// #[derive(IntoElement)] is used to create a Component out of anything that implements /// the `RenderOnce` trait. #[proc_macro_derive(IntoElement)] pub fn derive_into_element(input: TokenStream) -> TokenStream { - derive_into_element::derive_into_element(input) + derive_into_element::derive_into_element(input) } #[proc_macro_derive(Render)] #[doc(hidden)] pub fn derive_render(input: TokenStream) -> TokenStream { - derive_render::derive_render(input) + derive_render::derive_render(input) } /// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App` @@ -56,7 +56,7 @@ pub fn derive_render(input: TokenStream) -> TokenStream { /// ``` #[proc_macro_derive(AppContext, attributes(app))] pub fn derive_app_context(input: TokenStream) -> TokenStream { - derive_app_context::derive_app_context(input) + derive_app_context::derive_app_context(input) } /// #[derive(VisualContext)] is used to create a visual context out of anything that holds a `&mut Window` and @@ -89,62 +89,62 @@ pub fn derive_app_context(input: TokenStream) -> TokenStream { /// ``` #[proc_macro_derive(VisualContext, attributes(window, app))] pub fn derive_visual_context(input: TokenStream) -> TokenStream { - derive_visual_context::derive_visual_context(input) + derive_visual_context::derive_visual_context(input) } /// Used by GPUI to generate the style helpers. #[proc_macro] #[doc(hidden)] pub fn style_helpers(input: TokenStream) -> TokenStream { - styles::style_helpers(input) + styles::style_helpers(input) } /// Generates methods for visibility styles. #[proc_macro] pub fn visibility_style_methods(input: TokenStream) -> TokenStream { - styles::visibility_style_methods(input) + styles::visibility_style_methods(input) } /// Generates methods for margin styles. #[proc_macro] pub fn margin_style_methods(input: TokenStream) -> TokenStream { - styles::margin_style_methods(input) + styles::margin_style_methods(input) } /// Generates methods for padding styles. #[proc_macro] pub fn padding_style_methods(input: TokenStream) -> TokenStream { - styles::padding_style_methods(input) + styles::padding_style_methods(input) } /// Generates methods for position styles. #[proc_macro] pub fn position_style_methods(input: TokenStream) -> TokenStream { - styles::position_style_methods(input) + styles::position_style_methods(input) } /// Generates methods for overflow styles. #[proc_macro] pub fn overflow_style_methods(input: TokenStream) -> TokenStream { - styles::overflow_style_methods(input) + styles::overflow_style_methods(input) } /// Generates methods for cursor styles. #[proc_macro] pub fn cursor_style_methods(input: TokenStream) -> TokenStream { - styles::cursor_style_methods(input) + styles::cursor_style_methods(input) } /// Generates methods for border styles. #[proc_macro] pub fn border_style_methods(input: TokenStream) -> TokenStream { - styles::border_style_methods(input) + styles::border_style_methods(input) } /// Generates methods for box shadow styles. #[proc_macro] pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { - styles::box_shadow_style_methods(input) + styles::box_shadow_style_methods(input) } /// `#[gpui::test]` can be used to annotate test functions that run with GPUI support. @@ -186,7 +186,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { /// - `ITERATIONS`: forces the value of the `iterations` argument #[proc_macro_attribute] pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { - test::test(args, function) + test::test(args, function) } /// A variant of `#[gpui::test]` that supports property-based testing. @@ -259,7 +259,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { /// [`Strategy`]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html #[proc_macro_attribute] pub fn property_test(args: TokenStream, function: TokenStream) -> TokenStream { - property_test::test(args.into(), function.into()).into() + property_test::test(args.into(), function.into()).into() } /// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides @@ -281,17 +281,17 @@ pub fn property_test(args: TokenStream, function: TokenStream) -> TokenStream { #[cfg(any(feature = "inspector", debug_assertions))] #[proc_macro_attribute] pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { - derive_inspector_reflection::derive_inspector_reflection(_args, input) + derive_inspector_reflection::derive_inspector_reflection(_args, input) } pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option { - match &ast.data { - syn::Data::Struct(data_struct) => data_struct - .fields - .iter() - .find(|field| field.attrs.iter().any(|attr| attr.path().is_ident(name))) - .map(|field| field.ident.clone().unwrap()), - syn::Data::Enum(_) => None, - syn::Data::Union(_) => None, - } + match &ast.data { + syn::Data::Struct(data_struct) => data_struct + .fields + .iter() + .find(|field| field.attrs.iter().any(|attr| attr.path().is_ident(name))) + .map(|field| field.ident.clone().unwrap()), + syn::Data::Enum(_) => None, + syn::Data::Union(_) => None, + } } diff --git a/crates/gpui_macros/src/property_test.rs b/crates/gpui_macros/src/property_test.rs index fe1585cc06..3839f7cbfa 100644 --- a/crates/gpui_macros/src/property_test.rs +++ b/crates/gpui_macros/src/property_test.rs @@ -1,205 +1,205 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote, quote_spanned}; use syn::{ - Expr, FnArg, Ident, ItemFn, MetaNameValue, Token, Type, - parse::{Parse, ParseStream}, - parse2, - punctuated::Punctuated, - spanned::Spanned, - token::Comma, + Expr, FnArg, Ident, ItemFn, MetaNameValue, Token, Type, + parse::{Parse, ParseStream}, + parse2, + punctuated::Punctuated, + spanned::Spanned, + token::Comma, }; pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { - let item_span = item.span(); - let Ok(func) = parse2::(item) else { - return quote_spanned! { item_span => - compile_error!("#[gpui::property_test] must be placed on a function"); - }; - }; + let item_span = item.span(); + let Ok(func) = parse2::(item) else { + return quote_spanned! { item_span => + compile_error!("#[gpui::property_test] must be placed on a function"); + }; + }; - let args = match parse2::(args) { - Ok(args) => args, - Err(e) => return e.to_compile_error(), - }; + let args = match parse2::(args) { + Ok(args) => args, + Err(e) => return e.to_compile_error(), + }; - let test_name = func.sig.ident.clone(); - let test_ret_ty = func.sig.output.clone(); - let inner_fn_name = format_ident!("__{test_name}"); - let outer_fn_attributes = &func.attrs; + let test_name = func.sig.ident.clone(); + let test_ret_ty = func.sig.output.clone(); + let inner_fn_name = format_ident!("__{test_name}"); + let outer_fn_attributes = &func.attrs; - let parsed_args = parse_args(func.sig.inputs, &test_name); + let parsed_args = parse_args(func.sig.inputs, &test_name); - let inner_body = func.block; - let inner_arg_decls = parsed_args.inner_fn_decl_args; - let asyncness = func.sig.asyncness; + let inner_body = func.block; + let inner_arg_decls = parsed_args.inner_fn_decl_args; + let asyncness = func.sig.asyncness; - let inner_fn = quote! { - let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body; - }; + let inner_fn = quote! { + let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body; + }; - let arg_errors = parsed_args.errors; - let proptest_args = parsed_args.proptest_args; - let inner_args = parsed_args.inner_fn_args; - let cx_vars = parsed_args.cx_vars; - let cx_teardowns = parsed_args.cx_teardowns; + let arg_errors = parsed_args.errors; + let proptest_args = parsed_args.proptest_args; + let inner_args = parsed_args.inner_fn_args; + let cx_vars = parsed_args.cx_vars; + let cx_teardowns = parsed_args.cx_teardowns; - let proptest_args = quote! { - #[strategy = ::gpui::seed_strategy()] __seed: u64, - #proptest_args - }; + let proptest_args = quote! { + #[strategy = ::gpui::seed_strategy()] __seed: u64, + #proptest_args + }; - let run_test_body = match &asyncness { - None => quote! { - #cx_vars - let result = #inner_fn_name(#inner_args); - #cx_teardowns - result - }, - Some(_) => quote! { - let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); - #cx_vars - let result = foreground_executor.block_test(#inner_fn_name(#inner_args)); - #cx_teardowns - result - }, - }; + let run_test_body = match &asyncness { + None => quote! { + #cx_vars + let result = #inner_fn_name(#inner_args); + #cx_teardowns + result + }, + Some(_) => quote! { + let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); + #cx_vars + let result = foreground_executor.block_test(#inner_fn_name(#inner_args)); + #cx_teardowns + result + }, + }; - let fixed_macro_invocation = args.render(); + let fixed_macro_invocation = args.render(); - quote! { - #arg_errors + quote! { + #arg_errors - #fixed_macro_invocation - #(#outer_fn_attributes)* - fn #test_name(#proptest_args) #test_ret_ty { - #inner_fn + #fixed_macro_invocation + #(#outer_fn_attributes)* + fn #test_name(#proptest_args) #test_ret_ty { + #inner_fn - ::gpui::run_test_once( - __seed, - Box::new(move |dispatcher| #test_ret_ty { - #run_test_body - }), - ) - } - } + ::gpui::run_test_once( + __seed, + Box::new(move |dispatcher| #test_ret_ty { + #run_test_body + }), + ) + } + } } struct Args { - config: Option, - remaining_args: Vec, - errors: TokenStream, + config: Option, + remaining_args: Vec, + errors: TokenStream, } impl Args { - /// By default, proptest uses random seeds unless `$PROPTEST_SEED` is set. - /// Rather than managing both `$SEED` and `$PROPTEST_SEED`, we intercept - /// `config = ...` tokens and add a call to `gpui::apply_seed_to_config`. - fn render(&self) -> TokenStream { - let user_provided_config = match &self.config { - None => quote! { ::gpui::proptest::prelude::ProptestConfig::default() }, - Some(config) => config.into_token_stream(), - }; + /// By default, proptest uses random seeds unless `$PROPTEST_SEED` is set. + /// Rather than managing both `$SEED` and `$PROPTEST_SEED`, we intercept + /// `config = ...` tokens and add a call to `gpui::apply_seed_to_config`. + fn render(&self) -> TokenStream { + let user_provided_config = match &self.config { + None => quote! { ::gpui::proptest::prelude::ProptestConfig::default() }, + Some(config) => config.into_token_stream(), + }; - let fixed_config = quote!(::gpui::apply_seed_to_proptest_config(#user_provided_config)); - let remaining_args = &self.remaining_args; - let errors = &self.errors; + let fixed_config = quote!(::gpui::apply_seed_to_proptest_config(#user_provided_config)); + let remaining_args = &self.remaining_args; + let errors = &self.errors; - quote! { - #errors - #[::gpui::proptest::property_test( - proptest_path = "::gpui::proptest", - config = #fixed_config, - #(#remaining_args,)* - )] - } - } + quote! { + #errors + #[::gpui::proptest::property_test( + proptest_path = "::gpui::proptest", + config = #fixed_config, + #(#remaining_args,)* + )] + } + } } impl Parse for Args { - fn parse(input: ParseStream) -> syn::Result { - let pairs = Punctuated::::parse_terminated(input)?; + fn parse(input: ParseStream) -> syn::Result { + let pairs = Punctuated::::parse_terminated(input)?; - let mut config = None; - let mut remaining_args = vec![]; - let mut errors = quote!(); + let mut config = None; + let mut remaining_args = vec![]; + let mut errors = quote!(); - for pair in pairs { - match pair.path.get_ident().map(Ident::to_string).as_deref() { - Some("config") => config = Some(pair.value), - Some("proptest_path") => errors.extend(quote_spanned! {pair.span() => - compile_error!("`gpui::property_test` overrides the `proptest_path` parameter") - }), - _ => remaining_args.push(pair), - } - } + for pair in pairs { + match pair.path.get_ident().map(Ident::to_string).as_deref() { + Some("config") => config = Some(pair.value), + Some("proptest_path") => errors.extend(quote_spanned! {pair.span() => + compile_error!("`gpui::property_test` overrides the `proptest_path` parameter") + }), + _ => remaining_args.push(pair), + } + } - Ok(Self { - config, - remaining_args, - errors, - }) - } + Ok(Self { + config, + remaining_args, + errors, + }) + } } #[derive(Default)] struct ParsedArgs { - cx_vars: TokenStream, - cx_teardowns: TokenStream, - proptest_args: TokenStream, - errors: TokenStream, + cx_vars: TokenStream, + cx_teardowns: TokenStream, + proptest_args: TokenStream, + errors: TokenStream, - // exprs passed at the call-site - inner_fn_args: TokenStream, - // args in the declaration - inner_fn_decl_args: TokenStream, + // exprs passed at the call-site + inner_fn_args: TokenStream, + // args in the declaration + inner_fn_decl_args: TokenStream, } fn parse_args(args: Punctuated, test_name: &Ident) -> ParsedArgs { - let mut parsed = ParsedArgs::default(); - let mut args = args.into_iter().collect(); + let mut parsed = ParsedArgs::default(); + let mut args = args.into_iter().collect(); - remove_cxs(&mut parsed, &mut args, test_name); - remove_std_rng(&mut parsed, &mut args); - remove_background_executor(&mut parsed, &mut args); + remove_cxs(&mut parsed, &mut args, test_name); + remove_std_rng(&mut parsed, &mut args); + remove_background_executor(&mut parsed, &mut args); - // all remaining args forwarded to proptest's macro - parsed.proptest_args = quote!( #(#args),* ); + // all remaining args forwarded to proptest's macro + parsed.proptest_args = quote!( #(#args),* ); - parsed + parsed } fn remove_cxs(parsed: &mut ParsedArgs, args: &mut Vec, test_name: &Ident) { - let mut ix = 0; - args.retain_mut(|arg| { - if !is_test_cx(arg) { - return true; - } + let mut ix = 0; + args.retain_mut(|arg| { + if !is_test_cx(arg) { + return true; + } - let cx_varname = format_ident!("cx_{ix}"); - ix += 1; + let cx_varname = format_ident!("cx_{ix}"); + ix += 1; - parsed.cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#test_name)), - ); - )); - parsed.cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); + parsed.cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#test_name)), + ); + )); + parsed.cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); - parsed.inner_fn_decl_args.extend(quote!(#arg,)); - parsed.inner_fn_args.extend(quote!(&mut #cx_varname,)); + parsed.inner_fn_decl_args.extend(quote!(#arg,)); + parsed.inner_fn_args.extend(quote!(&mut #cx_varname,)); - false - }); + false + }); } fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec) { - args.retain_mut(|arg| { + args.retain_mut(|arg| { if !is_std_rng(arg) { return true; } @@ -213,61 +213,61 @@ fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec) { } fn remove_background_executor(parsed: &mut ParsedArgs, args: &mut Vec) { - args.retain_mut(|arg| { - if !is_background_executor(arg) { - return true; - } + args.retain_mut(|arg| { + if !is_background_executor(arg) { + return true; + } - parsed.inner_fn_decl_args.extend(quote!(#arg,)); - parsed - .inner_fn_args - .extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new( - dispatcher.clone() - )),)); + parsed.inner_fn_decl_args.extend(quote!(#arg,)); + parsed + .inner_fn_args + .extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new( + dispatcher.clone() + )),)); - false - }); + false + }); } // Matches `&TestAppContext` or `&foo::bar::baz::TestAppContext` fn is_test_cx(arg: &FnArg) -> bool { - let FnArg::Typed(arg) = arg else { - return false; - }; + let FnArg::Typed(arg) = arg else { + return false; + }; - let Type::Reference(ty) = &*arg.ty else { - return false; - }; + let Type::Reference(ty) = &*arg.ty else { + return false; + }; - let Type::Path(ty) = &*ty.elem else { - return false; - }; + let Type::Path(ty) = &*ty.elem else { + return false; + }; - ty.path - .segments - .last() - .is_some_and(|seg| seg.ident == "TestAppContext") + ty.path + .segments + .last() + .is_some_and(|seg| seg.ident == "TestAppContext") } fn is_std_rng(arg: &FnArg) -> bool { - is_path_with_last_segment(arg, "StdRng") + is_path_with_last_segment(arg, "StdRng") } fn is_background_executor(arg: &FnArg) -> bool { - is_path_with_last_segment(arg, "BackgroundExecutor") + is_path_with_last_segment(arg, "BackgroundExecutor") } fn is_path_with_last_segment(arg: &FnArg, last_segment: &str) -> bool { - let FnArg::Typed(arg) = arg else { - return false; - }; + let FnArg::Typed(arg) = arg else { + return false; + }; - let Type::Path(ty) = &*arg.ty else { - return false; - }; + let Type::Path(ty) = &*arg.ty else { + return false; + }; - ty.path - .segments - .last() - .is_some_and(|seg| seg.ident == last_segment) + ty.path + .segments + .last() + .is_some_and(|seg| seg.ident == last_segment) } diff --git a/crates/gpui_macros/src/register_action.rs b/crates/gpui_macros/src/register_action.rs index ca36ce3186..5934d2afad 100644 --- a/crates/gpui_macros/src/register_action.rs +++ b/crates/gpui_macros/src/register_action.rs @@ -4,44 +4,44 @@ use quote::{format_ident, quote}; use syn::parse_macro_input; pub(crate) fn register_action(ident: TokenStream) -> TokenStream { - let name = parse_macro_input!(ident as Ident); - let registration = generate_register_action(&name); + let name = parse_macro_input!(ident as Ident); + let registration = generate_register_action(&name); - TokenStream::from(quote! { - #registration - }) + TokenStream::from(quote! { + #registration + }) } pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 { - let action_builder_fn_name = format_ident!( - "__gpui_actions_builder_{}", - type_name.to_string().to_lowercase() - ); + let action_builder_fn_name = format_ident!( + "__gpui_actions_builder_{}", + type_name.to_string().to_lowercase() + ); - quote! { - impl #type_name { - /// This is an auto generated function, do not use. - #[automatically_derived] - #[doc(hidden)] - fn __autogenerated() { - /// This is an auto generated function, do not use. - #[doc(hidden)] - fn #action_builder_fn_name() -> gpui::MacroActionData { - gpui::MacroActionData { - name: <#type_name as gpui::Action>::name_for_type(), - type_id: ::std::any::TypeId::of::<#type_name>(), - build: <#type_name as gpui::Action>::build, - json_schema: <#type_name as gpui::Action>::action_json_schema, - deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(), - deprecation_message: <#type_name as gpui::Action>::deprecation_message(), - documentation: <#type_name as gpui::Action>::documentation(), - } - } + quote! { + impl #type_name { + /// This is an auto generated function, do not use. + #[automatically_derived] + #[doc(hidden)] + fn __autogenerated() { + /// This is an auto generated function, do not use. + #[doc(hidden)] + fn #action_builder_fn_name() -> gpui::MacroActionData { + gpui::MacroActionData { + name: <#type_name as gpui::Action>::name_for_type(), + type_id: ::std::any::TypeId::of::<#type_name>(), + build: <#type_name as gpui::Action>::build, + json_schema: <#type_name as gpui::Action>::action_json_schema, + deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(), + deprecation_message: <#type_name as gpui::Action>::deprecation_message(), + documentation: <#type_name as gpui::Action>::documentation(), + } + } - gpui::private::inventory::submit! { - gpui::MacroActionBuilder(#action_builder_fn_name) - } - } - } - } + gpui::private::inventory::submit! { + gpui::MacroActionBuilder(#action_builder_fn_name) + } + } + } + } } diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index 6a0095a6c7..eeed1193d8 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/crates/gpui_macros/src/styles.rs @@ -2,1476 +2,1476 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ - Token, Visibility, braced, - parse::{Parse, ParseStream, Result}, - parse_macro_input, + Token, Visibility, braced, + parse::{Parse, ParseStream, Result}, + parse_macro_input, }; #[derive(Debug)] struct StyleableMacroInput { - method_visibility: Visibility, + method_visibility: Visibility, } impl Parse for StyleableMacroInput { - fn parse(input: ParseStream) -> Result { - if !input.peek(syn::token::Brace) { - return Ok(Self { - method_visibility: Visibility::Inherited, - }); - } + fn parse(input: ParseStream) -> Result { + if !input.peek(syn::token::Brace) { + return Ok(Self { + method_visibility: Visibility::Inherited, + }); + } - let content; - braced!(content in input); + let content; + braced!(content in input); - let mut method_visibility = None; + let mut method_visibility = None; - let ident: syn::Ident = content.parse()?; - if ident == "visibility" { - let _colon: Token![:] = content.parse()?; - method_visibility = Some(content.parse()?); - } + let ident: syn::Ident = content.parse()?; + if ident == "visibility" { + let _colon: Token![:] = content.parse()?; + method_visibility = Some(content.parse()?); + } - Ok(Self { - method_visibility: method_visibility.unwrap_or(Visibility::Inherited), - }) - } + Ok(Self { + method_visibility: method_visibility.unwrap_or(Visibility::Inherited), + }) + } } pub fn style_helpers(input: TokenStream) -> TokenStream { - let _ = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_methods(); - let output = quote! { - #(#methods)* - }; + let _ = parse_macro_input!(input as StyleableMacroInput); + let methods = generate_methods(); + let output = quote! { + #(#methods)* + }; - output.into() + output.into() } pub fn visibility_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the visibility of the element to `visible`. - /// [Docs](https://tailwindcss.com/docs/visibility) - #visibility fn visible(mut self) -> Self { - self.style().visibility = Some(gpui::Visibility::Visible); - self - } + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; + let output = quote! { + /// Sets the visibility of the element to `visible`. + /// [Docs](https://tailwindcss.com/docs/visibility) + #visibility fn visible(mut self) -> Self { + self.style().visibility = Some(gpui::Visibility::Visible); + self + } - /// Sets the visibility of the element to `hidden`. - /// [Docs](https://tailwindcss.com/docs/visibility) - #visibility fn invisible(mut self) -> Self { - self.style().visibility = Some(gpui::Visibility::Hidden); - self - } - }; + /// Sets the visibility of the element to `hidden`. + /// [Docs](https://tailwindcss.com/docs/visibility) + #visibility fn invisible(mut self) -> Self { + self.style().visibility = Some(gpui::Visibility::Hidden); + self + } + }; - output.into() + output.into() } pub fn margin_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_box_style_methods( - margin_box_style_prefixes(), - box_style_suffixes(), - input.method_visibility, - ); - let output = quote! { - #(#methods)* - }; + let input = parse_macro_input!(input as StyleableMacroInput); + let methods = generate_box_style_methods( + margin_box_style_prefixes(), + box_style_suffixes(), + input.method_visibility, + ); + let output = quote! { + #(#methods)* + }; - output.into() + output.into() } pub fn padding_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_box_style_methods( - padding_box_style_prefixes(), - box_style_suffixes(), - input.method_visibility, - ); - let output = quote! { - #(#methods)* - }; + let input = parse_macro_input!(input as StyleableMacroInput); + let methods = generate_box_style_methods( + padding_box_style_prefixes(), + box_style_suffixes(), + input.method_visibility, + ); + let output = quote! { + #(#methods)* + }; - output.into() + output.into() } pub fn position_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let methods = generate_box_style_methods( - position_box_style_prefixes(), - box_style_suffixes(), - visibility.clone(), - ); - let output = quote! { - /// Sets the position of the element to `relative`. - /// [Docs](https://tailwindcss.com/docs/position) - #visibility fn relative(mut self) -> Self { - self.style().position = Some(gpui::Position::Relative); - self - } + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; + let methods = generate_box_style_methods( + position_box_style_prefixes(), + box_style_suffixes(), + visibility.clone(), + ); + let output = quote! { + /// Sets the position of the element to `relative`. + /// [Docs](https://tailwindcss.com/docs/position) + #visibility fn relative(mut self) -> Self { + self.style().position = Some(gpui::Position::Relative); + self + } - /// Sets the position of the element to `absolute`. - /// [Docs](https://tailwindcss.com/docs/position) - #visibility fn absolute(mut self) -> Self { - self.style().position = Some(gpui::Position::Absolute); - self - } + /// Sets the position of the element to `absolute`. + /// [Docs](https://tailwindcss.com/docs/position) + #visibility fn absolute(mut self) -> Self { + self.style().position = Some(gpui::Position::Absolute); + self + } - #(#methods)* - }; + #(#methods)* + }; - output.into() + output.into() } pub fn overflow_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the behavior of content that overflows the container to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_hidden(mut self) -> Self { - self.style().overflow.x = Some(gpui::Overflow::Hidden); - self.style().overflow.y = Some(gpui::Overflow::Hidden); - self - } + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; + let output = quote! { + /// Sets the behavior of content that overflows the container to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + #visibility fn overflow_hidden(mut self) -> Self { + self.style().overflow.x = Some(gpui::Overflow::Hidden); + self.style().overflow.y = Some(gpui::Overflow::Hidden); + self + } - /// Sets the behavior of content that overflows the container on the X axis to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_x_hidden(mut self) -> Self { - self.style().overflow.x = Some(gpui::Overflow::Hidden); - self - } + /// Sets the behavior of content that overflows the container on the X axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + #visibility fn overflow_x_hidden(mut self) -> Self { + self.style().overflow.x = Some(gpui::Overflow::Hidden); + self + } - /// Sets the behavior of content that overflows the container on the Y axis to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_y_hidden(mut self) -> Self { - self.style().overflow.y = Some(gpui::Overflow::Hidden); - self - } - }; + /// Sets the behavior of content that overflows the container on the Y axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + #visibility fn overflow_y_hidden(mut self) -> Self { + self.style().overflow.y = Some(gpui::Overflow::Hidden); + self + } + }; - output.into() + output.into() } pub fn cursor_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Set the cursor style when hovering over this element - #visibility fn cursor(mut self, cursor: CursorStyle) -> Self { - self.style().mouse_cursor = Some(cursor); - self - } + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; + let output = quote! { + /// Set the cursor style when hovering over this element + #visibility fn cursor(mut self, cursor: CursorStyle) -> Self { + self.style().mouse_cursor = Some(cursor); + self + } - /// Sets the cursor style when hovering an element to `default`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_default(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::Arrow); - self - } + /// Sets the cursor style when hovering an element to `default`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_default(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::Arrow); + self + } - /// Sets the cursor style when hovering an element to `pointer`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_pointer(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::PointingHand); - self - } + /// Sets the cursor style when hovering an element to `pointer`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_pointer(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::PointingHand); + self + } - /// Sets cursor style when hovering over an element to `text`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_text(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::IBeam); - self - } + /// Sets cursor style when hovering over an element to `text`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_text(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::IBeam); + self + } - /// Sets cursor style when hovering over an element to `move`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_move(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); - self - } + /// Sets cursor style when hovering over an element to `move`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_move(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); + self + } - /// Sets cursor style when hovering over an element to `not-allowed`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_not_allowed(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); - self - } + /// Sets cursor style when hovering over an element to `not-allowed`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_not_allowed(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); + self + } - /// Sets cursor style when hovering over an element to `context-menu`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_context_menu(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ContextualMenu); - self - } + /// Sets cursor style when hovering over an element to `context-menu`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_context_menu(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ContextualMenu); + self + } - /// Sets cursor style when hovering over an element to `crosshair`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_crosshair(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::Crosshair); - self - } + /// Sets cursor style when hovering over an element to `crosshair`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_crosshair(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::Crosshair); + self + } - /// Sets cursor style when hovering over an element to `vertical-text`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_vertical_text(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::IBeamCursorForVerticalLayout); - self - } + /// Sets cursor style when hovering over an element to `vertical-text`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_vertical_text(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::IBeamCursorForVerticalLayout); + self + } - /// Sets cursor style when hovering over an element to `alias`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_alias(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::DragLink); - self - } + /// Sets cursor style when hovering over an element to `alias`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_alias(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::DragLink); + self + } - /// Sets cursor style when hovering over an element to `copy`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_copy(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::DragCopy); - self - } + /// Sets cursor style when hovering over an element to `copy`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_copy(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::DragCopy); + self + } - /// Sets cursor style when hovering over an element to `no-drop`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_no_drop(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); - self - } + /// Sets cursor style when hovering over an element to `no-drop`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_no_drop(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); + self + } - /// Sets cursor style when hovering over an element to `grab`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_grab(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OpenHand); - self - } + /// Sets cursor style when hovering over an element to `grab`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_grab(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::OpenHand); + self + } - /// Sets cursor style when hovering over an element to `grabbing`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_grabbing(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); - self - } + /// Sets cursor style when hovering over an element to `grabbing`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_grabbing(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); + self + } - /// Sets cursor style when hovering over an element to `ew-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_ew_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeftRight); - self - } + /// Sets cursor style when hovering over an element to `ew-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_ew_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeftRight); + self + } - /// Sets cursor style when hovering over an element to `ns-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_ns_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpDown); - self - } + /// Sets cursor style when hovering over an element to `ns-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_ns_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpDown); + self + } - /// Sets cursor style when hovering over an element to `nesw-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_nesw_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpRightDownLeft); - self - } + /// Sets cursor style when hovering over an element to `nesw-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_nesw_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpRightDownLeft); + self + } - /// Sets cursor style when hovering over an element to `nwse-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_nwse_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpLeftDownRight); - self - } + /// Sets cursor style when hovering over an element to `nwse-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_nwse_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpLeftDownRight); + self + } - /// Sets cursor style when hovering over an element to `col-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_col_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeColumn); - self - } + /// Sets cursor style when hovering over an element to `col-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_col_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeColumn); + self + } - /// Sets cursor style when hovering over an element to `row-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_row_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRow); - self - } + /// Sets cursor style when hovering over an element to `row-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_row_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRow); + self + } - /// Sets cursor style when hovering over an element to `n-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_n_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUp); - self - } + /// Sets cursor style when hovering over an element to `n-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_n_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUp); + self + } - /// Sets cursor style when hovering over an element to `e-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_e_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRight); - self - } + /// Sets cursor style when hovering over an element to `e-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_e_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRight); + self + } - /// Sets cursor style when hovering over an element to `s-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_s_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeDown); - self - } + /// Sets cursor style when hovering over an element to `s-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_s_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeDown); + self + } - /// Sets cursor style when hovering over an element to `w-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_w_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft); - self - } + /// Sets cursor style when hovering over an element to `w-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_w_resize(mut self) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft); + self + } - }; + }; - output.into() + output.into() } pub fn border_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; - let mut methods = Vec::new(); + let mut methods = Vec::new(); - for border_style_prefix in border_prefixes() { - methods.push(generate_custom_value_setter( - visibility.clone(), - border_style_prefix.prefix, - quote! { AbsoluteLength }, - &border_style_prefix.fields, - border_style_prefix.doc_string_prefix, - )); + for border_style_prefix in border_prefixes() { + methods.push(generate_custom_value_setter( + visibility.clone(), + border_style_prefix.prefix, + quote! { AbsoluteLength }, + &border_style_prefix.fields, + border_style_prefix.doc_string_prefix, + )); - for border_style_suffix in border_suffixes() { - methods.push(generate_predefined_setter( - visibility.clone(), - border_style_prefix.prefix, - border_style_suffix.suffix, - &border_style_prefix.fields, - &border_style_suffix.width_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = border_style_prefix.doc_string_prefix, - suffix = border_style_suffix.doc_string_suffix, - ), - )); - } - } + for border_style_suffix in border_suffixes() { + methods.push(generate_predefined_setter( + visibility.clone(), + border_style_prefix.prefix, + border_style_suffix.suffix, + &border_style_prefix.fields, + &border_style_suffix.width_tokens, + false, + &format!( + "{prefix}\n\n{suffix}", + prefix = border_style_prefix.doc_string_prefix, + suffix = border_style_suffix.doc_string_suffix, + ), + )); + } + } - let output = quote! { - /// Sets the border color of the element. - #visibility fn border_color(mut self, border_color: C) -> Self - where - C: Into, - Self: Sized, - { - self.style().border_color = Some(border_color.into()); - self - } + let output = quote! { + /// Sets the border color of the element. + #visibility fn border_color(mut self, border_color: C) -> Self + where + C: Into, + Self: Sized, + { + self.style().border_color = Some(border_color.into()); + self + } - #(#methods)* - }; + #(#methods)* + }; - output.into() + output.into() } pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow(mut self, shadows: std::vec::Vec) -> Self { - self.style().box_shadow = Some(shadows); - self - } + let input = parse_macro_input!(input as StyleableMacroInput); + let visibility = input.method_visibility; + let output = quote! { + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow(mut self, shadows: std::vec::Vec) -> Self { + self.style().box_shadow = Some(shadows); + self + } - /// Clears the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_none(mut self) -> Self { - self.style().box_shadow = Some(Default::default()); - self - } + /// Clears the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_none(mut self) -> Self { + self.style().box_shadow = Some(Default::default()); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_2xs(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_2xs(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.05), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - inset: false, - }]); - self - } + self.style().box_shadow = Some(vec![BoxShadow { + color: hsla(0., 0., 0., 0.05), + offset: point(px(0.), px(1.)), + blur_radius: px(0.), + spread_radius: px(0.), + inset: false, + }]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_xs(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_xs(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.05), - offset: point(px(0.), px(1.)), - blur_radius: px(2.), - spread_radius: px(0.), - inset: false, - }]); - self - } + self.style().box_shadow = Some(vec![BoxShadow { + color: hsla(0., 0., 0., 0.05), + offset: point(px(0.), px(1.)), + blur_radius: px(2.), + spread_radius: px(0.), + inset: false, + }]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_sm(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_sm(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(3.), - spread_radius: px(0.), - inset: false, - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(2.), - spread_radius: px(-1.), - inset: false, - } - ]); - self - } + self.style().box_shadow = Some(vec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(1.)), + blur_radius: px(3.), + spread_radius: px(0.), + inset: false, + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(1.)), + blur_radius: px(2.), + spread_radius: px(-1.), + inset: false, + } + ]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_md(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_md(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(4.)), - blur_radius: px(6.), - spread_radius: px(-1.), - inset: false, - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(2.)), - blur_radius: px(4.), - spread_radius: px(-2.), - inset: false, - } - ]); - self - } + self.style().box_shadow = Some(vec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(4.)), + blur_radius: px(6.), + spread_radius: px(-1.), + inset: false, + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(2.)), + blur_radius: px(4.), + spread_radius: px(-2.), + inset: false, + } + ]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_lg(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_lg(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(10.)), - blur_radius: px(15.), - spread_radius: px(-3.), - inset: false, - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(4.)), - blur_radius: px(6.), - spread_radius: px(-4.), - inset: false, - } - ]); - self - } + self.style().box_shadow = Some(vec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(10.)), + blur_radius: px(15.), + spread_radius: px(-3.), + inset: false, + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(4.)), + blur_radius: px(6.), + spread_radius: px(-4.), + inset: false, + } + ]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_xl(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_xl(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(20.)), - blur_radius: px(25.), - spread_radius: px(-5.), - inset: false, - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(8.)), - blur_radius: px(10.), - spread_radius: px(-6.), - inset: false, - } - ]); - self - } + self.style().box_shadow = Some(vec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(20.)), + blur_radius: px(25.), + spread_radius: px(-5.), + inset: false, + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(8.)), + blur_radius: px(10.), + spread_radius: px(-6.), + inset: false, + } + ]); + self + } - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_2xl(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_2xl(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.25), - offset: point(px(0.), px(25.)), - blur_radius: px(50.), - spread_radius: px(-12.), - inset: false, - }]); - self - } - }; + self.style().box_shadow = Some(vec![BoxShadow { + color: hsla(0., 0., 0., 0.25), + offset: point(px(0.), px(25.)), + blur_radius: px(50.), + spread_radius: px(-12.), + inset: false, + }]); + self + } + }; - output.into() + output.into() } struct BoxStylePrefix { - prefix: &'static str, - auto_allowed: bool, - fields: Vec, - doc_string_prefix: &'static str, + prefix: &'static str, + auto_allowed: bool, + fields: Vec, + doc_string_prefix: &'static str, } struct BoxStyleSuffix { - suffix: &'static str, - length_tokens: TokenStream2, - doc_string_suffix: &'static str, + suffix: &'static str, + length_tokens: TokenStream2, + doc_string_suffix: &'static str, } struct CornerStylePrefix { - prefix: &'static str, - fields: Vec, - doc_string_prefix: &'static str, + prefix: &'static str, + fields: Vec, + doc_string_prefix: &'static str, } struct CornerStyleSuffix { - suffix: &'static str, - radius_tokens: TokenStream2, - doc_string_suffix: &'static str, + suffix: &'static str, + radius_tokens: TokenStream2, + doc_string_suffix: &'static str, } struct BorderStylePrefix { - prefix: &'static str, - fields: Vec, - doc_string_prefix: &'static str, + prefix: &'static str, + fields: Vec, + doc_string_prefix: &'static str, } struct BorderStyleSuffix { - suffix: &'static str, - width_tokens: TokenStream2, - doc_string_suffix: &'static str, + suffix: &'static str, + width_tokens: TokenStream2, + doc_string_suffix: &'static str, } fn generate_box_style_methods( - prefixes: Vec, - suffixes: Vec, - visibility: Visibility, + prefixes: Vec, + suffixes: Vec, + visibility: Visibility, ) -> Vec { - let mut methods = Vec::new(); + let mut methods = Vec::new(); - for box_style_prefix in prefixes { - methods.push(generate_custom_value_setter( - visibility.clone(), - box_style_prefix.prefix, - if box_style_prefix.auto_allowed { - quote! { Length } - } else { - quote! { DefiniteLength } - }, - &box_style_prefix.fields, - box_style_prefix.doc_string_prefix, - )); + for box_style_prefix in prefixes { + methods.push(generate_custom_value_setter( + visibility.clone(), + box_style_prefix.prefix, + if box_style_prefix.auto_allowed { + quote! { Length } + } else { + quote! { DefiniteLength } + }, + &box_style_prefix.fields, + box_style_prefix.doc_string_prefix, + )); - for box_style_suffix in &suffixes { - if box_style_suffix.suffix != "auto" || box_style_prefix.auto_allowed { - methods.push(generate_predefined_setter( - visibility.clone(), - box_style_prefix.prefix, - box_style_suffix.suffix, - &box_style_prefix.fields, - &box_style_suffix.length_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = box_style_prefix.doc_string_prefix, - suffix = box_style_suffix.doc_string_suffix, - ), - )); - } + for box_style_suffix in &suffixes { + if box_style_suffix.suffix != "auto" || box_style_prefix.auto_allowed { + methods.push(generate_predefined_setter( + visibility.clone(), + box_style_prefix.prefix, + box_style_suffix.suffix, + &box_style_prefix.fields, + &box_style_suffix.length_tokens, + false, + &format!( + "{prefix}\n\n{suffix}", + prefix = box_style_prefix.doc_string_prefix, + suffix = box_style_suffix.doc_string_suffix, + ), + )); + } - if box_style_suffix.suffix != "auto" { - methods.push(generate_predefined_setter( - visibility.clone(), - box_style_prefix.prefix, - box_style_suffix.suffix, - &box_style_prefix.fields, - &box_style_suffix.length_tokens, - true, - &format!( - "{prefix}\n\n{suffix}", - prefix = box_style_prefix.doc_string_prefix, - suffix = box_style_suffix.doc_string_suffix, - ), - )); - } - } - } + if box_style_suffix.suffix != "auto" { + methods.push(generate_predefined_setter( + visibility.clone(), + box_style_prefix.prefix, + box_style_suffix.suffix, + &box_style_prefix.fields, + &box_style_suffix.length_tokens, + true, + &format!( + "{prefix}\n\n{suffix}", + prefix = box_style_prefix.doc_string_prefix, + suffix = box_style_suffix.doc_string_suffix, + ), + )); + } + } + } - methods + methods } fn generate_methods() -> Vec { - let visibility = Visibility::Inherited; - let mut methods = - generate_box_style_methods(box_prefixes(), box_style_suffixes(), visibility.clone()); + let visibility = Visibility::Inherited; + let mut methods = + generate_box_style_methods(box_prefixes(), box_style_suffixes(), visibility.clone()); - for corner_style_prefix in corner_prefixes() { - methods.push(generate_custom_value_setter( - visibility.clone(), - corner_style_prefix.prefix, - quote! { AbsoluteLength }, - &corner_style_prefix.fields, - corner_style_prefix.doc_string_prefix, - )); + for corner_style_prefix in corner_prefixes() { + methods.push(generate_custom_value_setter( + visibility.clone(), + corner_style_prefix.prefix, + quote! { AbsoluteLength }, + &corner_style_prefix.fields, + corner_style_prefix.doc_string_prefix, + )); - for corner_style_suffix in corner_suffixes() { - methods.push(generate_predefined_setter( - visibility.clone(), - corner_style_prefix.prefix, - corner_style_suffix.suffix, - &corner_style_prefix.fields, - &corner_style_suffix.radius_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = corner_style_prefix.doc_string_prefix, - suffix = corner_style_suffix.doc_string_suffix, - ), - )); - } - } + for corner_style_suffix in corner_suffixes() { + methods.push(generate_predefined_setter( + visibility.clone(), + corner_style_prefix.prefix, + corner_style_suffix.suffix, + &corner_style_prefix.fields, + &corner_style_suffix.radius_tokens, + false, + &format!( + "{prefix}\n\n{suffix}", + prefix = corner_style_prefix.doc_string_prefix, + suffix = corner_style_suffix.doc_string_suffix, + ), + )); + } + } - methods + methods } fn generate_predefined_setter( - visibility: Visibility, - name: &'static str, - length: &'static str, - fields: &[TokenStream2], - length_tokens: &TokenStream2, - negate: bool, - doc_string: &str, + visibility: Visibility, + name: &'static str, + length: &'static str, + fields: &[TokenStream2], + length_tokens: &TokenStream2, + negate: bool, + doc_string: &str, ) -> TokenStream2 { - let (negation_qualifier, negation_token) = if negate { - ("_neg", quote! { - }) - } else { - ("", quote! {}) - }; + let (negation_qualifier, negation_token) = if negate { + ("_neg", quote! { - }) + } else { + ("", quote! {}) + }; - let method_name = if length.is_empty() { - format_ident!("{name}{negation_qualifier}") - } else { - format_ident!("{name}{negation_qualifier}_{length}") - }; + let method_name = if length.is_empty() { + format_ident!("{name}{negation_qualifier}") + } else { + format_ident!("{name}{negation_qualifier}_{length}") + }; - let field_assignments = fields - .iter() - .map(|field_tokens| { - quote! { - style.#field_tokens = Some((#negation_token gpui::#length_tokens).into()); - } - }) - .collect::>(); + let field_assignments = fields + .iter() + .map(|field_tokens| { + quote! { + style.#field_tokens = Some((#negation_token gpui::#length_tokens).into()); + } + }) + .collect::>(); - let method = quote! { - #[doc = #doc_string] - #visibility fn #method_name(mut self) -> Self { - let style = self.style(); - #(#field_assignments)* - self - } - }; + let method = quote! { + #[doc = #doc_string] + #visibility fn #method_name(mut self) -> Self { + let style = self.style(); + #(#field_assignments)* + self + } + }; - method + method } fn generate_custom_value_setter( - visibility: Visibility, - prefix: &str, - length_type: TokenStream2, - fields: &[TokenStream2], - doc_string: &str, + visibility: Visibility, + prefix: &str, + length_type: TokenStream2, + fields: &[TokenStream2], + doc_string: &str, ) -> TokenStream2 { - let method_name = format_ident!("{}", prefix); + let method_name = format_ident!("{}", prefix); - let mut iter = fields.iter(); - let last = iter.next_back().unwrap(); - let field_assignments = iter - .map(|field_tokens| { - quote! { - style.#field_tokens = Some(length.clone().into()); - } - }) - .chain(std::iter::once(quote! { - style.#last = Some(length.into()); - })) - .collect::>(); + let mut iter = fields.iter(); + let last = iter.next_back().unwrap(); + let field_assignments = iter + .map(|field_tokens| { + quote! { + style.#field_tokens = Some(length.clone().into()); + } + }) + .chain(std::iter::once(quote! { + style.#last = Some(length.into()); + })) + .collect::>(); - let method = quote! { - #[doc = #doc_string] - #visibility fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { - let style = self.style(); - #(#field_assignments)* - self - } - }; + let method = quote! { + #[doc = #doc_string] + #visibility fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { + let style = self.style(); + #(#field_assignments)* + self + } + }; - method + method } fn margin_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "m", - auto_allowed: true, - fields: vec![ - quote! { margin.top }, - quote! { margin.bottom }, - quote! { margin.left }, - quote! { margin.right }, - ], - doc_string_prefix: "Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)", - }, - BoxStylePrefix { - prefix: "mt", - auto_allowed: true, - fields: vec![quote! { margin.top }], - doc_string_prefix: "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "mb", - auto_allowed: true, - fields: vec![quote! { margin.bottom }], - doc_string_prefix: "Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "my", - auto_allowed: true, - fields: vec![quote! { margin.top }, quote! { margin.bottom }], - doc_string_prefix: "Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)", - }, - BoxStylePrefix { - prefix: "mx", - auto_allowed: true, - fields: vec![quote! { margin.left }, quote! { margin.right }], - doc_string_prefix: "Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)", - }, - BoxStylePrefix { - prefix: "ml", - auto_allowed: true, - fields: vec![quote! { margin.left }], - doc_string_prefix: "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "mr", - auto_allowed: true, - fields: vec![quote! { margin.right }], - doc_string_prefix: "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - ] + vec![ + BoxStylePrefix { + prefix: "m", + auto_allowed: true, + fields: vec![ + quote! { margin.top }, + quote! { margin.bottom }, + quote! { margin.left }, + quote! { margin.right }, + ], + doc_string_prefix: "Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)", + }, + BoxStylePrefix { + prefix: "mt", + auto_allowed: true, + fields: vec![quote! { margin.top }], + doc_string_prefix: "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "mb", + auto_allowed: true, + fields: vec![quote! { margin.bottom }], + doc_string_prefix: "Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "my", + auto_allowed: true, + fields: vec![quote! { margin.top }, quote! { margin.bottom }], + doc_string_prefix: "Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)", + }, + BoxStylePrefix { + prefix: "mx", + auto_allowed: true, + fields: vec![quote! { margin.left }, quote! { margin.right }], + doc_string_prefix: "Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)", + }, + BoxStylePrefix { + prefix: "ml", + auto_allowed: true, + fields: vec![quote! { margin.left }], + doc_string_prefix: "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "mr", + auto_allowed: true, + fields: vec![quote! { margin.right }], + doc_string_prefix: "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", + }, + ] } fn padding_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "p", - auto_allowed: false, - fields: vec![ - quote! { padding.top }, - quote! { padding.bottom }, - quote! { padding.left }, - quote! { padding.right }, - ], - doc_string_prefix: "Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)", - }, - BoxStylePrefix { - prefix: "pt", - auto_allowed: false, - fields: vec![quote! { padding.top }], - doc_string_prefix: "Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "pb", - auto_allowed: false, - fields: vec![quote! { padding.bottom }], - doc_string_prefix: "Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "px", - auto_allowed: false, - fields: vec![quote! { padding.left }, quote! { padding.right }], - doc_string_prefix: "Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)", - }, - BoxStylePrefix { - prefix: "py", - auto_allowed: false, - fields: vec![quote! { padding.top }, quote! { padding.bottom }], - doc_string_prefix: "Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)", - }, - BoxStylePrefix { - prefix: "pl", - auto_allowed: false, - fields: vec![quote! { padding.left }], - doc_string_prefix: "Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "pr", - auto_allowed: false, - fields: vec![quote! { padding.right }], - doc_string_prefix: "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - ] + vec![ + BoxStylePrefix { + prefix: "p", + auto_allowed: false, + fields: vec![ + quote! { padding.top }, + quote! { padding.bottom }, + quote! { padding.left }, + quote! { padding.right }, + ], + doc_string_prefix: "Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)", + }, + BoxStylePrefix { + prefix: "pt", + auto_allowed: false, + fields: vec![quote! { padding.top }], + doc_string_prefix: "Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "pb", + auto_allowed: false, + fields: vec![quote! { padding.bottom }], + doc_string_prefix: "Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "px", + auto_allowed: false, + fields: vec![quote! { padding.left }, quote! { padding.right }], + doc_string_prefix: "Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)", + }, + BoxStylePrefix { + prefix: "py", + auto_allowed: false, + fields: vec![quote! { padding.top }, quote! { padding.bottom }], + doc_string_prefix: "Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)", + }, + BoxStylePrefix { + prefix: "pl", + auto_allowed: false, + fields: vec![quote! { padding.left }], + doc_string_prefix: "Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", + }, + BoxStylePrefix { + prefix: "pr", + auto_allowed: false, + fields: vec![quote! { padding.right }], + doc_string_prefix: "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", + }, + ] } fn position_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "inset", - auto_allowed: true, - fields: vec![ - quote! { inset.top }, - quote! { inset.right }, - quote! { inset.bottom }, - quote! { inset.left }, - ], - doc_string_prefix: "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "top", - auto_allowed: true, - fields: vec![quote! { inset.top }], - doc_string_prefix: "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "bottom", - auto_allowed: true, - fields: vec![quote! { inset.bottom }], - doc_string_prefix: "Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "left", - auto_allowed: true, - fields: vec![quote! { inset.left }], - doc_string_prefix: "Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "right", - auto_allowed: true, - fields: vec![quote! { inset.right }], - doc_string_prefix: "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - ] + vec![ + BoxStylePrefix { + prefix: "inset", + auto_allowed: true, + fields: vec![ + quote! { inset.top }, + quote! { inset.right }, + quote! { inset.bottom }, + quote! { inset.left }, + ], + doc_string_prefix: "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + }, + BoxStylePrefix { + prefix: "top", + auto_allowed: true, + fields: vec![quote! { inset.top }], + doc_string_prefix: "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + }, + BoxStylePrefix { + prefix: "bottom", + auto_allowed: true, + fields: vec![quote! { inset.bottom }], + doc_string_prefix: "Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + }, + BoxStylePrefix { + prefix: "left", + auto_allowed: true, + fields: vec![quote! { inset.left }], + doc_string_prefix: "Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + }, + BoxStylePrefix { + prefix: "right", + auto_allowed: true, + fields: vec![quote! { inset.right }], + doc_string_prefix: "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + }, + ] } fn box_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "w", - auto_allowed: true, - fields: vec![quote! { size.width }], - doc_string_prefix: "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)", - }, - BoxStylePrefix { - prefix: "h", - auto_allowed: true, - fields: vec![quote! { size.height }], - doc_string_prefix: "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)", - }, - BoxStylePrefix { - prefix: "size", - auto_allowed: true, - fields: vec![quote! {size.width}, quote! {size.height}], - doc_string_prefix: "Sets the width and height of the element.", - }, - BoxStylePrefix { - prefix: "min_size", - auto_allowed: true, - fields: vec![quote! {min_size.width}, quote! {min_size.height}], - doc_string_prefix: "Sets the minimum width and height of the element.", - }, - BoxStylePrefix { - prefix: "min_w", - auto_allowed: true, - fields: vec![quote! { min_size.width }], - doc_string_prefix: "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "min_h", - auto_allowed: true, - fields: vec![quote! { min_size.height }], - doc_string_prefix: "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)", - }, - BoxStylePrefix { - prefix: "max_size", - auto_allowed: true, - fields: vec![quote! {max_size.width}, quote! {max_size.height}], - doc_string_prefix: "Sets the maximum width and height of the element.", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "max_w", - auto_allowed: true, - fields: vec![quote! { max_size.width }], - doc_string_prefix: "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "max_h", - auto_allowed: true, - fields: vec![quote! { max_size.height }], - doc_string_prefix: "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)", - }, - BoxStylePrefix { - prefix: "gap", - auto_allowed: false, - fields: vec![quote! { gap.width }, quote! { gap.height }], - doc_string_prefix: "Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)", - }, - BoxStylePrefix { - prefix: "gap_x", - auto_allowed: false, - fields: vec![quote! { gap.width }], - doc_string_prefix: "Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", - }, - BoxStylePrefix { - prefix: "gap_y", - auto_allowed: false, - fields: vec![quote! { gap.height }], - doc_string_prefix: "Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", - }, - ] + vec![ + BoxStylePrefix { + prefix: "w", + auto_allowed: true, + fields: vec![quote! { size.width }], + doc_string_prefix: "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)", + }, + BoxStylePrefix { + prefix: "h", + auto_allowed: true, + fields: vec![quote! { size.height }], + doc_string_prefix: "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)", + }, + BoxStylePrefix { + prefix: "size", + auto_allowed: true, + fields: vec![quote! {size.width}, quote! {size.height}], + doc_string_prefix: "Sets the width and height of the element.", + }, + BoxStylePrefix { + prefix: "min_size", + auto_allowed: true, + fields: vec![quote! {min_size.width}, quote! {min_size.height}], + doc_string_prefix: "Sets the minimum width and height of the element.", + }, + BoxStylePrefix { + prefix: "min_w", + auto_allowed: true, + fields: vec![quote! { min_size.width }], + doc_string_prefix: "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)", + }, + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + BoxStylePrefix { + prefix: "min_h", + auto_allowed: true, + fields: vec![quote! { min_size.height }], + doc_string_prefix: "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)", + }, + BoxStylePrefix { + prefix: "max_size", + auto_allowed: true, + fields: vec![quote! {max_size.width}, quote! {max_size.height}], + doc_string_prefix: "Sets the maximum width and height of the element.", + }, + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + BoxStylePrefix { + prefix: "max_w", + auto_allowed: true, + fields: vec![quote! { max_size.width }], + doc_string_prefix: "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)", + }, + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + BoxStylePrefix { + prefix: "max_h", + auto_allowed: true, + fields: vec![quote! { max_size.height }], + doc_string_prefix: "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)", + }, + BoxStylePrefix { + prefix: "gap", + auto_allowed: false, + fields: vec![quote! { gap.width }, quote! { gap.height }], + doc_string_prefix: "Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)", + }, + BoxStylePrefix { + prefix: "gap_x", + auto_allowed: false, + fields: vec![quote! { gap.width }], + doc_string_prefix: "Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", + }, + BoxStylePrefix { + prefix: "gap_y", + auto_allowed: false, + fields: vec![quote! { gap.height }], + doc_string_prefix: "Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", + }, + ] } fn box_style_suffixes() -> Vec { - vec![ - BoxStyleSuffix { - suffix: "0", - length_tokens: quote! { px(0.) }, - doc_string_suffix: "0px", - }, - BoxStyleSuffix { - suffix: "0p5", - length_tokens: quote! { rems(0.125) }, - doc_string_suffix: "2px (0.125rem)", - }, - BoxStyleSuffix { - suffix: "1", - length_tokens: quote! { rems(0.25) }, - doc_string_suffix: "4px (0.25rem)", - }, - BoxStyleSuffix { - suffix: "1p5", - length_tokens: quote! { rems(0.375) }, - doc_string_suffix: "6px (0.375rem)", - }, - BoxStyleSuffix { - suffix: "2", - length_tokens: quote! { rems(0.5) }, - doc_string_suffix: "8px (0.5rem)", - }, - BoxStyleSuffix { - suffix: "2p5", - length_tokens: quote! { rems(0.625) }, - doc_string_suffix: "10px (0.625rem)", - }, - BoxStyleSuffix { - suffix: "3", - length_tokens: quote! { rems(0.75) }, - doc_string_suffix: "12px (0.75rem)", - }, - BoxStyleSuffix { - suffix: "3p5", - length_tokens: quote! { rems(0.875) }, - doc_string_suffix: "14px (0.875rem)", - }, - BoxStyleSuffix { - suffix: "4", - length_tokens: quote! { rems(1.) }, - doc_string_suffix: "16px (1rem)", - }, - BoxStyleSuffix { - suffix: "5", - length_tokens: quote! { rems(1.25) }, - doc_string_suffix: "20px (1.25rem)", - }, - BoxStyleSuffix { - suffix: "6", - length_tokens: quote! { rems(1.5) }, - doc_string_suffix: "24px (1.5rem)", - }, - BoxStyleSuffix { - suffix: "7", - length_tokens: quote! { rems(1.75) }, - doc_string_suffix: "28px (1.75rem)", - }, - BoxStyleSuffix { - suffix: "8", - length_tokens: quote! { rems(2.0) }, - doc_string_suffix: "32px (2rem)", - }, - BoxStyleSuffix { - suffix: "9", - length_tokens: quote! { rems(2.25) }, - doc_string_suffix: "36px (2.25rem)", - }, - BoxStyleSuffix { - suffix: "10", - length_tokens: quote! { rems(2.5) }, - doc_string_suffix: "40px (2.5rem)", - }, - BoxStyleSuffix { - suffix: "11", - length_tokens: quote! { rems(2.75) }, - doc_string_suffix: "44px (2.75rem)", - }, - BoxStyleSuffix { - suffix: "12", - length_tokens: quote! { rems(3.) }, - doc_string_suffix: "48px (3rem)", - }, - BoxStyleSuffix { - suffix: "16", - length_tokens: quote! { rems(4.) }, - doc_string_suffix: "64px (4rem)", - }, - BoxStyleSuffix { - suffix: "20", - length_tokens: quote! { rems(5.) }, - doc_string_suffix: "80px (5rem)", - }, - BoxStyleSuffix { - suffix: "24", - length_tokens: quote! { rems(6.) }, - doc_string_suffix: "96px (6rem)", - }, - BoxStyleSuffix { - suffix: "32", - length_tokens: quote! { rems(8.) }, - doc_string_suffix: "128px (8rem)", - }, - BoxStyleSuffix { - suffix: "40", - length_tokens: quote! { rems(10.) }, - doc_string_suffix: "160px (10rem)", - }, - BoxStyleSuffix { - suffix: "48", - length_tokens: quote! { rems(12.) }, - doc_string_suffix: "192px (12rem)", - }, - BoxStyleSuffix { - suffix: "56", - length_tokens: quote! { rems(14.) }, - doc_string_suffix: "224px (14rem)", - }, - BoxStyleSuffix { - suffix: "64", - length_tokens: quote! { rems(16.) }, - doc_string_suffix: "256px (16rem)", - }, - BoxStyleSuffix { - suffix: "72", - length_tokens: quote! { rems(18.) }, - doc_string_suffix: "288px (18rem)", - }, - BoxStyleSuffix { - suffix: "80", - length_tokens: quote! { rems(20.) }, - doc_string_suffix: "320px (20rem)", - }, - BoxStyleSuffix { - suffix: "96", - length_tokens: quote! { rems(24.) }, - doc_string_suffix: "384px (24rem)", - }, - BoxStyleSuffix { - suffix: "112", - length_tokens: quote! { rems(28.) }, - doc_string_suffix: "448px (28rem)", - }, - BoxStyleSuffix { - suffix: "128", - length_tokens: quote! { rems(32.) }, - doc_string_suffix: "512px (32rem)", - }, - BoxStyleSuffix { - suffix: "auto", - length_tokens: quote! { auto() }, - doc_string_suffix: "Auto", - }, - BoxStyleSuffix { - suffix: "px", - length_tokens: quote! { px(1.) }, - doc_string_suffix: "1px", - }, - BoxStyleSuffix { - suffix: "full", - length_tokens: quote! { relative(1.) }, - doc_string_suffix: "100%", - }, - BoxStyleSuffix { - suffix: "1_2", - length_tokens: quote! { relative(0.5) }, - doc_string_suffix: "50% (1/2)", - }, - BoxStyleSuffix { - suffix: "1_3", - length_tokens: quote! { relative(1./3.) }, - doc_string_suffix: "33% (1/3)", - }, - BoxStyleSuffix { - suffix: "2_3", - length_tokens: quote! { relative(2./3.) }, - doc_string_suffix: "66% (2/3)", - }, - BoxStyleSuffix { - suffix: "1_4", - length_tokens: quote! { relative(0.25) }, - doc_string_suffix: "25% (1/4)", - }, - BoxStyleSuffix { - suffix: "2_4", - length_tokens: quote! { relative(0.5) }, - doc_string_suffix: "50% (2/4)", - }, - BoxStyleSuffix { - suffix: "3_4", - length_tokens: quote! { relative(0.75) }, - doc_string_suffix: "75% (3/4)", - }, - BoxStyleSuffix { - suffix: "1_5", - length_tokens: quote! { relative(0.2) }, - doc_string_suffix: "20% (1/5)", - }, - BoxStyleSuffix { - suffix: "2_5", - length_tokens: quote! { relative(0.4) }, - doc_string_suffix: "40% (2/5)", - }, - BoxStyleSuffix { - suffix: "3_5", - length_tokens: quote! { relative(0.6) }, - doc_string_suffix: "60% (3/5)", - }, - BoxStyleSuffix { - suffix: "4_5", - length_tokens: quote! { relative(0.8) }, - doc_string_suffix: "80% (4/5)", - }, - BoxStyleSuffix { - suffix: "1_6", - length_tokens: quote! { relative(1./6.) }, - doc_string_suffix: "16% (1/6)", - }, - BoxStyleSuffix { - suffix: "5_6", - length_tokens: quote! { relative(5./6.) }, - doc_string_suffix: "80% (5/6)", - }, - BoxStyleSuffix { - suffix: "1_12", - length_tokens: quote! { relative(1./12.) }, - doc_string_suffix: "8% (1/12)", - }, - ] + vec![ + BoxStyleSuffix { + suffix: "0", + length_tokens: quote! { px(0.) }, + doc_string_suffix: "0px", + }, + BoxStyleSuffix { + suffix: "0p5", + length_tokens: quote! { rems(0.125) }, + doc_string_suffix: "2px (0.125rem)", + }, + BoxStyleSuffix { + suffix: "1", + length_tokens: quote! { rems(0.25) }, + doc_string_suffix: "4px (0.25rem)", + }, + BoxStyleSuffix { + suffix: "1p5", + length_tokens: quote! { rems(0.375) }, + doc_string_suffix: "6px (0.375rem)", + }, + BoxStyleSuffix { + suffix: "2", + length_tokens: quote! { rems(0.5) }, + doc_string_suffix: "8px (0.5rem)", + }, + BoxStyleSuffix { + suffix: "2p5", + length_tokens: quote! { rems(0.625) }, + doc_string_suffix: "10px (0.625rem)", + }, + BoxStyleSuffix { + suffix: "3", + length_tokens: quote! { rems(0.75) }, + doc_string_suffix: "12px (0.75rem)", + }, + BoxStyleSuffix { + suffix: "3p5", + length_tokens: quote! { rems(0.875) }, + doc_string_suffix: "14px (0.875rem)", + }, + BoxStyleSuffix { + suffix: "4", + length_tokens: quote! { rems(1.) }, + doc_string_suffix: "16px (1rem)", + }, + BoxStyleSuffix { + suffix: "5", + length_tokens: quote! { rems(1.25) }, + doc_string_suffix: "20px (1.25rem)", + }, + BoxStyleSuffix { + suffix: "6", + length_tokens: quote! { rems(1.5) }, + doc_string_suffix: "24px (1.5rem)", + }, + BoxStyleSuffix { + suffix: "7", + length_tokens: quote! { rems(1.75) }, + doc_string_suffix: "28px (1.75rem)", + }, + BoxStyleSuffix { + suffix: "8", + length_tokens: quote! { rems(2.0) }, + doc_string_suffix: "32px (2rem)", + }, + BoxStyleSuffix { + suffix: "9", + length_tokens: quote! { rems(2.25) }, + doc_string_suffix: "36px (2.25rem)", + }, + BoxStyleSuffix { + suffix: "10", + length_tokens: quote! { rems(2.5) }, + doc_string_suffix: "40px (2.5rem)", + }, + BoxStyleSuffix { + suffix: "11", + length_tokens: quote! { rems(2.75) }, + doc_string_suffix: "44px (2.75rem)", + }, + BoxStyleSuffix { + suffix: "12", + length_tokens: quote! { rems(3.) }, + doc_string_suffix: "48px (3rem)", + }, + BoxStyleSuffix { + suffix: "16", + length_tokens: quote! { rems(4.) }, + doc_string_suffix: "64px (4rem)", + }, + BoxStyleSuffix { + suffix: "20", + length_tokens: quote! { rems(5.) }, + doc_string_suffix: "80px (5rem)", + }, + BoxStyleSuffix { + suffix: "24", + length_tokens: quote! { rems(6.) }, + doc_string_suffix: "96px (6rem)", + }, + BoxStyleSuffix { + suffix: "32", + length_tokens: quote! { rems(8.) }, + doc_string_suffix: "128px (8rem)", + }, + BoxStyleSuffix { + suffix: "40", + length_tokens: quote! { rems(10.) }, + doc_string_suffix: "160px (10rem)", + }, + BoxStyleSuffix { + suffix: "48", + length_tokens: quote! { rems(12.) }, + doc_string_suffix: "192px (12rem)", + }, + BoxStyleSuffix { + suffix: "56", + length_tokens: quote! { rems(14.) }, + doc_string_suffix: "224px (14rem)", + }, + BoxStyleSuffix { + suffix: "64", + length_tokens: quote! { rems(16.) }, + doc_string_suffix: "256px (16rem)", + }, + BoxStyleSuffix { + suffix: "72", + length_tokens: quote! { rems(18.) }, + doc_string_suffix: "288px (18rem)", + }, + BoxStyleSuffix { + suffix: "80", + length_tokens: quote! { rems(20.) }, + doc_string_suffix: "320px (20rem)", + }, + BoxStyleSuffix { + suffix: "96", + length_tokens: quote! { rems(24.) }, + doc_string_suffix: "384px (24rem)", + }, + BoxStyleSuffix { + suffix: "112", + length_tokens: quote! { rems(28.) }, + doc_string_suffix: "448px (28rem)", + }, + BoxStyleSuffix { + suffix: "128", + length_tokens: quote! { rems(32.) }, + doc_string_suffix: "512px (32rem)", + }, + BoxStyleSuffix { + suffix: "auto", + length_tokens: quote! { auto() }, + doc_string_suffix: "Auto", + }, + BoxStyleSuffix { + suffix: "px", + length_tokens: quote! { px(1.) }, + doc_string_suffix: "1px", + }, + BoxStyleSuffix { + suffix: "full", + length_tokens: quote! { relative(1.) }, + doc_string_suffix: "100%", + }, + BoxStyleSuffix { + suffix: "1_2", + length_tokens: quote! { relative(0.5) }, + doc_string_suffix: "50% (1/2)", + }, + BoxStyleSuffix { + suffix: "1_3", + length_tokens: quote! { relative(1./3.) }, + doc_string_suffix: "33% (1/3)", + }, + BoxStyleSuffix { + suffix: "2_3", + length_tokens: quote! { relative(2./3.) }, + doc_string_suffix: "66% (2/3)", + }, + BoxStyleSuffix { + suffix: "1_4", + length_tokens: quote! { relative(0.25) }, + doc_string_suffix: "25% (1/4)", + }, + BoxStyleSuffix { + suffix: "2_4", + length_tokens: quote! { relative(0.5) }, + doc_string_suffix: "50% (2/4)", + }, + BoxStyleSuffix { + suffix: "3_4", + length_tokens: quote! { relative(0.75) }, + doc_string_suffix: "75% (3/4)", + }, + BoxStyleSuffix { + suffix: "1_5", + length_tokens: quote! { relative(0.2) }, + doc_string_suffix: "20% (1/5)", + }, + BoxStyleSuffix { + suffix: "2_5", + length_tokens: quote! { relative(0.4) }, + doc_string_suffix: "40% (2/5)", + }, + BoxStyleSuffix { + suffix: "3_5", + length_tokens: quote! { relative(0.6) }, + doc_string_suffix: "60% (3/5)", + }, + BoxStyleSuffix { + suffix: "4_5", + length_tokens: quote! { relative(0.8) }, + doc_string_suffix: "80% (4/5)", + }, + BoxStyleSuffix { + suffix: "1_6", + length_tokens: quote! { relative(1./6.) }, + doc_string_suffix: "16% (1/6)", + }, + BoxStyleSuffix { + suffix: "5_6", + length_tokens: quote! { relative(5./6.) }, + doc_string_suffix: "80% (5/6)", + }, + BoxStyleSuffix { + suffix: "1_12", + length_tokens: quote! { relative(1./12.) }, + doc_string_suffix: "8% (1/12)", + }, + ] } fn corner_prefixes() -> Vec { - vec![ - CornerStylePrefix { - prefix: "rounded", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.top_right }, - quote! { corner_radii.bottom_right }, - quote! { corner_radii.bottom_left }, - ], - doc_string_prefix: "Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)", - }, - CornerStylePrefix { - prefix: "rounded_t", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.top_right }, - ], - doc_string_prefix: "Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_b", - fields: vec![ - quote! { corner_radii.bottom_left }, - quote! { corner_radii.bottom_right }, - ], - doc_string_prefix: "Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_r", - fields: vec![ - quote! { corner_radii.top_right }, - quote! { corner_radii.bottom_right }, - ], - doc_string_prefix: "Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_l", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.bottom_left }, - ], - doc_string_prefix: "Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_tl", - fields: vec![quote! { corner_radii.top_left }], - doc_string_prefix: "Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_tr", - fields: vec![quote! { corner_radii.top_right }], - doc_string_prefix: "Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_bl", - fields: vec![quote! { corner_radii.bottom_left }], - doc_string_prefix: "Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_br", - fields: vec![quote! { corner_radii.bottom_right }], - doc_string_prefix: "Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - ] + vec![ + CornerStylePrefix { + prefix: "rounded", + fields: vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.top_right }, + quote! { corner_radii.bottom_right }, + quote! { corner_radii.bottom_left }, + ], + doc_string_prefix: "Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)", + }, + CornerStylePrefix { + prefix: "rounded_t", + fields: vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.top_right }, + ], + doc_string_prefix: "Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", + }, + CornerStylePrefix { + prefix: "rounded_b", + fields: vec![ + quote! { corner_radii.bottom_left }, + quote! { corner_radii.bottom_right }, + ], + doc_string_prefix: "Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", + }, + CornerStylePrefix { + prefix: "rounded_r", + fields: vec![ + quote! { corner_radii.top_right }, + quote! { corner_radii.bottom_right }, + ], + doc_string_prefix: "Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", + }, + CornerStylePrefix { + prefix: "rounded_l", + fields: vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.bottom_left }, + ], + doc_string_prefix: "Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", + }, + CornerStylePrefix { + prefix: "rounded_tl", + fields: vec![quote! { corner_radii.top_left }], + doc_string_prefix: "Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", + }, + CornerStylePrefix { + prefix: "rounded_tr", + fields: vec![quote! { corner_radii.top_right }], + doc_string_prefix: "Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", + }, + CornerStylePrefix { + prefix: "rounded_bl", + fields: vec![quote! { corner_radii.bottom_left }], + doc_string_prefix: "Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", + }, + CornerStylePrefix { + prefix: "rounded_br", + fields: vec![quote! { corner_radii.bottom_right }], + doc_string_prefix: "Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", + }, + ] } fn corner_suffixes() -> Vec { - vec![ - CornerStyleSuffix { - suffix: "none", - radius_tokens: quote! { px(0.) }, - doc_string_suffix: "0px", - }, - CornerStyleSuffix { - suffix: "xs", - radius_tokens: quote! { rems(0.125) }, - doc_string_suffix: "2px (0.125rem)", - }, - CornerStyleSuffix { - suffix: "sm", - radius_tokens: quote! { rems(0.25) }, - doc_string_suffix: "4px (0.25rem)", - }, - CornerStyleSuffix { - suffix: "md", - radius_tokens: quote! { rems(0.375) }, - doc_string_suffix: "6px (0.375rem)", - }, - CornerStyleSuffix { - suffix: "lg", - radius_tokens: quote! { rems(0.5) }, - doc_string_suffix: "8px (0.5rem)", - }, - CornerStyleSuffix { - suffix: "xl", - radius_tokens: quote! { rems(0.75) }, - doc_string_suffix: "12px (0.75rem)", - }, - CornerStyleSuffix { - suffix: "2xl", - radius_tokens: quote! { rems(1.) }, - doc_string_suffix: "16px (1rem)", - }, - CornerStyleSuffix { - suffix: "3xl", - radius_tokens: quote! { rems(1.5) }, - doc_string_suffix: "24px (1.5rem)", - }, - CornerStyleSuffix { - suffix: "full", - radius_tokens: quote! { px(9999.) }, - doc_string_suffix: "9999px", - }, - ] + vec![ + CornerStyleSuffix { + suffix: "none", + radius_tokens: quote! { px(0.) }, + doc_string_suffix: "0px", + }, + CornerStyleSuffix { + suffix: "xs", + radius_tokens: quote! { rems(0.125) }, + doc_string_suffix: "2px (0.125rem)", + }, + CornerStyleSuffix { + suffix: "sm", + radius_tokens: quote! { rems(0.25) }, + doc_string_suffix: "4px (0.25rem)", + }, + CornerStyleSuffix { + suffix: "md", + radius_tokens: quote! { rems(0.375) }, + doc_string_suffix: "6px (0.375rem)", + }, + CornerStyleSuffix { + suffix: "lg", + radius_tokens: quote! { rems(0.5) }, + doc_string_suffix: "8px (0.5rem)", + }, + CornerStyleSuffix { + suffix: "xl", + radius_tokens: quote! { rems(0.75) }, + doc_string_suffix: "12px (0.75rem)", + }, + CornerStyleSuffix { + suffix: "2xl", + radius_tokens: quote! { rems(1.) }, + doc_string_suffix: "16px (1rem)", + }, + CornerStyleSuffix { + suffix: "3xl", + radius_tokens: quote! { rems(1.5) }, + doc_string_suffix: "24px (1.5rem)", + }, + CornerStyleSuffix { + suffix: "full", + radius_tokens: quote! { px(9999.) }, + doc_string_suffix: "9999px", + }, + ] } fn border_prefixes() -> Vec { - vec![ - BorderStylePrefix { - prefix: "border", - fields: vec![ - quote! { border_widths.top }, - quote! { border_widths.right }, - quote! { border_widths.bottom }, - quote! { border_widths.left }, - ], - doc_string_prefix: "Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)", - }, - BorderStylePrefix { - prefix: "border_t", - fields: vec![quote! { border_widths.top }], - doc_string_prefix: "Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_b", - fields: vec![quote! { border_widths.bottom }], - doc_string_prefix: "Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_r", - fields: vec![quote! { border_widths.right }], - doc_string_prefix: "Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_l", - fields: vec![quote! { border_widths.left }], - doc_string_prefix: "Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_x", - fields: vec![ - quote! { border_widths.left }, - quote! { border_widths.right }, - ], - doc_string_prefix: "Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", - }, - BorderStylePrefix { - prefix: "border_y", - fields: vec![ - quote! { border_widths.top }, - quote! { border_widths.bottom }, - ], - doc_string_prefix: "Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", - }, - ] + vec![ + BorderStylePrefix { + prefix: "border", + fields: vec![ + quote! { border_widths.top }, + quote! { border_widths.right }, + quote! { border_widths.bottom }, + quote! { border_widths.left }, + ], + doc_string_prefix: "Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)", + }, + BorderStylePrefix { + prefix: "border_t", + fields: vec![quote! { border_widths.top }], + doc_string_prefix: "Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", + }, + BorderStylePrefix { + prefix: "border_b", + fields: vec![quote! { border_widths.bottom }], + doc_string_prefix: "Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", + }, + BorderStylePrefix { + prefix: "border_r", + fields: vec![quote! { border_widths.right }], + doc_string_prefix: "Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", + }, + BorderStylePrefix { + prefix: "border_l", + fields: vec![quote! { border_widths.left }], + doc_string_prefix: "Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", + }, + BorderStylePrefix { + prefix: "border_x", + fields: vec![ + quote! { border_widths.left }, + quote! { border_widths.right }, + ], + doc_string_prefix: "Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", + }, + BorderStylePrefix { + prefix: "border_y", + fields: vec![ + quote! { border_widths.top }, + quote! { border_widths.bottom }, + ], + doc_string_prefix: "Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", + }, + ] } fn border_suffixes() -> Vec { - vec![ - BorderStyleSuffix { - suffix: "0", - width_tokens: quote! { px(0.)}, - doc_string_suffix: "0px", - }, - BorderStyleSuffix { - suffix: "1", - width_tokens: quote! { px(1.) }, - doc_string_suffix: "1px", - }, - BorderStyleSuffix { - suffix: "2", - width_tokens: quote! { px(2.) }, - doc_string_suffix: "2px", - }, - BorderStyleSuffix { - suffix: "3", - width_tokens: quote! { px(3.) }, - doc_string_suffix: "3px", - }, - BorderStyleSuffix { - suffix: "4", - width_tokens: quote! { px(4.) }, - doc_string_suffix: "4px", - }, - BorderStyleSuffix { - suffix: "5", - width_tokens: quote! { px(5.) }, - doc_string_suffix: "5px", - }, - BorderStyleSuffix { - suffix: "6", - width_tokens: quote! { px(6.) }, - doc_string_suffix: "6px", - }, - BorderStyleSuffix { - suffix: "7", - width_tokens: quote! { px(7.) }, - doc_string_suffix: "7px", - }, - BorderStyleSuffix { - suffix: "8", - width_tokens: quote! { px(8.) }, - doc_string_suffix: "8px", - }, - BorderStyleSuffix { - suffix: "9", - width_tokens: quote! { px(9.) }, - doc_string_suffix: "9px", - }, - BorderStyleSuffix { - suffix: "10", - width_tokens: quote! { px(10.) }, - doc_string_suffix: "10px", - }, - BorderStyleSuffix { - suffix: "11", - width_tokens: quote! { px(11.) }, - doc_string_suffix: "11px", - }, - BorderStyleSuffix { - suffix: "12", - width_tokens: quote! { px(12.) }, - doc_string_suffix: "12px", - }, - BorderStyleSuffix { - suffix: "16", - width_tokens: quote! { px(16.) }, - doc_string_suffix: "16px", - }, - BorderStyleSuffix { - suffix: "20", - width_tokens: quote! { px(20.) }, - doc_string_suffix: "20px", - }, - BorderStyleSuffix { - suffix: "24", - width_tokens: quote! { px(24.) }, - doc_string_suffix: "24px", - }, - BorderStyleSuffix { - suffix: "32", - width_tokens: quote! { px(32.) }, - doc_string_suffix: "32px", - }, - ] + vec![ + BorderStyleSuffix { + suffix: "0", + width_tokens: quote! { px(0.)}, + doc_string_suffix: "0px", + }, + BorderStyleSuffix { + suffix: "1", + width_tokens: quote! { px(1.) }, + doc_string_suffix: "1px", + }, + BorderStyleSuffix { + suffix: "2", + width_tokens: quote! { px(2.) }, + doc_string_suffix: "2px", + }, + BorderStyleSuffix { + suffix: "3", + width_tokens: quote! { px(3.) }, + doc_string_suffix: "3px", + }, + BorderStyleSuffix { + suffix: "4", + width_tokens: quote! { px(4.) }, + doc_string_suffix: "4px", + }, + BorderStyleSuffix { + suffix: "5", + width_tokens: quote! { px(5.) }, + doc_string_suffix: "5px", + }, + BorderStyleSuffix { + suffix: "6", + width_tokens: quote! { px(6.) }, + doc_string_suffix: "6px", + }, + BorderStyleSuffix { + suffix: "7", + width_tokens: quote! { px(7.) }, + doc_string_suffix: "7px", + }, + BorderStyleSuffix { + suffix: "8", + width_tokens: quote! { px(8.) }, + doc_string_suffix: "8px", + }, + BorderStyleSuffix { + suffix: "9", + width_tokens: quote! { px(9.) }, + doc_string_suffix: "9px", + }, + BorderStyleSuffix { + suffix: "10", + width_tokens: quote! { px(10.) }, + doc_string_suffix: "10px", + }, + BorderStyleSuffix { + suffix: "11", + width_tokens: quote! { px(11.) }, + doc_string_suffix: "11px", + }, + BorderStyleSuffix { + suffix: "12", + width_tokens: quote! { px(12.) }, + doc_string_suffix: "12px", + }, + BorderStyleSuffix { + suffix: "16", + width_tokens: quote! { px(16.) }, + doc_string_suffix: "16px", + }, + BorderStyleSuffix { + suffix: "20", + width_tokens: quote! { px(20.) }, + doc_string_suffix: "20px", + }, + BorderStyleSuffix { + suffix: "24", + width_tokens: quote! { px(24.) }, + doc_string_suffix: "24px", + }, + BorderStyleSuffix { + suffix: "32", + width_tokens: quote! { px(32.) }, + doc_string_suffix: "32px", + }, + ] } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 087e01740d..9b8cbc4aa3 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -3,345 +3,345 @@ use proc_macro2::Ident; use quote::{format_ident, quote}; use std::mem; use syn::{ - self, Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaList, PathSegment, Token, Type, - parse::{Parse, ParseStream}, - parse_quote, - punctuated::Punctuated, - spanned::Spanned, + self, Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaList, PathSegment, Token, Type, + parse::{Parse, ParseStream}, + parse_quote, + punctuated::Punctuated, + spanned::Spanned, }; struct Args { - seeds: Vec, - max_retries: usize, - max_iterations: usize, - on_failure_fn_name: proc_macro2::TokenStream, + seeds: Vec, + max_retries: usize, + max_iterations: usize, + on_failure_fn_name: proc_macro2::TokenStream, } impl Parse for Args { - fn parse(input: ParseStream) -> Result { - let mut seeds = Vec::::new(); - let mut max_retries = 0; - let mut max_iterations = 1; - let mut on_failure_fn_name = quote!(None); + fn parse(input: ParseStream) -> Result { + let mut seeds = Vec::::new(); + let mut max_retries = 0; + let mut max_iterations = 1; + let mut on_failure_fn_name = quote!(None); - let metas = Punctuated::::parse_terminated(input)?; + let metas = Punctuated::::parse_terminated(input)?; - for meta in metas { - let ident = { - let meta_path = match &meta { - Meta::NameValue(meta) => &meta.path, - Meta::List(list) => &list.path, - Meta::Path(path) => { - return Err(syn::Error::new(path.span(), "invalid path argument")); - } - }; - let Some(ident) = meta_path.get_ident() else { - return Err(syn::Error::new(meta_path.span(), "unexpected path")); - }; - ident.to_string() - }; + for meta in metas { + let ident = { + let meta_path = match &meta { + Meta::NameValue(meta) => &meta.path, + Meta::List(list) => &list.path, + Meta::Path(path) => { + return Err(syn::Error::new(path.span(), "invalid path argument")); + } + }; + let Some(ident) = meta_path.get_ident() else { + return Err(syn::Error::new(meta_path.span(), "unexpected path")); + }; + ident.to_string() + }; - match (&meta, ident.as_str()) { - (Meta::NameValue(meta), "retries") => { - max_retries = parse_usize_from_expr(&meta.value)? - } - (Meta::NameValue(meta), "iterations") => { - max_iterations = parse_usize_from_expr(&meta.value)? - } - (Meta::NameValue(meta), "on_failure") => { - let Expr::Lit(ExprLit { - lit: Lit::Str(name), - .. - }) = &meta.value - else { - return Err(syn::Error::new( - meta.value.span(), - "on_failure argument must be a string", - )); - }; - let segments = name - .value() - .split("::") - .map(|part| PathSegment::from(Ident::new(part, name.span()))) - .collect(); - let path = syn::Path { - leading_colon: None, - segments, - }; - on_failure_fn_name = quote!(Some(#path)); - } - (Meta::NameValue(meta), "seed") => { - seeds = vec![parse_usize_from_expr(&meta.value)? as u64] - } - (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?, - (Meta::Path(_), _) => { - return Err(syn::Error::new(meta.span(), "invalid path argument")); - } - (_, _) => { - return Err(syn::Error::new(meta.span(), "invalid argument name")); - } - } - } + match (&meta, ident.as_str()) { + (Meta::NameValue(meta), "retries") => { + max_retries = parse_usize_from_expr(&meta.value)? + } + (Meta::NameValue(meta), "iterations") => { + max_iterations = parse_usize_from_expr(&meta.value)? + } + (Meta::NameValue(meta), "on_failure") => { + let Expr::Lit(ExprLit { + lit: Lit::Str(name), + .. + }) = &meta.value + else { + return Err(syn::Error::new( + meta.value.span(), + "on_failure argument must be a string", + )); + }; + let segments = name + .value() + .split("::") + .map(|part| PathSegment::from(Ident::new(part, name.span()))) + .collect(); + let path = syn::Path { + leading_colon: None, + segments, + }; + on_failure_fn_name = quote!(Some(#path)); + } + (Meta::NameValue(meta), "seed") => { + seeds = vec![parse_usize_from_expr(&meta.value)? as u64] + } + (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?, + (Meta::Path(_), _) => { + return Err(syn::Error::new(meta.span(), "invalid path argument")); + } + (_, _) => { + return Err(syn::Error::new(meta.span(), "invalid argument name")); + } + } + } - Ok(Args { - seeds, - max_retries, - max_iterations, - on_failure_fn_name, - }) - } + Ok(Args { + seeds, + max_retries, + max_iterations, + on_failure_fn_name, + }) + } } pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { - let args = syn::parse_macro_input!(args as Args); - let mut inner_fn = match syn::parse::(function) { - Ok(f) => f, - Err(err) => return error_to_stream(err), - }; + let args = syn::parse_macro_input!(args as Args); + let mut inner_fn = match syn::parse::(function) { + Ok(f) => f, + Err(err) => return error_to_stream(err), + }; - let inner_fn_attributes = mem::take(&mut inner_fn.attrs); - let inner_fn_name = format_ident!("__{}", inner_fn.sig.ident); - let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); + let inner_fn_attributes = mem::take(&mut inner_fn.attrs); + let inner_fn_name = format_ident!("__{}", inner_fn.sig.ident); + let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); - let result = generate_test_function( - args, - inner_fn, - inner_fn_attributes, - inner_fn_name, - outer_fn_name, - ); - match result { - Ok(tokens) => tokens, - Err(tokens) => tokens, - } + let result = generate_test_function( + args, + inner_fn, + inner_fn_attributes, + inner_fn_name, + outer_fn_name, + ); + match result { + Ok(tokens) => tokens, + Err(tokens) => tokens, + } } fn generate_test_function( - args: Args, - inner_fn: ItemFn, - inner_fn_attributes: Vec, - inner_fn_name: Ident, - outer_fn_name: Ident, + args: Args, + inner_fn: ItemFn, + inner_fn_attributes: Vec, + inner_fn_name: Ident, + outer_fn_name: Ident, ) -> Result { - let seeds = &args.seeds; - let max_retries = args.max_retries; - let num_iterations = args.max_iterations; - let on_failure_fn_name = &args.on_failure_fn_name; - let seeds = quote!( #(#seeds),* ); + let seeds = &args.seeds; + let max_retries = args.max_retries; + let num_iterations = args.max_iterations; + let on_failure_fn_name = &args.on_failure_fn_name; + let seeds = quote!( #(#seeds),* ); - let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { - // Pass to the test function the number of app contexts that it needs, - // based on its parameter list. - let mut cx_vars = proc_macro2::TokenStream::new(); - let mut cx_teardowns = proc_macro2::TokenStream::new(); - let mut inner_fn_args = proc_macro2::TokenStream::new(); - for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { - if let FnArg::Typed(arg) = arg { - if let Type::Path(ty) = &*arg.ty { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("StdRng") => { - inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); - continue; - } - Some("BackgroundExecutor") => { - inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new( - std::sync::Arc::new(dispatcher.clone()), - ),)); - continue; - } - _ => {} - } - } else if let Type::Reference(ty) = &*arg.ty - && let Type::Path(ty) = &*ty.elem - { - let last_segment = ty.path.segments.last(); - if let Some("TestAppContext") = - last_segment.map(|s| s.ident.to_string()).as_deref() - { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)), - ); - let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); - )); - cx_teardowns.extend(quote!( - #cx_varname.run_until_parked(); - #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); - #cx_varname.run_until_parked(); - drop(#cx_varname); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - } - } + let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { + // Pass to the test function the number of app contexts that it needs, + // based on its parameter list. + let mut cx_vars = proc_macro2::TokenStream::new(); + let mut cx_teardowns = proc_macro2::TokenStream::new(); + let mut inner_fn_args = proc_macro2::TokenStream::new(); + for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { + if let FnArg::Typed(arg) = arg { + if let Type::Path(ty) = &*arg.ty { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("StdRng") => { + inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); + continue; + } + Some("BackgroundExecutor") => { + inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new( + std::sync::Arc::new(dispatcher.clone()), + ),)); + continue; + } + _ => {} + } + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + if let Some("TestAppContext") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)), + ); + let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); + )); + cx_teardowns.extend(quote!( + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } + } + } - return Err(error_with_message("invalid function signature", arg)); - } + return Err(error_with_message("invalid function signature", arg)); + } - parse_quote! { - #[test] - fn #outer_fn_name() { - #inner_fn + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn - gpui::run_test( - #num_iterations, - &[#seeds], - #max_retries, - &mut |dispatcher, _seed| { - let exec = std::sync::Arc::new(dispatcher.clone()); - #cx_vars - gpui::ForegroundExecutor::new(exec.clone()).block_test(#inner_fn_name(#inner_fn_args)); - drop(exec); - #cx_teardowns - // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity - // cycles as cancelled tasks will be dropped properly once the runnable gets run again - // - // async-task does not give us the power to do this just yet though - dispatcher.drain_tasks(); - drop(dispatcher); - }, - #on_failure_fn_name - ); - } - } - } else { - // Pass to the test function the number of app contexts that it needs, - // based on its parameter list. - let mut cx_vars = proc_macro2::TokenStream::new(); - let mut cx_teardowns = proc_macro2::TokenStream::new(); - let mut inner_fn_args = proc_macro2::TokenStream::new(); - for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { - if let FnArg::Typed(arg) = arg { - if let Type::Path(ty) = &*arg.ty { - let last_segment = ty.path.segments.last(); + gpui::run_test( + #num_iterations, + &[#seeds], + #max_retries, + &mut |dispatcher, _seed| { + let exec = std::sync::Arc::new(dispatcher.clone()); + #cx_vars + gpui::ForegroundExecutor::new(exec.clone()).block_test(#inner_fn_name(#inner_fn_args)); + drop(exec); + #cx_teardowns + // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity + // cycles as cancelled tasks will be dropped properly once the runnable gets run again + // + // async-task does not give us the power to do this just yet though + dispatcher.drain_tasks(); + drop(dispatcher); + }, + #on_failure_fn_name + ); + } + } + } else { + // Pass to the test function the number of app contexts that it needs, + // based on its parameter list. + let mut cx_vars = proc_macro2::TokenStream::new(); + let mut cx_teardowns = proc_macro2::TokenStream::new(); + let mut inner_fn_args = proc_macro2::TokenStream::new(); + for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { + if let FnArg::Typed(arg) = arg { + if let Type::Path(ty) = &*arg.ty { + let last_segment = ty.path.segments.last(); - if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() { - inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); - continue; - } - } else if let Type::Reference(ty) = &*arg.ty - && let Type::Path(ty) = &*ty.elem - { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("App") => { - let cx_varname = format_ident!("cx_{}", ix); - let cx_varname_lock = format_ident!("cx_{}_lock", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); - let _entity_refcounts = #cx_varname_lock.ref_counts_drop_handle(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); - cx_teardowns.extend(quote!( - drop(#cx_varname_lock); - #cx_varname.run_until_parked(); - #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); - #cx_varname.run_until_parked(); - drop(#cx_varname); - )); - continue; - } - Some("TestAppContext") => { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); - )); - cx_teardowns.extend(quote!( - #cx_varname.run_until_parked(); - #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); - #cx_varname.run_until_parked(); - drop(#cx_varname); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - _ => {} - } - } - } + if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() { + inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); + continue; + } + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("App") => { + let cx_varname = format_ident!("cx_{}", ix); + let cx_varname_lock = format_ident!("cx_{}_lock", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); + let _entity_refcounts = #cx_varname_lock.ref_counts_drop_handle(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); + cx_teardowns.extend(quote!( + drop(#cx_varname_lock); + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + continue; + } + Some("TestAppContext") => { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); + )); + cx_teardowns.extend(quote!( + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } + _ => {} + } + } + } - return Err(error_with_message("invalid function signature", arg)); - } + return Err(error_with_message("invalid function signature", arg)); + } - parse_quote! { - #[test] - fn #outer_fn_name() { - #inner_fn + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn - gpui::run_test( - #num_iterations, - &[#seeds], - #max_retries, - &mut |dispatcher, _seed| { - #cx_vars - #inner_fn_name(#inner_fn_args); - #cx_teardowns - // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity - // cycles as cancelled tasks will be dropped properly once they runnable gets run again - // - // async-task does not give us the power to do this just yet though - dispatcher.drain_tasks(); - drop(dispatcher); - }, - #on_failure_fn_name, - ); - } - } - }; - outer_fn.attrs.extend(inner_fn_attributes); + gpui::run_test( + #num_iterations, + &[#seeds], + #max_retries, + &mut |dispatcher, _seed| { + #cx_vars + #inner_fn_name(#inner_fn_args); + #cx_teardowns + // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity + // cycles as cancelled tasks will be dropped properly once they runnable gets run again + // + // async-task does not give us the power to do this just yet though + dispatcher.drain_tasks(); + drop(dispatcher); + }, + #on_failure_fn_name, + ); + } + } + }; + outer_fn.attrs.extend(inner_fn_attributes); - Ok(TokenStream::from(quote!(#outer_fn))) + Ok(TokenStream::from(quote!(#outer_fn))) } fn parse_usize_from_expr(expr: &Expr) -> Result { - let Expr::Lit(ExprLit { - lit: Lit::Int(int), .. - }) = expr - else { - return Err(syn::Error::new(expr.span(), "expected an integer")); - }; - int.base10_parse() - .map_err(|_| syn::Error::new(int.span(), "failed to parse integer")) + let Expr::Lit(ExprLit { + lit: Lit::Int(int), .. + }) = expr + else { + return Err(syn::Error::new(expr.span(), "expected an integer")); + }; + int.base10_parse() + .map_err(|_| syn::Error::new(int.span(), "failed to parse integer")) } fn parse_u64_array(meta_list: &MetaList) -> Result, syn::Error> { - let mut result = Vec::new(); - let tokens = &meta_list.tokens; - let parser = |input: ParseStream| { - let exprs = Punctuated::::parse_terminated(input)?; - for expr in exprs { - if let Expr::Lit(ExprLit { - lit: Lit::Int(int), .. - }) = expr - { - let value: usize = int.base10_parse()?; - result.push(value as u64); - } else { - return Err(syn::Error::new(expr.span(), "expected an integer")); - } - } - Ok(()) - }; - syn::parse::Parser::parse2(parser, tokens.clone())?; - Ok(result) + let mut result = Vec::new(); + let tokens = &meta_list.tokens; + let parser = |input: ParseStream| { + let exprs = Punctuated::::parse_terminated(input)?; + for expr in exprs { + if let Expr::Lit(ExprLit { + lit: Lit::Int(int), .. + }) = expr + { + let value: usize = int.base10_parse()?; + result.push(value as u64); + } else { + return Err(syn::Error::new(expr.span(), "expected an integer")); + } + } + Ok(()) + }; + syn::parse::Parser::parse2(parser, tokens.clone())?; + Ok(result) } fn error_with_message(message: &str, spanned: impl Spanned) -> TokenStream { - error_to_stream(syn::Error::new(spanned.span(), message)) + error_to_stream(syn::Error::new(spanned.span(), message)) } fn error_to_stream(err: syn::Error) -> TokenStream { - TokenStream::from(err.into_compile_error()) + TokenStream::from(err.into_compile_error()) } diff --git a/crates/gpui_macros/tests/derive_context.rs b/crates/gpui_macros/tests/derive_context.rs index 6c122eff25..507b4dc804 100644 --- a/crates/gpui_macros/tests/derive_context.rs +++ b/crates/gpui_macros/tests/derive_context.rs @@ -1,13 +1,13 @@ #[test] fn test_derive_context() { - use gpui::{App, Window}; - use gpui_macros::{AppContext, VisualContext}; + use gpui::{App, Window}; + use gpui_macros::{AppContext, VisualContext}; - #[derive(AppContext, VisualContext)] - struct _MyCustomContext<'a, 'b> { - #[app] - app: &'a mut App, - #[window] - window: &'b mut Window, - } + #[derive(AppContext, VisualContext)] + struct _MyCustomContext<'a, 'b> { + #[app] + app: &'a mut App, + #[window] + window: &'b mut Window, + } } diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index 92f4e56e9c..77e3514ad3 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -3,131 +3,131 @@ // 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(not(rust_analyzer), gpui_macros::derive_inspector_reflection)] trait Transform: Clone { - /// Doubles the value - fn double(self) -> Self; + /// Doubles the value + fn double(self) -> Self; - /// Triples the value - fn triple(self) -> Self; + /// Triples the value + fn triple(self) -> Self; - /// Increments the value by one - /// - /// This method has a default implementation - fn increment(self) -> Self { - // Default implementation - self.add_one() - } + /// Increments the value by one + /// + /// This method has a default implementation + fn increment(self) -> Self { + // Default implementation + self.add_one() + } - /// Quadruples the value by doubling twice - fn quadruple(self) -> Self { - // Default implementation with mut self - self.double().double() - } + /// Quadruples the value by doubling twice + fn quadruple(self) -> Self { + // Default implementation with mut self + self.double().double() + } - // These methods will be filtered out: - #[allow(dead_code)] - fn add(&self, other: &Self) -> Self; - #[allow(dead_code)] - fn set_value(&mut self, value: i32); - #[allow(dead_code)] - fn get_value(&self) -> i32; + // These methods will be filtered out: + #[allow(dead_code)] + fn add(&self, other: &Self) -> Self; + #[allow(dead_code)] + fn set_value(&mut self, value: i32); + #[allow(dead_code)] + fn get_value(&self) -> i32; - /// Adds one to the value - fn add_one(self) -> Self; + /// Adds one to the value + fn add_one(self) -> Self; } #[derive(Debug, Clone, PartialEq)] struct Number(i32); impl Transform for Number { - fn double(self) -> Self { - Number(self.0 * 2) - } + fn double(self) -> Self { + Number(self.0 * 2) + } - fn triple(self) -> Self { - Number(self.0 * 3) - } + fn triple(self) -> Self { + Number(self.0 * 3) + } - fn add(&self, other: &Self) -> Self { - Number(self.0 + other.0) - } + fn add(&self, other: &Self) -> Self { + Number(self.0 + other.0) + } - fn set_value(&mut self, value: i32) { - self.0 = value; - } + fn set_value(&mut self, value: i32) { + self.0 = value; + } - fn get_value(&self) -> i32 { - self.0 - } + fn get_value(&self) -> i32 { + self.0 + } - fn add_one(self) -> Self { - Number(self.0 + 1) - } + fn add_one(self) -> Self { + Number(self.0 + 1) + } } #[test] fn test_derive_inspector_reflection() { - use transform_reflection::*; + use transform_reflection::*; - // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self - let methods = methods::(); + // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self + let methods = methods::(); - assert_eq!(methods.len(), 5); - let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); - assert!(method_names.contains(&"double")); - assert!(method_names.contains(&"triple")); - assert!(method_names.contains(&"increment")); - assert!(method_names.contains(&"quadruple")); - assert!(method_names.contains(&"add_one")); + assert_eq!(methods.len(), 5); + let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); + assert!(method_names.contains(&"double")); + assert!(method_names.contains(&"triple")); + assert!(method_names.contains(&"increment")); + assert!(method_names.contains(&"quadruple")); + assert!(method_names.contains(&"add_one")); - // Invoke methods by name - let num = Number(5); + // Invoke methods by name + let num = Number(5); - let doubled = find_method::("double").unwrap().invoke(num.clone()); - assert_eq!(doubled, Number(10)); + let doubled = find_method::("double").unwrap().invoke(num.clone()); + assert_eq!(doubled, Number(10)); - let tripled = find_method::("triple").unwrap().invoke(num.clone()); - assert_eq!(tripled, Number(15)); + let tripled = find_method::("triple").unwrap().invoke(num.clone()); + assert_eq!(tripled, Number(15)); - let incremented = find_method::("increment") - .unwrap() - .invoke(num.clone()); - assert_eq!(incremented, Number(6)); + let incremented = find_method::("increment") + .unwrap() + .invoke(num.clone()); + assert_eq!(incremented, Number(6)); - let quadrupled = find_method::("quadruple").unwrap().invoke(num); - assert_eq!(quadrupled, Number(20)); + let quadrupled = find_method::("quadruple").unwrap().invoke(num); + assert_eq!(quadrupled, Number(20)); - // Try to invoke a non-existent method - let result = find_method::("nonexistent"); - assert!(result.is_none()); + // Try to invoke a non-existent method + let result = find_method::("nonexistent"); + assert!(result.is_none()); - // Chain operations - let num = Number(10); - let result = find_method::("double") - .map(|m| m.invoke(num)) - .and_then(|n| find_method::("increment").map(|m| m.invoke(n))) - .and_then(|n| find_method::("triple").map(|m| m.invoke(n))); + // Chain operations + let num = Number(10); + let result = find_method::("double") + .map(|m| m.invoke(num)) + .and_then(|n| find_method::("increment").map(|m| m.invoke(n))) + .and_then(|n| find_method::("triple").map(|m| m.invoke(n))); - assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63 + assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63 - // Test documentationumentation capture - let double_method = find_method::("double").unwrap(); - assert_eq!(double_method.documentation, Some("Doubles the value")); + // Test documentationumentation capture + let double_method = find_method::("double").unwrap(); + assert_eq!(double_method.documentation, Some("Doubles the value")); - let triple_method = find_method::("triple").unwrap(); - assert_eq!(triple_method.documentation, Some("Triples the value")); + let triple_method = find_method::("triple").unwrap(); + assert_eq!(triple_method.documentation, Some("Triples the value")); - let increment_method = find_method::("increment").unwrap(); - assert_eq!( - increment_method.documentation, - Some("Increments the value by one\n\nThis method has a default implementation") - ); + let increment_method = find_method::("increment").unwrap(); + assert_eq!( + increment_method.documentation, + Some("Increments the value by one\n\nThis method has a default implementation") + ); - let quadruple_method = find_method::("quadruple").unwrap(); - assert_eq!( - quadruple_method.documentation, - Some("Quadruples the value by doubling twice") - ); + let quadruple_method = find_method::("quadruple").unwrap(); + assert_eq!( + quadruple_method.documentation, + Some("Quadruples the value by doubling twice") + ); - let add_one_method = find_method::("add_one").unwrap(); - assert_eq!(add_one_method.documentation, Some("Adds one to the value")); + let add_one_method = find_method::("add_one").unwrap(); + assert_eq!(add_one_method.documentation, Some("Adds one to the value")); } diff --git a/crates/gpui_macros/tests/render_test.rs b/crates/gpui_macros/tests/render_test.rs index 98c9062381..9efaf41fd9 100644 --- a/crates/gpui_macros/tests/render_test.rs +++ b/crates/gpui_macros/tests/render_test.rs @@ -1,7 +1,7 @@ #[test] fn test_derive_render() { - use gpui_macros::Render; + use gpui_macros::Render; - #[derive(Render)] - struct _Element; + #[derive(Render)] + struct _Element; } diff --git a/crates/gpui_media/build.rs b/crates/gpui_media/build.rs index 090002e9e9..17a81c3eae 100644 --- a/crates/gpui_media/build.rs +++ b/crates/gpui_media/build.rs @@ -1,43 +1,43 @@ #![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] #[cfg(target_os = "macos")] fn main() { - use std::{env, path::PathBuf, process::Command}; + use std::{env, path::PathBuf, process::Command}; - let sdk_path = String::from_utf8( - Command::new("xcrun") - .args(["--sdk", "macosx", "--show-sdk-path"]) - .output() - .unwrap() - .stdout, - ) - .unwrap(); - let sdk_path = sdk_path.trim_end(); + let sdk_path = String::from_utf8( + Command::new("xcrun") + .args(["--sdk", "macosx", "--show-sdk-path"]) + .output() + .unwrap() + .stdout, + ) + .unwrap(); + let sdk_path = sdk_path.trim_end(); - println!("cargo:rerun-if-changed=src/bindings.h"); - let bindings = bindgen::Builder::default() - .header("src/bindings.h") - .clang_arg(format!("-isysroot{}", sdk_path)) - .clang_arg("-xobjective-c") - .allowlist_type("CMItemIndex") - .allowlist_type("CMSampleTimingInfo") - .allowlist_type("CMVideoCodecType") - .allowlist_type("VTEncodeInfoFlags") - .allowlist_function("CMTimeMake") - .allowlist_var("kCVPixelFormatType_.*") - .allowlist_var("kCVReturn.*") - .allowlist_var("VTEncodeInfoFlags_.*") - .allowlist_var("kCMVideoCodecType_.*") - .allowlist_var("kCMTime.*") - .allowlist_var("kCMSampleAttachmentKey_.*") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .layout_tests(false) - .generate() - .expect("unable to generate bindings"); + println!("cargo:rerun-if-changed=src/bindings.h"); + let bindings = bindgen::Builder::default() + .header("src/bindings.h") + .clang_arg(format!("-isysroot{}", sdk_path)) + .clang_arg("-xobjective-c") + .allowlist_type("CMItemIndex") + .allowlist_type("CMSampleTimingInfo") + .allowlist_type("CMVideoCodecType") + .allowlist_type("VTEncodeInfoFlags") + .allowlist_function("CMTimeMake") + .allowlist_var("kCVPixelFormatType_.*") + .allowlist_var("kCVReturn.*") + .allowlist_var("VTEncodeInfoFlags_.*") + .allowlist_var("kCMVideoCodecType_.*") + .allowlist_var("kCMTime.*") + .allowlist_var("kCMSampleAttachmentKey_.*") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .layout_tests(false) + .generate() + .expect("unable to generate bindings"); - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("couldn't write dispatch bindings"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("couldn't write dispatch bindings"); } #[cfg(not(target_os = "macos"))] diff --git a/crates/gpui_media/src/media.rs b/crates/gpui_media/src/media.rs index c42bad62e7..1145917045 100644 --- a/crates/gpui_media/src/media.rs +++ b/crates/gpui_media/src/media.rs @@ -5,348 +5,348 @@ mod bindings; #[cfg(target_os = "macos")] pub mod core_media { - #![allow(non_snake_case)] + #![allow(non_snake_case)] - pub use crate::bindings::{ - CMItemIndex, CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType, - kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264, - }; - use anyhow::Result; - use core_foundation::{ - array::{CFArray, CFArrayRef}, - base::{CFTypeID, OSStatus, TCFType}, - declare_TCFType, - dictionary::CFDictionary, - impl_CFTypeDescription, impl_TCFType, - string::CFString, - }; - use core_video::image_buffer::{CVImageBuffer, CVImageBufferRef}; - use std::{ffi::c_void, ptr}; + pub use crate::bindings::{ + CMItemIndex, CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType, + kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264, + }; + use anyhow::Result; + use core_foundation::{ + array::{CFArray, CFArrayRef}, + base::{CFTypeID, OSStatus, TCFType}, + declare_TCFType, + dictionary::CFDictionary, + impl_CFTypeDescription, impl_TCFType, + string::CFString, + }; + use core_video::image_buffer::{CVImageBuffer, CVImageBufferRef}; + use std::{ffi::c_void, ptr}; - #[repr(C)] - pub struct __CMSampleBuffer(c_void); - // The ref type must be a pointer to the underlying struct. - pub type CMSampleBufferRef = *const __CMSampleBuffer; + #[repr(C)] + pub struct __CMSampleBuffer(c_void); + // The ref type must be a pointer to the underlying struct. + pub type CMSampleBufferRef = *const __CMSampleBuffer; - declare_TCFType!(CMSampleBuffer, CMSampleBufferRef); - impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID); - impl_CFTypeDescription!(CMSampleBuffer); + declare_TCFType!(CMSampleBuffer, CMSampleBufferRef); + impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID); + impl_CFTypeDescription!(CMSampleBuffer); - impl CMSampleBuffer { - pub fn attachments(&self) -> Vec> { - unsafe { - let attachments = - CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true); - CFArray::::wrap_under_get_rule(attachments) - .into_iter() - .map(|attachments| { - CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef()) - }) - .collect() - } - } + impl CMSampleBuffer { + pub fn attachments(&self) -> Vec> { + unsafe { + let attachments = + CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true); + CFArray::::wrap_under_get_rule(attachments) + .into_iter() + .map(|attachments| { + CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef()) + }) + .collect() + } + } - pub fn image_buffer(&self) -> Option { - unsafe { - let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef()); - if ptr.is_null() { - None - } else { - Some(CVImageBuffer::wrap_under_get_rule(ptr)) - } - } - } + pub fn image_buffer(&self) -> Option { + unsafe { + let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef()); + if ptr.is_null() { + None + } else { + Some(CVImageBuffer::wrap_under_get_rule(ptr)) + } + } + } - pub fn sample_timing_info(&self, index: usize) -> Result { - unsafe { - let mut timing_info = CMSampleTimingInfo { - duration: kCMTimeInvalid, - presentationTimeStamp: kCMTimeInvalid, - decodeTimeStamp: kCMTimeInvalid, - }; - let result = CMSampleBufferGetSampleTimingInfo( - self.as_concrete_TypeRef(), - index as CMItemIndex, - &mut timing_info, - ); - anyhow::ensure!( - result == 0, - "error getting sample timing info, code {result}" - ); - Ok(timing_info) - } - } + pub fn sample_timing_info(&self, index: usize) -> Result { + unsafe { + let mut timing_info = CMSampleTimingInfo { + duration: kCMTimeInvalid, + presentationTimeStamp: kCMTimeInvalid, + decodeTimeStamp: kCMTimeInvalid, + }; + let result = CMSampleBufferGetSampleTimingInfo( + self.as_concrete_TypeRef(), + index as CMItemIndex, + &mut timing_info, + ); + anyhow::ensure!( + result == 0, + "error getting sample timing info, code {result}" + ); + Ok(timing_info) + } + } - pub fn format_description(&self) -> CMFormatDescription { - unsafe { - CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription( - self.as_concrete_TypeRef(), - )) - } - } + pub fn format_description(&self) -> CMFormatDescription { + unsafe { + CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription( + self.as_concrete_TypeRef(), + )) + } + } - pub fn data(&self) -> CMBlockBuffer { - unsafe { - CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer( - self.as_concrete_TypeRef(), - )) - } - } - } + pub fn data(&self) -> CMBlockBuffer { + unsafe { + CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer( + self.as_concrete_TypeRef(), + )) + } + } + } - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMSampleBufferGetTypeID() -> CFTypeID; - fn CMSampleBufferGetSampleAttachmentsArray( - buffer: CMSampleBufferRef, - create_if_necessary: bool, - ) -> CFArrayRef; - fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef; - fn CMSampleBufferGetSampleTimingInfo( - buffer: CMSampleBufferRef, - index: CMItemIndex, - timing_info_out: *mut CMSampleTimingInfo, - ) -> OSStatus; - fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef; - fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef; - } + #[link(name = "CoreMedia", kind = "framework")] + unsafe extern "C" { + fn CMSampleBufferGetTypeID() -> CFTypeID; + fn CMSampleBufferGetSampleAttachmentsArray( + buffer: CMSampleBufferRef, + create_if_necessary: bool, + ) -> CFArrayRef; + fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef; + fn CMSampleBufferGetSampleTimingInfo( + buffer: CMSampleBufferRef, + index: CMItemIndex, + timing_info_out: *mut CMSampleTimingInfo, + ) -> OSStatus; + fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef; + fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef; + } - #[repr(C)] - pub struct __CMFormatDescription(c_void); - pub type CMFormatDescriptionRef = *const __CMFormatDescription; + #[repr(C)] + pub struct __CMFormatDescription(c_void); + pub type CMFormatDescriptionRef = *const __CMFormatDescription; - declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef); - impl_TCFType!( - CMFormatDescription, - CMFormatDescriptionRef, - CMFormatDescriptionGetTypeID - ); - impl_CFTypeDescription!(CMFormatDescription); + declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef); + impl_TCFType!( + CMFormatDescription, + CMFormatDescriptionRef, + CMFormatDescriptionGetTypeID + ); + impl_CFTypeDescription!(CMFormatDescription); - impl CMFormatDescription { - pub fn h264_parameter_set_count(&self) -> usize { - unsafe { - let mut count = 0; - let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - self.as_concrete_TypeRef(), - 0, - ptr::null_mut(), - ptr::null_mut(), - &mut count, - ptr::null_mut(), - ); - assert_eq!(result, 0); - count - } - } + impl CMFormatDescription { + pub fn h264_parameter_set_count(&self) -> usize { + unsafe { + let mut count = 0; + let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + self.as_concrete_TypeRef(), + 0, + ptr::null_mut(), + ptr::null_mut(), + &mut count, + ptr::null_mut(), + ); + assert_eq!(result, 0); + count + } + } - pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> { - unsafe { - let mut bytes = ptr::null(); - let mut len = 0; - let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - self.as_concrete_TypeRef(), - index, - &mut bytes, - &mut len, - ptr::null_mut(), - ptr::null_mut(), - ); - anyhow::ensure!(result == 0, "error getting parameter set, code: {result}"); - Ok(std::slice::from_raw_parts(bytes, len)) - } - } - } + pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> { + unsafe { + let mut bytes = ptr::null(); + let mut len = 0; + let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + self.as_concrete_TypeRef(), + index, + &mut bytes, + &mut len, + ptr::null_mut(), + ptr::null_mut(), + ); + anyhow::ensure!(result == 0, "error getting parameter set, code: {result}"); + Ok(std::slice::from_raw_parts(bytes, len)) + } + } + } - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMFormatDescriptionGetTypeID() -> CFTypeID; - fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - video_desc: CMFormatDescriptionRef, - parameter_set_index: usize, - parameter_set_pointer_out: *mut *const u8, - parameter_set_size_out: *mut usize, - parameter_set_count_out: *mut usize, - NALUnitHeaderLengthOut: *mut isize, - ) -> OSStatus; - } + #[link(name = "CoreMedia", kind = "framework")] + unsafe extern "C" { + fn CMFormatDescriptionGetTypeID() -> CFTypeID; + fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + video_desc: CMFormatDescriptionRef, + parameter_set_index: usize, + parameter_set_pointer_out: *mut *const u8, + parameter_set_size_out: *mut usize, + parameter_set_count_out: *mut usize, + NALUnitHeaderLengthOut: *mut isize, + ) -> OSStatus; + } - #[repr(C)] - pub struct __CMBlockBuffer(c_void); - pub type CMBlockBufferRef = *const __CMBlockBuffer; + #[repr(C)] + pub struct __CMBlockBuffer(c_void); + pub type CMBlockBufferRef = *const __CMBlockBuffer; - declare_TCFType!(CMBlockBuffer, CMBlockBufferRef); - impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID); - impl_CFTypeDescription!(CMBlockBuffer); + declare_TCFType!(CMBlockBuffer, CMBlockBufferRef); + impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID); + impl_CFTypeDescription!(CMBlockBuffer); - impl CMBlockBuffer { - pub fn bytes(&self) -> &[u8] { - unsafe { - let mut bytes = ptr::null(); - let mut len = 0; - let result = CMBlockBufferGetDataPointer( - self.as_concrete_TypeRef(), - 0, - &mut 0, - &mut len, - &mut bytes, - ); - assert!(result == 0, "could not get block buffer data"); - std::slice::from_raw_parts(bytes, len) - } - } - } + impl CMBlockBuffer { + pub fn bytes(&self) -> &[u8] { + unsafe { + let mut bytes = ptr::null(); + let mut len = 0; + let result = CMBlockBufferGetDataPointer( + self.as_concrete_TypeRef(), + 0, + &mut 0, + &mut len, + &mut bytes, + ); + assert!(result == 0, "could not get block buffer data"); + std::slice::from_raw_parts(bytes, len) + } + } + } - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMBlockBufferGetTypeID() -> CFTypeID; - fn CMBlockBufferGetDataPointer( - buffer: CMBlockBufferRef, - offset: usize, - length_at_offset_out: *mut usize, - total_length_out: *mut usize, - data_pointer_out: *mut *const u8, - ) -> OSStatus; - } + #[link(name = "CoreMedia", kind = "framework")] + unsafe extern "C" { + fn CMBlockBufferGetTypeID() -> CFTypeID; + fn CMBlockBufferGetDataPointer( + buffer: CMBlockBufferRef, + offset: usize, + length_at_offset_out: *mut usize, + total_length_out: *mut usize, + data_pointer_out: *mut *const u8, + ) -> OSStatus; + } } #[cfg(target_os = "macos")] pub mod core_video { - #![allow(non_snake_case)] + #![allow(non_snake_case)] - #[cfg(target_os = "macos")] - use core_foundation::{ - base::{CFTypeID, TCFType}, - declare_TCFType, impl_CFTypeDescription, impl_TCFType, - }; - #[cfg(target_os = "macos")] - use std::ffi::c_void; + #[cfg(target_os = "macos")] + use core_foundation::{ + base::{CFTypeID, TCFType}, + declare_TCFType, impl_CFTypeDescription, impl_TCFType, + }; + #[cfg(target_os = "macos")] + use std::ffi::c_void; - use crate::bindings::{CVReturn, kCVReturnSuccess}; - pub use crate::bindings::{ - kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, - kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar, - }; - use anyhow::Result; - use core_foundation::{ - base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef, - }; - use foreign_types::ForeignTypeRef; + use crate::bindings::{CVReturn, kCVReturnSuccess}; + pub use crate::bindings::{ + kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar, + }; + use anyhow::Result; + use core_foundation::{ + base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef, + }; + use foreign_types::ForeignTypeRef; - use metal::{MTLDevice, MTLPixelFormat}; - use std::ptr; + use metal::{MTLDevice, MTLPixelFormat}; + use std::ptr; - #[repr(C)] - pub struct __CVMetalTextureCache(c_void); - pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache; + #[repr(C)] + pub struct __CVMetalTextureCache(c_void); + pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache; - declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef); - impl_TCFType!( - CVMetalTextureCache, - CVMetalTextureCacheRef, - CVMetalTextureCacheGetTypeID - ); - impl_CFTypeDescription!(CVMetalTextureCache); + declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef); + impl_TCFType!( + CVMetalTextureCache, + CVMetalTextureCacheRef, + CVMetalTextureCacheGetTypeID + ); + impl_CFTypeDescription!(CVMetalTextureCache); - impl CVMetalTextureCache { - /// # Safety - /// - /// metal_device must be valid according to CVMetalTextureCacheCreate - pub unsafe fn new(metal_device: *mut MTLDevice) -> Result { - let mut this = ptr::null(); - let result = unsafe { - CVMetalTextureCacheCreate( - kCFAllocatorDefault, - ptr::null(), - metal_device, - ptr::null(), - &mut this, - ) - }; - anyhow::ensure!( - result == kCVReturnSuccess, - "could not create texture cache, code: {result}" - ); - unsafe { Ok(CVMetalTextureCache::wrap_under_create_rule(this)) } - } + impl CVMetalTextureCache { + /// # Safety + /// + /// metal_device must be valid according to CVMetalTextureCacheCreate + pub unsafe fn new(metal_device: *mut MTLDevice) -> Result { + let mut this = ptr::null(); + let result = unsafe { + CVMetalTextureCacheCreate( + kCFAllocatorDefault, + ptr::null(), + metal_device, + ptr::null(), + &mut this, + ) + }; + anyhow::ensure!( + result == kCVReturnSuccess, + "could not create texture cache, code: {result}" + ); + unsafe { Ok(CVMetalTextureCache::wrap_under_create_rule(this)) } + } - /// # Safety - /// - /// The arguments to this function must be valid according to CVMetalTextureCacheCreateTextureFromImage - pub unsafe fn create_texture_from_image( - &self, - source: ::core_video::image_buffer::CVImageBufferRef, - texture_attributes: CFDictionaryRef, - pixel_format: MTLPixelFormat, - width: usize, - height: usize, - plane_index: usize, - ) -> Result { - let mut this = ptr::null(); - let result = unsafe { - CVMetalTextureCacheCreateTextureFromImage( - kCFAllocatorDefault, - self.as_concrete_TypeRef(), - source, - texture_attributes, - pixel_format, - width, - height, - plane_index, - &mut this, - ) - }; - anyhow::ensure!( - result == kCVReturnSuccess, - "could not create texture, code: {result}" - ); - unsafe { Ok(CVMetalTexture::wrap_under_create_rule(this)) } - } - } + /// # Safety + /// + /// The arguments to this function must be valid according to CVMetalTextureCacheCreateTextureFromImage + pub unsafe fn create_texture_from_image( + &self, + source: ::core_video::image_buffer::CVImageBufferRef, + texture_attributes: CFDictionaryRef, + pixel_format: MTLPixelFormat, + width: usize, + height: usize, + plane_index: usize, + ) -> Result { + let mut this = ptr::null(); + let result = unsafe { + CVMetalTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, + self.as_concrete_TypeRef(), + source, + texture_attributes, + pixel_format, + width, + height, + plane_index, + &mut this, + ) + }; + anyhow::ensure!( + result == kCVReturnSuccess, + "could not create texture, code: {result}" + ); + unsafe { Ok(CVMetalTexture::wrap_under_create_rule(this)) } + } + } - #[link(name = "CoreVideo", kind = "framework")] - unsafe extern "C" { - fn CVMetalTextureCacheGetTypeID() -> CFTypeID; - fn CVMetalTextureCacheCreate( - allocator: CFAllocatorRef, - cache_attributes: CFDictionaryRef, - metal_device: *const MTLDevice, - texture_attributes: CFDictionaryRef, - cache_out: *mut CVMetalTextureCacheRef, - ) -> CVReturn; - fn CVMetalTextureCacheCreateTextureFromImage( - allocator: CFAllocatorRef, - texture_cache: CVMetalTextureCacheRef, - source_image: ::core_video::image_buffer::CVImageBufferRef, - texture_attributes: CFDictionaryRef, - pixel_format: MTLPixelFormat, - width: usize, - height: usize, - plane_index: usize, - texture_out: *mut CVMetalTextureRef, - ) -> CVReturn; - } + #[link(name = "CoreVideo", kind = "framework")] + unsafe extern "C" { + fn CVMetalTextureCacheGetTypeID() -> CFTypeID; + fn CVMetalTextureCacheCreate( + allocator: CFAllocatorRef, + cache_attributes: CFDictionaryRef, + metal_device: *const MTLDevice, + texture_attributes: CFDictionaryRef, + cache_out: *mut CVMetalTextureCacheRef, + ) -> CVReturn; + fn CVMetalTextureCacheCreateTextureFromImage( + allocator: CFAllocatorRef, + texture_cache: CVMetalTextureCacheRef, + source_image: ::core_video::image_buffer::CVImageBufferRef, + texture_attributes: CFDictionaryRef, + pixel_format: MTLPixelFormat, + width: usize, + height: usize, + plane_index: usize, + texture_out: *mut CVMetalTextureRef, + ) -> CVReturn; + } - #[repr(C)] - pub struct __CVMetalTexture(c_void); - pub type CVMetalTextureRef = *const __CVMetalTexture; + #[repr(C)] + pub struct __CVMetalTexture(c_void); + pub type CVMetalTextureRef = *const __CVMetalTexture; - declare_TCFType!(CVMetalTexture, CVMetalTextureRef); - impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID); - impl_CFTypeDescription!(CVMetalTexture); + declare_TCFType!(CVMetalTexture, CVMetalTextureRef); + impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID); + impl_CFTypeDescription!(CVMetalTexture); - impl CVMetalTexture { - pub fn as_texture_ref(&self) -> &metal::TextureRef { - unsafe { - let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef()); - metal::TextureRef::from_ptr(texture as *mut _) - } - } - } + impl CVMetalTexture { + pub fn as_texture_ref(&self) -> &metal::TextureRef { + unsafe { + let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef()); + metal::TextureRef::from_ptr(texture as *mut _) + } + } + } - #[link(name = "CoreVideo", kind = "framework")] - unsafe extern "C" { - fn CVMetalTextureGetTypeID() -> CFTypeID; - fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void; - } + #[link(name = "CoreVideo", kind = "framework")] + unsafe extern "C" { + fn CVMetalTextureGetTypeID() -> CFTypeID; + fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void; + } } diff --git a/crates/gpui_platform/src/gpui_platform.rs b/crates/gpui_platform/src/gpui_platform.rs index 1d2fea90b4..2472002bf1 100644 --- a/crates/gpui_platform/src/gpui_platform.rs +++ b/crates/gpui_platform/src/gpui_platform.rs @@ -7,180 +7,180 @@ use std::rc::Rc; /// Returns a background executor for the current platform. pub fn background_executor() -> gpui::BackgroundExecutor { - current_platform(true).background_executor() + current_platform(true).background_executor() } pub fn application() -> gpui::Application { - gpui::Application::with_platform(current_platform(false)) + gpui::Application::with_platform(current_platform(false)) } pub fn headless() -> gpui::Application { - gpui::Application::with_platform(current_platform(true)) + gpui::Application::with_platform(current_platform(true)) } /// Unlike `application`, this function returns a single-threaded web application. #[cfg(target_family = "wasm")] pub fn single_threaded_web() -> gpui::Application { - gpui::Application::with_platform(Rc::new(gpui_web::WebPlatform::new(false))) + gpui::Application::with_platform(Rc::new(gpui_web::WebPlatform::new(false))) } /// Initializes panic hooks and logging for the web platform. /// Call this before running the application in a wasm_bindgen entrypoint. #[cfg(target_family = "wasm")] pub fn web_init() { - console_error_panic_hook::set_once(); - gpui_web::init_logging(); + console_error_panic_hook::set_once(); + gpui_web::init_logging(); } /// Returns the default [`Platform`] for the current OS. pub fn current_platform(headless: bool) -> Rc { - #[cfg(target_os = "macos")] - { - Rc::new(gpui_macos::MacPlatform::new(headless)) - } + #[cfg(target_os = "macos")] + { + Rc::new(gpui_macos::MacPlatform::new(headless)) + } - #[cfg(target_os = "windows")] - { - Rc::new( - gpui_windows::WindowsPlatform::new(headless) - .expect("failed to initialize Windows platform"), - ) - } + #[cfg(target_os = "windows")] + { + Rc::new( + gpui_windows::WindowsPlatform::new(headless) + .expect("failed to initialize Windows platform"), + ) + } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - gpui_linux::current_platform(headless) - } + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + gpui_linux::current_platform(headless) + } - #[cfg(target_family = "wasm")] - { - let _ = headless; - Rc::new(gpui_web::WebPlatform::new(true)) - } + #[cfg(target_family = "wasm")] + { + let _ = headless; + Rc::new(gpui_web::WebPlatform::new(true)) + } } /// Returns a new [`HeadlessRenderer`] for the current platform, if available. #[cfg(feature = "test-support")] pub fn current_headless_renderer() -> Option> { - #[cfg(target_os = "macos")] - { - Some(Box::new( - gpui_macos::metal_renderer::MetalHeadlessRenderer::new(), - )) - } + #[cfg(target_os = "macos")] + { + Some(Box::new( + gpui_macos::metal_renderer::MetalHeadlessRenderer::new(), + )) + } - #[cfg(not(target_os = "macos"))] - { - None - } + #[cfg(not(target_os = "macos"))] + { + None + } } #[cfg(all(test, target_os = "macos"))] mod tests { - use super::*; - use gpui::{AppContext, Empty, VisualTestAppContext}; - use std::cell::RefCell; - use std::time::Duration; + use super::*; + use gpui::{AppContext, Empty, VisualTestAppContext}; + use std::cell::RefCell; + use std::time::Duration; - // Note: All VisualTestAppContext tests are ignored by default because they require - // the macOS main thread. Standard Rust tests run on worker threads, which causes - // SIGABRT when interacting with macOS AppKit/Cocoa APIs. - // - // To run these tests, use: - // cargo test -p gpui visual_test_context -- --ignored --test-threads=1 + // Note: All VisualTestAppContext tests are ignored by default because they require + // the macOS main thread. Standard Rust tests run on worker threads, which causes + // SIGABRT when interacting with macOS AppKit/Cocoa APIs. + // + // To run these tests, use: + // cargo test -p gpui visual_test_context -- --ignored --test-threads=1 - #[test] - #[ignore] // Requires macOS main thread - fn test_foreground_tasks_run_with_run_until_parked() { - let mut cx = VisualTestAppContext::new(current_platform(false)); + #[test] + #[ignore] // Requires macOS main thread + fn test_foreground_tasks_run_with_run_until_parked() { + let mut cx = VisualTestAppContext::new(current_platform(false)); - let task_ran = Rc::new(RefCell::new(false)); + let task_ran = Rc::new(RefCell::new(false)); - // Spawn a foreground task via the App's spawn method - // This should use our TestDispatcher, not the MacDispatcher - { - let task_ran = task_ran.clone(); - cx.update(|cx| { - cx.spawn(async move |_| { - *task_ran.borrow_mut() = true; - }) - .detach(); - }); - } + // Spawn a foreground task via the App's spawn method + // This should use our TestDispatcher, not the MacDispatcher + { + let task_ran = task_ran.clone(); + cx.update(|cx| { + cx.spawn(async move |_| { + *task_ran.borrow_mut() = true; + }) + .detach(); + }); + } - // The task should not have run yet - assert!(!*task_ran.borrow()); + // The task should not have run yet + assert!(!*task_ran.borrow()); - // Run until parked should execute the foreground task - cx.run_until_parked(); + // Run until parked should execute the foreground task + cx.run_until_parked(); - // Now the task should have run - assert!(*task_ran.borrow()); - } + // Now the task should have run + assert!(*task_ran.borrow()); + } - #[test] - #[ignore] // Requires macOS main thread - fn test_advance_clock_triggers_delayed_tasks() { - let mut cx = VisualTestAppContext::new(current_platform(false)); + #[test] + #[ignore] // Requires macOS main thread + fn test_advance_clock_triggers_delayed_tasks() { + let mut cx = VisualTestAppContext::new(current_platform(false)); - let task_ran = Rc::new(RefCell::new(false)); + let task_ran = Rc::new(RefCell::new(false)); - // Spawn a task that waits for a timer - { - let task_ran = task_ran.clone(); - let executor = cx.background_executor.clone(); - cx.update(|cx| { - cx.spawn(async move |_| { - executor.timer(Duration::from_millis(500)).await; - *task_ran.borrow_mut() = true; - }) - .detach(); - }); - } + // Spawn a task that waits for a timer + { + let task_ran = task_ran.clone(); + let executor = cx.background_executor.clone(); + cx.update(|cx| { + cx.spawn(async move |_| { + executor.timer(Duration::from_millis(500)).await; + *task_ran.borrow_mut() = true; + }) + .detach(); + }); + } - // Run until parked - the task should be waiting on the timer - cx.run_until_parked(); - assert!(!*task_ran.borrow()); + // Run until parked - the task should be waiting on the timer + cx.run_until_parked(); + assert!(!*task_ran.borrow()); - // Advance clock past the timer duration - cx.advance_clock(Duration::from_millis(600)); + // Advance clock past the timer duration + cx.advance_clock(Duration::from_millis(600)); - // Now the task should have completed - assert!(*task_ran.borrow()); - } + // Now the task should have completed + assert!(*task_ran.borrow()); + } - #[test] - #[ignore] // Requires macOS main thread - window creation fails on test threads - fn test_window_spawn_uses_test_dispatcher() { - let mut cx = VisualTestAppContext::new(current_platform(false)); + #[test] + #[ignore] // Requires macOS main thread - window creation fails on test threads + fn test_window_spawn_uses_test_dispatcher() { + let mut cx = VisualTestAppContext::new(current_platform(false)); - let task_ran = Rc::new(RefCell::new(false)); + let task_ran = Rc::new(RefCell::new(false)); - let window = cx - .open_offscreen_window_default(|_, cx| cx.new(|_| Empty)) - .expect("Failed to open window"); + let window = cx + .open_offscreen_window_default(|_, cx| cx.new(|_| Empty)) + .expect("Failed to open window"); - // Spawn a task via window.spawn - this is the critical test case - // for tooltip behavior, as tooltips use window.spawn for delayed show - { - let task_ran = task_ran.clone(); - cx.update_window(window.into(), |_, window, cx| { - window - .spawn(cx, async move |_| { - *task_ran.borrow_mut() = true; - }) - .detach(); - }) - .ok(); - } + // Spawn a task via window.spawn - this is the critical test case + // for tooltip behavior, as tooltips use window.spawn for delayed show + { + let task_ran = task_ran.clone(); + cx.update_window(window.into(), |_, window, cx| { + window + .spawn(cx, async move |_| { + *task_ran.borrow_mut() = true; + }) + .detach(); + }) + .ok(); + } - // The task should not have run yet - assert!(!*task_ran.borrow()); + // The task should not have run yet + assert!(!*task_ran.borrow()); - // Run until parked should execute the foreground task spawned via window - cx.run_until_parked(); + // Run until parked should execute the foreground task spawned via window + cx.run_until_parked(); - // Now the task should have run - assert!(*task_ran.borrow()); - } + // Now the task should have run + assert!(*task_ran.borrow()); + } } diff --git a/crates/gpui_refineable/src/refineable.rs b/crates/gpui_refineable/src/refineable.rs index b2305d4b5a..45fc170dae 100644 --- a/crates/gpui_refineable/src/refineable.rs +++ b/crates/gpui_refineable/src/refineable.rs @@ -27,45 +27,45 @@ pub use derive_refineable::Refineable; /// Fields can be marked with: /// - `#[refineable]`: Field is itself refineable (uses nested refinement type) pub trait Refineable: Clone { - type Refinement: Refineable + IsEmpty + Default; + type Refinement: Refineable + IsEmpty + Default; - /// Applies the given refinement to this instance, modifying it in place. - /// - /// Only non-empty values in the refinement are applied. - /// - /// * For refineable fields, this recursively calls `refine`. - /// * For other fields, the value is replaced if present in the refinement. - fn refine(&mut self, refinement: &Self::Refinement); + /// Applies the given refinement to this instance, modifying it in place. + /// + /// Only non-empty values in the refinement are applied. + /// + /// * For refineable fields, this recursively calls `refine`. + /// * For other fields, the value is replaced if present in the refinement. + fn refine(&mut self, refinement: &Self::Refinement); - /// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling - /// `refine` on it. - fn refined(self, refinement: Self::Refinement) -> Self; + /// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling + /// `refine` on it. + fn refined(self, refinement: Self::Refinement) -> Self; - /// Creates an instance from a cascade by merging all refinements atop the default value. - fn from_cascade(cascade: &Cascade) -> Self - where - Self: Default + Sized, - { - Self::default().refined(cascade.merged()) - } + /// Creates an instance from a cascade by merging all refinements atop the default value. + fn from_cascade(cascade: &Cascade) -> Self + where + Self: Default + Sized, + { + Self::default().refined(cascade.merged()) + } - /// Returns `true` if this instance would contain all values from the refinement. - /// - /// For refineable fields, this recursively checks `is_superset_of`. For other fields, this - /// checks if the refinement's `Some` values match this instance's values. - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool; + /// Returns `true` if this instance would contain all values from the refinement. + /// + /// For refineable fields, this recursively checks `is_superset_of`. For other fields, this + /// checks if the refinement's `Some` values match this instance's values. + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool; - /// Returns a refinement that represents the difference between this instance and the given - /// refinement. - /// - /// For refineable fields, this recursively calls `subtract`. For other fields, the field is - /// `None` if the field's value is equal to the refinement. - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement; + /// Returns a refinement that represents the difference between this instance and the given + /// refinement. + /// + /// For refineable fields, this recursively calls `subtract`. For other fields, the field is + /// `None` if the field's value is equal to the refinement. + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement; } pub trait IsEmpty { - /// Returns `true` if applying this refinement would have no effect. - fn is_empty(&self) -> bool; + /// Returns `true` if applying this refinement would have no effect. + fn is_empty(&self) -> bool; } /// A cascade of refinements that can be merged in priority order. @@ -80,9 +80,9 @@ pub trait IsEmpty { pub struct Cascade(Vec>); impl Default for Cascade { - fn default() -> Self { - Self(vec![Some(Default::default())]) - } + fn default() -> Self { + Self(vec![Some(Default::default())]) + } } /// A handle to a specific slot in a cascade. @@ -93,40 +93,40 @@ impl Default for Cascade { pub struct CascadeSlot(usize); impl Cascade { - /// Reserves a new slot in the cascade and returns a handle to it. - /// - /// The new slot is initially empty (`None`) and can be populated later - /// using `set()`. - pub fn reserve(&mut self) -> CascadeSlot { - self.0.push(None); - CascadeSlot(self.0.len() - 1) - } + /// Reserves a new slot in the cascade and returns a handle to it. + /// + /// The new slot is initially empty (`None`) and can be populated later + /// using `set()`. + pub fn reserve(&mut self) -> CascadeSlot { + self.0.push(None); + CascadeSlot(self.0.len() - 1) + } - /// Returns a mutable reference to the base refinement (slot 0). - /// - /// The base refinement is always present and serves as the foundation - /// for the cascade. - pub fn base(&mut self) -> &mut S::Refinement { - self.0[0].as_mut().unwrap() - } + /// Returns a mutable reference to the base refinement (slot 0). + /// + /// The base refinement is always present and serves as the foundation + /// for the cascade. + pub fn base(&mut self) -> &mut S::Refinement { + self.0[0].as_mut().unwrap() + } - /// Sets the refinement for a specific slot in the cascade. - /// - /// Setting a slot to `None` effectively removes it from consideration - /// during merging. - pub fn set(&mut self, slot: CascadeSlot, refinement: Option) { - self.0[slot.0] = refinement - } + /// Sets the refinement for a specific slot in the cascade. + /// + /// Setting a slot to `None` effectively removes it from consideration + /// during merging. + pub fn set(&mut self, slot: CascadeSlot, refinement: Option) { + self.0[slot.0] = refinement + } - /// Merges all refinements in the cascade into a single refinement. - /// - /// Refinements are applied in order, with later slots taking precedence. - /// Empty slots (`None`) are skipped during merging. - pub fn merged(&self) -> S::Refinement { - let mut merged = self.0[0].clone().unwrap(); - for refinement in self.0.iter().skip(1).flatten() { - merged.refine(refinement); - } - merged - } + /// Merges all refinements in the cascade into a single refinement. + /// + /// Refinements are applied in order, with later slots taking precedence. + /// Empty slots (`None`) are skipped during merging. + pub fn merged(&self) -> S::Refinement { + let mut merged = self.0[0].clone().unwrap(); + for refinement in self.0.iter().skip(1).flatten() { + merged.refine(refinement); + } + merged + } } diff --git a/crates/gpui_scheduler/src/clock.rs b/crates/gpui_scheduler/src/clock.rs index c015fec153..296190db7c 100644 --- a/crates/gpui_scheduler/src/clock.rs +++ b/crates/gpui_scheduler/src/clock.rs @@ -5,51 +5,51 @@ use std::time::Duration; pub use web_time::Instant; pub trait Clock { - fn utc_now(&self) -> DateTime; - fn now(&self) -> Instant; + fn utc_now(&self) -> DateTime; + fn now(&self) -> Instant; } pub struct TestClock(Mutex); struct TestClockState { - now: Instant, - utc_now: DateTime, + now: Instant, + utc_now: DateTime, } impl TestClock { - pub fn new() -> Self { - const START_TIME: &str = "2025-07-01T23:59:58-00:00"; - let utc_now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc(); - Self(Mutex::new(TestClockState { - now: Instant::now(), - utc_now, - })) - } + pub fn new() -> Self { + const START_TIME: &str = "2025-07-01T23:59:58-00:00"; + let utc_now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc(); + Self(Mutex::new(TestClockState { + now: Instant::now(), + utc_now, + })) + } - pub fn set_utc_now(&self, now: DateTime) { - let mut state = self.0.lock(); - state.utc_now = now; - } + pub fn set_utc_now(&self, now: DateTime) { + let mut state = self.0.lock(); + state.utc_now = now; + } - pub fn advance(&self, duration: Duration) { - let mut state = self.0.lock(); - state.now += duration; - state.utc_now += duration; - } + pub fn advance(&self, duration: Duration) { + let mut state = self.0.lock(); + state.now += duration; + state.utc_now += duration; + } } impl Default for TestClock { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl Clock for TestClock { - fn utc_now(&self) -> DateTime { - self.0.lock().utc_now - } + fn utc_now(&self) -> DateTime { + self.0.lock().utc_now + } - fn now(&self) -> Instant { - self.0.lock().now - } + fn now(&self) -> Instant { + self.0.lock().now + } } diff --git a/crates/gpui_scheduler/src/executor.rs b/crates/gpui_scheduler/src/executor.rs index 93645c4a85..2f051ed50d 100644 --- a/crates/gpui_scheduler/src/executor.rs +++ b/crates/gpui_scheduler/src/executor.rs @@ -1,26 +1,26 @@ use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer}; use async_task::Runnable; use std::{ - any::Any, - future::Future, - marker::PhantomData, - mem::ManuallyDrop, - panic::Location, - pin::Pin, - rc::Rc, - sync::Arc, - task::{Context, Poll}, - thread::{self, ThreadId}, - time::Duration, + any::Any, + future::Future, + marker::PhantomData, + mem::ManuallyDrop, + panic::Location, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + thread::{self, ThreadId}, + time::Duration, }; /// Type-erased closure shape expected by [`Scheduler::spawn_dedicated`]: /// runs on a [`LocalExecutor`], returns a boxed future whose output is itself /// boxed as `Box`. pub type DedicatedFn = Box< - dyn FnOnce(LocalExecutor) -> Pin> + 'static>> - + Send - + 'static, + dyn FnOnce(LocalExecutor) -> Pin> + 'static>> + + Send + + 'static, >; /// A `!Send` executor pinned to a single session. Tasks spawned on it run in @@ -29,135 +29,135 @@ pub type DedicatedFn = Box< /// a dedicated OS thread for sessions created by `spawn_dedicated_thread`. #[derive(Clone)] pub struct LocalExecutor { - session_id: SessionId, - scheduler: Arc, - // Spawned tasks' schedule callbacks each hold an `Arc` clone of this - // closure, so the destination it captures stays alive as long as work - // could still land on it. - dispatch: Arc) + Send + Sync>, - not_send: PhantomData>, + session_id: SessionId, + scheduler: Arc, + // Spawned tasks' schedule callbacks each hold an `Arc` clone of this + // closure, so the destination it captures stays alive as long as work + // could still land on it. + dispatch: Arc) + Send + Sync>, + not_send: PhantomData>, } impl LocalExecutor { - /// Constructs a local executor that runs spawned tasks by sending their - /// runnables through `dispatch`. The `scheduler` is retained for access to - /// clocks, timers, and other scheduler-level services. - /// - /// For the common case of routing runnables through - /// `Scheduler::schedule_local`, callers pass a closure that does exactly - /// that. `spawn_dedicated_thread` instead passes a closure that sends to - /// the dedicated thread's channel. - pub fn new( - session_id: SessionId, - scheduler: Arc, - dispatch: impl Fn(Runnable) + Send + Sync + 'static, - ) -> Self { - Self { - session_id, - scheduler, - dispatch: Arc::new(dispatch), - not_send: PhantomData, - } - } + /// Constructs a local executor that runs spawned tasks by sending their + /// runnables through `dispatch`. The `scheduler` is retained for access to + /// clocks, timers, and other scheduler-level services. + /// + /// For the common case of routing runnables through + /// `Scheduler::schedule_local`, callers pass a closure that does exactly + /// that. `spawn_dedicated_thread` instead passes a closure that sends to + /// the dedicated thread's channel. + pub fn new( + session_id: SessionId, + scheduler: Arc, + dispatch: impl Fn(Runnable) + Send + Sync + 'static, + ) -> Self { + Self { + session_id, + scheduler, + dispatch: Arc::new(dispatch), + not_send: PhantomData, + } + } - pub fn session_id(&self) -> SessionId { - self.session_id - } + pub fn session_id(&self) -> SessionId { + self.session_id + } - pub fn scheduler(&self) -> &Arc { - &self.scheduler - } + pub fn scheduler(&self) -> &Arc { + &self.scheduler + } - #[track_caller] - pub fn spawn(&self, future: F) -> Task - where - F: Future + 'static, - F::Output: 'static, - { - let dispatch = self.dispatch.clone(); - let location = Location::caller(); - let (runnable, task) = spawn_local_with_source_location( - future, - move |runnable| dispatch(runnable), - RunnableMeta { location }, - ); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } + #[track_caller] + pub fn spawn(&self, future: F) -> Task + where + F: Future + 'static, + F::Output: 'static, + { + let dispatch = self.dispatch.clone(); + let location = Location::caller(); + let (runnable, task) = spawn_local_with_source_location( + future, + move |runnable| dispatch(runnable), + RunnableMeta { location }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } - pub fn block_on(&self, future: Fut) -> Fut::Output { - use std::cell::Cell; + pub fn block_on(&self, future: Fut) -> Fut::Output { + use std::cell::Cell; - let output = Cell::new(None); - let future = async { - output.set(Some(future.await)); - }; - let mut future = std::pin::pin!(future); + let output = Cell::new(None); + let future = async { + output.set(Some(future.await)); + }; + let mut future = std::pin::pin!(future); - self.scheduler - .block(Some(self.session_id), future.as_mut(), None); + self.scheduler + .block(Some(self.session_id), future.as_mut(), None); - output.take().expect("block_on future did not complete") - } + output.take().expect("block_on future did not complete") + } - /// Block until the future completes or timeout occurs. - /// Returns Ok(output) if completed, Err(future) if timed out. - pub fn block_with_timeout( - &self, - timeout: Duration, - future: Fut, - ) -> Result + use> { - use std::cell::Cell; + /// Block until the future completes or timeout occurs. + /// Returns Ok(output) if completed, Err(future) if timed out. + pub fn block_with_timeout( + &self, + timeout: Duration, + future: Fut, + ) -> Result + use> { + use std::cell::Cell; - let output = Cell::new(None); - let mut future = Box::pin(future); + let output = Cell::new(None); + let mut future = Box::pin(future); - { - let future_ref = &mut future; - let wrapper = async { - output.set(Some(future_ref.await)); - }; - let mut wrapper = std::pin::pin!(wrapper); + { + let future_ref = &mut future; + let wrapper = async { + output.set(Some(future_ref.await)); + }; + let mut wrapper = std::pin::pin!(wrapper); - self.scheduler - .block(Some(self.session_id), wrapper.as_mut(), Some(timeout)); - } + self.scheduler + .block(Some(self.session_id), wrapper.as_mut(), Some(timeout)); + } - match output.take() { - Some(value) => Ok(value), - None => Err(future), - } - } + match output.take() { + Some(value) => Ok(value), + None => Err(future), + } + } - #[track_caller] - pub fn timer(&self, duration: Duration) -> Timer { - self.scheduler.timer(duration) - } + #[track_caller] + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } - pub fn now(&self) -> Instant { - self.scheduler.clock().now() - } + pub fn now(&self) -> Instant { + self.scheduler.clock().now() + } - /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. - /// The closure runs on a new OS thread under `PlatformScheduler`, or on - /// the test scheduler's loop under `TestScheduler`. - /// - /// The returned `Task` represents the dedicated work: dropping it cancels - /// the dedicated closure, `.await`ing it yields the closure's return - /// value, `.detach()`ing it lets the dedicated work run independently of - /// the caller. - #[track_caller] - pub fn spawn_dedicated(&self, f: F) -> Task - where - F: FnOnce(LocalExecutor) -> Fut + Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + Sync + 'static, - { - self.scheduler - .clone() - .spawn_dedicated(box_dedicated(f)) - .downcast::() - } + /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. + /// The closure runs on a new OS thread under `PlatformScheduler`, or on + /// the test scheduler's loop under `TestScheduler`. + /// + /// The returned `Task` represents the dedicated work: dropping it cancels + /// the dedicated closure, `.await`ing it yields the closure's return + /// value, `.detach()`ing it lets the dedicated work run independently of + /// the caller. + #[track_caller] + pub fn spawn_dedicated(&self, f: F) -> Task + where + F: FnOnce(LocalExecutor) -> Fut + Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + Sync + 'static, + { + self.scheduler + .clone() + .spawn_dedicated(box_dedicated(f)) + .downcast::() + } } /// Boxes the user-supplied dedicated closure into the type-erased shape @@ -166,117 +166,117 @@ impl LocalExecutor { /// back to `Fut::Output` by [`Task::downcast`] in the wrapper. fn box_dedicated(f: F) -> DedicatedFn where - F: FnOnce(LocalExecutor) -> Fut + Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + Sync + 'static, + F: FnOnce(LocalExecutor) -> Fut + Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + Sync + 'static, { - Box::new(move |executor| { - Box::pin(async move { Box::new(f(executor).await) as Box }) - }) + Box::new(move |executor| { + Box::pin(async move { Box::new(f(executor).await) as Box }) + }) } #[derive(Clone)] pub struct BackgroundExecutor { - scheduler: Arc, + scheduler: Arc, } impl BackgroundExecutor { - pub fn new(scheduler: Arc) -> Self { - Self { scheduler } - } + pub fn new(scheduler: Arc) -> Self { + Self { scheduler } + } - #[track_caller] - pub fn spawn(&self, future: F) -> Task - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - self.spawn_with_priority(Priority::default(), future) - } + #[track_caller] + pub fn spawn(&self, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.spawn_with_priority(Priority::default(), future) + } - #[track_caller] - pub fn spawn_with_priority(&self, priority: Priority, future: F) -> Task - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - let scheduler = Arc::downgrade(&self.scheduler); - let location = Location::caller(); - let (runnable, task) = async_task::Builder::new() - .metadata(RunnableMeta { location }) - .spawn( - move |_| future, - move |runnable| { - if let Some(scheduler) = scheduler.upgrade() { - scheduler.schedule_background_with_priority(runnable, priority); - } - }, - ); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } + #[track_caller] + pub fn spawn_with_priority(&self, priority: Priority, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let scheduler = Arc::downgrade(&self.scheduler); + let location = Location::caller(); + let (runnable, task) = async_task::Builder::new() + .metadata(RunnableMeta { location }) + .spawn( + move |_| future, + move |runnable| { + if let Some(scheduler) = scheduler.upgrade() { + scheduler.schedule_background_with_priority(runnable, priority); + } + }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } - /// Spawns a future on a dedicated realtime thread for audio processing. - #[track_caller] - pub fn spawn_realtime(&self, future: F) -> Task - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - let location = Location::caller(); - let (tx, rx) = flume::bounded::>(1); + /// Spawns a future on a dedicated realtime thread for audio processing. + #[track_caller] + pub fn spawn_realtime(&self, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let location = Location::caller(); + let (tx, rx) = flume::bounded::>(1); - self.scheduler.spawn_realtime(Box::new(move || { - while let Ok(runnable) = rx.recv() { - runnable.run(); - } - })); + self.scheduler.spawn_realtime(Box::new(move || { + while let Ok(runnable) = rx.recv() { + runnable.run(); + } + })); - let (runnable, task) = async_task::Builder::new() - .metadata(RunnableMeta { location }) - .spawn( - move |_| future, - move |runnable| { - let _ = tx.send(runnable); - }, - ); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } + let (runnable, task) = async_task::Builder::new() + .metadata(RunnableMeta { location }) + .spawn( + move |_| future, + move |runnable| { + let _ = tx.send(runnable); + }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } - #[track_caller] - pub fn timer(&self, duration: Duration) -> Timer { - self.scheduler.timer(duration) - } + #[track_caller] + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } - pub fn now(&self) -> Instant { - self.scheduler.clock().now() - } + pub fn now(&self) -> Instant { + self.scheduler.clock().now() + } - pub fn scheduler(&self) -> &Arc { - &self.scheduler - } + pub fn scheduler(&self) -> &Arc { + &self.scheduler + } - /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. - /// The closure runs on a new OS thread under `PlatformScheduler`, or on - /// the test scheduler's loop under `TestScheduler`. - /// - /// The returned `Task` represents the dedicated work: dropping it cancels - /// the dedicated closure, `.await`ing it yields the closure's return - /// value, `.detach()`ing it lets the dedicated work run independently of - /// the caller. - #[track_caller] - pub fn spawn_dedicated(&self, f: F) -> Task - where - F: FnOnce(LocalExecutor) -> Fut + Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + Sync + 'static, - { - self.scheduler - .clone() - .spawn_dedicated(box_dedicated(f)) - .downcast::() - } + /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. + /// The closure runs on a new OS thread under `PlatformScheduler`, or on + /// the test scheduler's loop under `TestScheduler`. + /// + /// The returned `Task` represents the dedicated work: dropping it cancels + /// the dedicated closure, `.await`ing it yields the closure's return + /// value, `.detach()`ing it lets the dedicated work run independently of + /// the caller. + #[track_caller] + pub fn spawn_dedicated(&self, f: F) -> Task + where + F: FnOnce(LocalExecutor) -> Fut + Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + Sync + 'static, + { + self.scheduler + .clone() + .spawn_dedicated(box_dedicated(f)) + .downcast::() + } } /// Task is a primitive that allows work to happen in the background. @@ -289,89 +289,89 @@ impl BackgroundExecutor { pub struct Task(TaskState); enum TaskState { - /// A task that is ready to return a value - Ready(Option), + /// A task that is ready to return a value + Ready(Option), - /// A task that is currently running. - Spawned(async_task::Task), + /// A task that is currently running. + Spawned(async_task::Task), - /// A typed view of a [`Task>`] obtained via - /// [`Task::downcast`]. The inner task drives the actual work; the - /// downcast layer just unwraps the `Box` on poll. - Downcast { - inner: Box>>, - marker: PhantomData T>, - }, + /// A typed view of a [`Task>`] obtained via + /// [`Task::downcast`]. The inner task drives the actual work; the + /// downcast layer just unwraps the `Box` on poll. + Downcast { + inner: Box>>, + marker: PhantomData T>, + }, } impl Task { - /// Creates a new task that will resolve with the value - pub fn ready(val: T) -> Self { - Task(TaskState::Ready(Some(val))) - } + /// Creates a new task that will resolve with the value + pub fn ready(val: T) -> Self { + Task(TaskState::Ready(Some(val))) + } - /// Creates a Task from an async_task::Task - pub fn from_async_task(task: async_task::Task) -> Self { - Task(TaskState::Spawned(task)) - } + /// Creates a Task from an async_task::Task + pub fn from_async_task(task: async_task::Task) -> Self { + Task(TaskState::Spawned(task)) + } - pub fn is_ready(&self) -> bool { - match &self.0 { - TaskState::Ready(_) => true, - TaskState::Spawned(task) => task.is_finished(), - TaskState::Downcast { inner, .. } => inner.is_ready(), - } - } + pub fn is_ready(&self) -> bool { + match &self.0 { + TaskState::Ready(_) => true, + TaskState::Spawned(task) => task.is_finished(), + TaskState::Downcast { inner, .. } => inner.is_ready(), + } + } - /// Detaching a task runs it to completion in the background - pub fn detach(self) { - match self { - Task(TaskState::Ready(_)) => {} - Task(TaskState::Spawned(task)) => task.detach(), - Task(TaskState::Downcast { inner, .. }) => inner.detach(), - } - } + /// Detaching a task runs it to completion in the background + pub fn detach(self) { + match self { + Task(TaskState::Ready(_)) => {} + Task(TaskState::Spawned(task)) => task.detach(), + Task(TaskState::Downcast { inner, .. }) => inner.detach(), + } + } - /// Converts this task into a fallible task that returns `Option`. - pub fn fallible(self) -> FallibleTask { - FallibleTask(match self.0 { - TaskState::Ready(val) => FallibleTaskState::Ready(val), - TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()), - TaskState::Downcast { inner, .. } => FallibleTaskState::Downcast { - inner: Box::new(inner.fallible()), - marker: PhantomData, - }, - }) - } + /// Converts this task into a fallible task that returns `Option`. + pub fn fallible(self) -> FallibleTask { + FallibleTask(match self.0 { + TaskState::Ready(val) => FallibleTaskState::Ready(val), + TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()), + TaskState::Downcast { inner, .. } => FallibleTaskState::Downcast { + inner: Box::new(inner.fallible()), + marker: PhantomData, + }, + }) + } } impl Task> { - /// Reinterprets the boxed output as a concrete `T` via downcast on - /// completion. Used by [`LocalExecutor::spawn_dedicated`] and - /// [`BackgroundExecutor::spawn_dedicated`] to recover the user closure's - /// `Fut::Output` from the dyn-safe [`Scheduler::spawn_dedicated`]. - /// - /// Panics on poll if the inner output is not in fact a `T` -- a logic - /// error in whatever produced the inner task, since the downcast type is - /// chosen by the caller of `downcast`. - pub fn downcast(self) -> Task { - Task(TaskState::Downcast { - inner: Box::new(self), - marker: PhantomData, - }) - } + /// Reinterprets the boxed output as a concrete `T` via downcast on + /// completion. Used by [`LocalExecutor::spawn_dedicated`] and + /// [`BackgroundExecutor::spawn_dedicated`] to recover the user closure's + /// `Fut::Output` from the dyn-safe [`Scheduler::spawn_dedicated`]. + /// + /// Panics on poll if the inner output is not in fact a `T` -- a logic + /// error in whatever produced the inner task, since the downcast type is + /// chosen by the caller of `downcast`. + pub fn downcast(self) -> Task { + Task(TaskState::Downcast { + inner: Box::new(self), + marker: PhantomData, + }) + } } impl std::fmt::Debug for Task { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - TaskState::Ready(_) => f.debug_tuple("Task::Ready").finish(), - TaskState::Spawned(task) => f.debug_tuple("Task::Spawned").field(task).finish(), - TaskState::Downcast { inner, .. } => { - f.debug_tuple("Task::Downcast").field(inner).finish() - } - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + TaskState::Ready(_) => f.debug_tuple("Task::Ready").finish(), + TaskState::Spawned(task) => f.debug_tuple("Task::Spawned").field(task).finish(), + TaskState::Downcast { inner, .. } => { + f.debug_tuple("Task::Downcast").field(inner).finish() + } + } + } } /// A task that returns `Option` instead of panicking when cancelled. @@ -379,168 +379,168 @@ impl std::fmt::Debug for Task { pub struct FallibleTask(FallibleTaskState); enum FallibleTaskState { - /// A task that is ready to return a value - Ready(Option), + /// A task that is ready to return a value + Ready(Option), - /// A task that is currently running (wraps async_task::FallibleTask). - Spawned(async_task::FallibleTask), + /// A task that is currently running (wraps async_task::FallibleTask). + Spawned(async_task::FallibleTask), - /// Mirror of [`TaskState::Downcast`] for fallible tasks. - Downcast { - inner: Box>>, - marker: PhantomData T>, - }, + /// Mirror of [`TaskState::Downcast`] for fallible tasks. + Downcast { + inner: Box>>, + marker: PhantomData T>, + }, } impl FallibleTask { - /// Creates a new fallible task that will resolve with the value. - pub fn ready(val: T) -> Self { - FallibleTask(FallibleTaskState::Ready(Some(val))) - } + /// Creates a new fallible task that will resolve with the value. + pub fn ready(val: T) -> Self { + FallibleTask(FallibleTaskState::Ready(Some(val))) + } - /// Detaching a task runs it to completion in the background. - pub fn detach(self) { - match self.0 { - FallibleTaskState::Ready(_) => {} - FallibleTaskState::Spawned(task) => task.detach(), - FallibleTaskState::Downcast { inner, .. } => inner.detach(), - } - } + /// Detaching a task runs it to completion in the background. + pub fn detach(self) { + match self.0 { + FallibleTaskState::Ready(_) => {} + FallibleTaskState::Spawned(task) => task.detach(), + FallibleTaskState::Downcast { inner, .. } => inner.detach(), + } + } } impl Future for FallibleTask { - type Output = Option; + type Output = Option; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - match unsafe { self.get_unchecked_mut() } { - FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()), - FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx), - FallibleTask(FallibleTaskState::Downcast { inner, .. }) => { - match Pin::new(inner.as_mut()).poll(cx) { - Poll::Ready(Some(boxed_any)) => Poll::Ready(Some( - *boxed_any - .downcast::() - .expect("FallibleTask::poll: downcast type mismatch"), - )), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()), + FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx), + FallibleTask(FallibleTaskState::Downcast { inner, .. }) => { + match Pin::new(inner.as_mut()).poll(cx) { + Poll::Ready(Some(boxed_any)) => Poll::Ready(Some( + *boxed_any + .downcast::() + .expect("FallibleTask::poll: downcast type mismatch"), + )), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } + } + } } impl std::fmt::Debug for FallibleTask { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(), - FallibleTaskState::Spawned(task) => { - f.debug_tuple("FallibleTask::Spawned").field(task).finish() - } - FallibleTaskState::Downcast { inner, .. } => f - .debug_tuple("FallibleTask::Downcast") - .field(inner) - .finish(), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(), + FallibleTaskState::Spawned(task) => { + f.debug_tuple("FallibleTask::Spawned").field(task).finish() + } + FallibleTaskState::Downcast { inner, .. } => f + .debug_tuple("FallibleTask::Downcast") + .field(inner) + .finish(), + } + } } impl Future for Task { - type Output = T; + type Output = T; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - match unsafe { self.get_unchecked_mut() } { - Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), - Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx), - Task(TaskState::Downcast { inner, .. }) => match Pin::new(inner.as_mut()).poll(cx) { - Poll::Ready(boxed_any) => Poll::Ready( - *boxed_any - .downcast::() - .expect("Task::poll: downcast type mismatch"), - ), - Poll::Pending => Poll::Pending, - }, - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), + Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx), + Task(TaskState::Downcast { inner, .. }) => match Pin::new(inner.as_mut()).poll(cx) { + Poll::Ready(boxed_any) => Poll::Ready( + *boxed_any + .downcast::() + .expect("Task::poll: downcast type mismatch"), + ), + Poll::Pending => Poll::Pending, + }, + } + } } /// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics. #[track_caller] fn spawn_local_with_source_location( - future: Fut, - schedule: S, - metadata: RunnableMeta, + future: Fut, + schedule: S, + metadata: RunnableMeta, ) -> ( - async_task::Runnable, - async_task::Task, + async_task::Runnable, + async_task::Task, ) where - Fut: Future + 'static, - Fut::Output: 'static, - S: async_task::Schedule + Send + Sync + 'static, + Fut: Future + 'static, + Fut::Output: 'static, + S: async_task::Schedule + Send + Sync + 'static, { - #[inline] - fn thread_id() -> ThreadId { - std::thread_local! { - static ID: ThreadId = thread::current().id(); - } - ID.try_with(|id| *id) - .unwrap_or_else(|_| thread::current().id()) - } + #[inline] + fn thread_id() -> ThreadId { + std::thread_local! { + static ID: ThreadId = thread::current().id(); + } + ID.try_with(|id| *id) + .unwrap_or_else(|_| thread::current().id()) + } - struct Checked { - id: ThreadId, - inner: ManuallyDrop, - location: &'static Location<'static>, - } + struct Checked { + id: ThreadId, + inner: ManuallyDrop, + location: &'static Location<'static>, + } - impl Drop for Checked { - fn drop(&mut self) { - assert_eq!( - self.id, - thread_id(), - "local task dropped by a thread that didn't spawn it. Task spawned at {}", - self.location - ); - // SAFETY: `inner` is wrapped in `ManuallyDrop`, so this is the only - // place it is dropped. The thread check above ensures local futures - // are dropped on the thread that created them. - unsafe { ManuallyDrop::drop(&mut self.inner) }; - } - } + impl Drop for Checked { + fn drop(&mut self) { + assert_eq!( + self.id, + thread_id(), + "local task dropped by a thread that didn't spawn it. Task spawned at {}", + self.location + ); + // SAFETY: `inner` is wrapped in `ManuallyDrop`, so this is the only + // place it is dropped. The thread check above ensures local futures + // are dropped on the thread that created them. + unsafe { ManuallyDrop::drop(&mut self.inner) }; + } + } - impl Future for Checked { - type Output = F::Output; + impl Future for Checked { + type Output = F::Output; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We don't move any fields out of `self`; this mutable - // reference is only used to check metadata and to project the pin to - // `inner` below. - let this = unsafe { self.get_unchecked_mut() }; - assert!( - this.id == thread_id(), - "local task polled by a thread that didn't spawn it. Task spawned at {}", - this.location - ); - // SAFETY: `inner` is structurally pinned by `Checked`; after - // `Checked` is pinned, `inner` is never moved. The thread check - // above ensures the local future is only polled by its spawning - // thread. - unsafe { Pin::new_unchecked(&mut *this.inner).poll(cx) } - } - } + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // SAFETY: We don't move any fields out of `self`; this mutable + // reference is only used to check metadata and to project the pin to + // `inner` below. + let this = unsafe { self.get_unchecked_mut() }; + assert!( + this.id == thread_id(), + "local task polled by a thread that didn't spawn it. Task spawned at {}", + this.location + ); + // SAFETY: `inner` is structurally pinned by `Checked`; after + // `Checked` is pinned, `inner` is never moved. The thread check + // above ensures the local future is only polled by its spawning + // thread. + unsafe { Pin::new_unchecked(&mut *this.inner).poll(cx) } + } + } - let location = metadata.location; + let location = metadata.location; - let future = move |_| Checked { - id: thread_id(), - inner: ManuallyDrop::new(future), - location, - }; + let future = move |_| Checked { + id: thread_id(), + inner: ManuallyDrop::new(future), + location, + }; - let builder = async_task::Builder::new().metadata(metadata); - // SAFETY: `Checked` enforces the invariants required by `spawn_unchecked`: - // the non-`Send` future is only polled and dropped on the thread that - // spawned it. - unsafe { builder.spawn_unchecked(future, schedule) } + let builder = async_task::Builder::new().metadata(metadata); + // SAFETY: `Checked` enforces the invariants required by `spawn_unchecked`: + // the non-`Send` future is only polled and dropped on the thread that + // spawned it. + unsafe { builder.spawn_unchecked(future, schedule) } } diff --git a/crates/gpui_scheduler/src/scheduler.rs b/crates/gpui_scheduler/src/scheduler.rs index 402d49ea18..cf5c17603b 100644 --- a/crates/gpui_scheduler/src/scheduler.rs +++ b/crates/gpui_scheduler/src/scheduler.rs @@ -11,14 +11,14 @@ pub use test_scheduler::*; use async_task::Runnable; use futures::channel::oneshot; use std::{ - any::Any, - future::Future, - panic::Location, - pin::Pin, - sync::Arc, - task::{Context, Poll}, - thread, - time::Duration, + any::Any, + future::Future, + panic::Location, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + thread, + time::Duration, }; /// Task priority for background tasks. @@ -29,104 +29,104 @@ use std::{ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[repr(u8)] pub enum Priority { - /// Realtime priority - /// - /// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. Only use for audio. - RealtimeAudio, - /// High priority - use for tasks critical to user experience/responsiveness. - High, - /// Medium priority - suitable for most use cases. - #[default] - Medium, - /// Low priority - use for background work that can be deprioritized. - Low, + /// Realtime priority + /// + /// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. Only use for audio. + RealtimeAudio, + /// High priority - use for tasks critical to user experience/responsiveness. + High, + /// Medium priority - suitable for most use cases. + #[default] + Medium, + /// Low priority - use for background work that can be deprioritized. + Low, } impl Priority { - /// Returns the relative probability weight for this priority level. - /// Used by schedulers to determine task selection probability. - pub const fn weight(self) -> u32 { - match self { - Priority::High => 60, - Priority::Medium => 30, - Priority::Low => 10, - // realtime priorities are not considered for probability scheduling - Priority::RealtimeAudio => 0, - } - } + /// Returns the relative probability weight for this priority level. + /// Used by schedulers to determine task selection probability. + pub const fn weight(self) -> u32 { + match self { + Priority::High => 60, + Priority::Medium => 30, + Priority::Low => 10, + // realtime priorities are not considered for probability scheduling + Priority::RealtimeAudio => 0, + } + } } /// Metadata attached to runnables for debugging and profiling. #[derive(Clone)] pub struct RunnableMeta { - /// The source location where the task was spawned. - pub location: &'static Location<'static>, + /// The source location where the task was spawned. + pub location: &'static Location<'static>, } impl std::fmt::Debug for RunnableMeta { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RunnableMeta") - .field("location", &self.location) - .finish() - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RunnableMeta") + .field("location", &self.location) + .finish() + } } pub trait Scheduler: Send + Sync { - /// Block until the given future completes or timeout occurs. - /// - /// Returns `true` if the future completed, `false` if it timed out. - /// The future is passed as a pinned mutable reference so the caller - /// retains ownership and can continue polling or return it on timeout. - fn block( - &self, - session_id: Option, - future: Pin<&mut dyn Future>, - timeout: Option, - ) -> bool; + /// Block until the given future completes or timeout occurs. + /// + /// Returns `true` if the future completed, `false` if it timed out. + /// The future is passed as a pinned mutable reference so the caller + /// retains ownership and can continue polling or return it on timeout. + fn block( + &self, + session_id: Option, + future: Pin<&mut dyn Future>, + timeout: Option, + ) -> bool; - /// Schedule a runnable on the local (session-pinned) queue for `session_id`. - /// Runnables scheduled here run in order on whichever thread drains the - /// session — the main thread for ordinary sessions, or a dedicated OS - /// thread for sessions created via `spawn_dedicated_thread`. - fn schedule_local(&self, session_id: SessionId, runnable: Runnable); + /// Schedule a runnable on the local (session-pinned) queue for `session_id`. + /// Runnables scheduled here run in order on whichever thread drains the + /// session — the main thread for ordinary sessions, or a dedicated OS + /// thread for sessions created via `spawn_dedicated_thread`. + fn schedule_local(&self, session_id: SessionId, runnable: Runnable); - /// Schedule a background task with the given priority. - fn schedule_background_with_priority( - &self, - runnable: Runnable, - priority: Priority, - ); + /// Schedule a background task with the given priority. + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ); - /// Spawn a closure on a dedicated realtime thread for audio processing. - fn spawn_realtime(&self, f: Box); + /// Spawn a closure on a dedicated realtime thread for audio processing. + fn spawn_realtime(&self, f: Box); - /// Schedule a background task with default (medium) priority. - fn schedule_background(&self, runnable: Runnable) { - self.schedule_background_with_priority(runnable, Priority::default()); - } + /// Schedule a background task with default (medium) priority. + fn schedule_background(&self, runnable: Runnable) { + self.schedule_background_with_priority(runnable, Priority::default()); + } - #[track_caller] - fn timer(&self, timeout: Duration) -> Timer; - fn clock(&self) -> Arc; + #[track_caller] + fn timer(&self, timeout: Duration) -> Timer; + fn clock(&self) -> Arc; - /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. - /// - /// `PlatformScheduler` runs the closure on a new OS thread (see - /// [`spawn_dedicated_thread`]). `TestScheduler` runs it on the test - /// scheduler's loop alongside everything else so determinism under - /// `TestScheduler::many` is preserved. - /// - /// This is the dyn-safe entry point: the closure's output is type-erased - /// as `Box` so the trait stays object-safe. - /// Callers typically reach for the type-safe wrappers on - /// [`LocalExecutor::spawn_dedicated`] and - /// [`BackgroundExecutor::spawn_dedicated`], which compose this method - /// with [`Task::downcast`] to recover the closure's concrete return type. - fn spawn_dedicated(self: Arc, f: DedicatedFn) -> Task>; + /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`]. + /// + /// `PlatformScheduler` runs the closure on a new OS thread (see + /// [`spawn_dedicated_thread`]). `TestScheduler` runs it on the test + /// scheduler's loop alongside everything else so determinism under + /// `TestScheduler::many` is preserved. + /// + /// This is the dyn-safe entry point: the closure's output is type-erased + /// as `Box` so the trait stays object-safe. + /// Callers typically reach for the type-safe wrappers on + /// [`LocalExecutor::spawn_dedicated`] and + /// [`BackgroundExecutor::spawn_dedicated`], which compose this method + /// with [`Task::downcast`] to recover the closure's concrete return type. + fn spawn_dedicated(self: Arc, f: DedicatedFn) -> Task>; - fn as_test(&self) -> Option<&TestScheduler> { - None - } + fn as_test(&self) -> Option<&TestScheduler> { + None + } } /// Spawn work on a fresh OS thread that's exclusive to the returned task and @@ -143,67 +143,67 @@ pub trait Scheduler: Send + Sync { /// every other live session on `scheduler`. Concrete schedulers typically wrap /// this in an inherent method that allocates the id from their own counter. pub fn spawn_dedicated_thread( - session_id: SessionId, - scheduler: Arc, - f: F, + session_id: SessionId, + scheduler: Arc, + f: F, ) -> Task where - F: FnOnce(LocalExecutor) -> Fut + Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, + F: FnOnce(LocalExecutor) -> Fut + Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, { - let (runnable_sender, runnable_receiver) = flume::unbounded::>(); - let (task_sender, task_receiver) = flume::bounded::>(1); + let (runnable_sender, runnable_receiver) = flume::unbounded::>(); + let (task_sender, task_receiver) = flume::bounded::>(1); - thread::Builder::new() - .name(format!("spawn_dedicated session {:?}", session_id)) - .spawn(move || { - let dispatch = move |runnable: Runnable| { - let _ = runnable_sender.send(runnable); - }; - let executor = LocalExecutor::new(session_id, scheduler, dispatch); - let root_task = executor.spawn(f(executor.clone())); - let _ = task_sender.send(root_task); - // After this drop, every strong reference to the runnable sender - // lives inside a spawned task or a user-held executor clone. The - // recv loop exits once all of those are gone. - drop(executor); + thread::Builder::new() + .name(format!("spawn_dedicated session {:?}", session_id)) + .spawn(move || { + let dispatch = move |runnable: Runnable| { + let _ = runnable_sender.send(runnable); + }; + let executor = LocalExecutor::new(session_id, scheduler, dispatch); + let root_task = executor.spawn(f(executor.clone())); + let _ = task_sender.send(root_task); + // After this drop, every strong reference to the runnable sender + // lives inside a spawned task or a user-held executor clone. The + // recv loop exits once all of those are gone. + drop(executor); - while let Ok(runnable) = runnable_receiver.recv() { - runnable.run(); - } - }) - .expect("failed to spawn dedicated thread"); + while let Ok(runnable) = runnable_receiver.recv() { + runnable.run(); + } + }) + .expect("failed to spawn dedicated thread"); - task_receiver - .recv() - .expect("dedicated thread failed to produce root task") + task_receiver + .recv() + .expect("dedicated thread failed to produce root task") } #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct SessionId(u16); impl SessionId { - pub fn new(id: u16) -> Self { - SessionId(id) - } + pub fn new(id: u16) -> Self { + SessionId(id) + } } pub struct Timer(oneshot::Receiver<()>); impl Timer { - pub fn new(rx: oneshot::Receiver<()>) -> Self { - Timer(rx) - } + pub fn new(rx: oneshot::Receiver<()>) -> Self { + Timer(rx) + } } impl Future for Timer { - type Output = (); + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { - match Pin::new(&mut self.0).poll(cx) { - Poll::Ready(_) => Poll::Ready(()), - Poll::Pending => Poll::Pending, - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { + match Pin::new(&mut self.0).poll(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } } diff --git a/crates/gpui_scheduler/src/test_scheduler.rs b/crates/gpui_scheduler/src/test_scheduler.rs index 600de2439a..5843ad9411 100644 --- a/crates/gpui_scheduler/src/test_scheduler.rs +++ b/crates/gpui_scheduler/src/test_scheduler.rs @@ -1,854 +1,853 @@ use crate::{ - BackgroundExecutor, Clock, Instant, LocalExecutor, Priority, RunnableMeta, Scheduler, - SessionId, Task, TestClock, Timer, + BackgroundExecutor, Clock, Instant, LocalExecutor, Priority, RunnableMeta, Scheduler, + SessionId, Task, TestClock, Timer, }; use async_task::Runnable; use backtrace::{Backtrace, BacktraceFrame}; use futures::channel::oneshot; use parking_lot::{Mutex, MutexGuard}; use rand::{ - distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform}, - prelude::*, + distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform}, + prelude::*, }; use std::any::Any; use std::{ - any::type_name_of_val, - collections::{BTreeMap, HashSet, VecDeque}, - env, - fmt::Write, - future::Future, - mem, - ops::RangeInclusive, - panic::{self, AssertUnwindSafe}, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, - }, - task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, - thread::{self, Thread}, - time::Duration, + any::type_name_of_val, + collections::{BTreeMap, HashSet, VecDeque}, + env, + fmt::Write, + future::Future, + mem, + ops::RangeInclusive, + panic::{self, AssertUnwindSafe}, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicBool, Ordering::SeqCst}, + }, + task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, + thread::{self, Thread}, + time::Duration, }; const PENDING_TRACES_VAR_NAME: &str = "PENDING_TRACES"; pub struct TestScheduler { - clock: Arc, - rng: Arc>, - state: Arc>, - thread: Thread, + clock: Arc, + rng: Arc>, + state: Arc>, + thread: Thread, } impl TestScheduler { - /// Run a test once with default configuration (seed 0) - pub fn once(f: impl AsyncFnOnce(Arc) -> R) -> R { - Self::with_seed(0, f) - } + /// Run a test once with default configuration (seed 0) + pub fn once(f: impl AsyncFnOnce(Arc) -> R) -> R { + Self::with_seed(0, f) + } - /// Run a test multiple times with sequential seeds (0, 1, 2, ...) - pub fn many( - default_iterations: usize, - mut f: impl AsyncFnMut(Arc) -> R, - ) -> Vec { - let num_iterations = std::env::var("ITERATIONS") - .map(|iterations| iterations.parse().unwrap()) - .unwrap_or(default_iterations); + /// Run a test multiple times with sequential seeds (0, 1, 2, ...) + pub fn many( + default_iterations: usize, + mut f: impl AsyncFnMut(Arc) -> R, + ) -> Vec { + let num_iterations = std::env::var("ITERATIONS") + .map(|iterations| iterations.parse().unwrap()) + .unwrap_or(default_iterations); - let seed = std::env::var("SEED") - .map(|seed| seed.parse().unwrap()) - .unwrap_or(0); + let seed = std::env::var("SEED") + .map(|seed| seed.parse().unwrap()) + .unwrap_or(0); - let interactive = std::env::var("SCHEDULER_NONINTERACTIVE").is_err(); + let interactive = std::env::var("SCHEDULER_NONINTERACTIVE").is_err(); - (seed..seed + num_iterations as u64) - .map(|seed| { - let mut unwind_safe_f = AssertUnwindSafe(&mut f); - if interactive { - eprintln!("Running seed: {seed}"); - } - match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) { - Ok(result) => result, - Err(error) => { - eprintln!("\x1b[31mFailing Seed: {seed}\x1b[0m"); - panic::resume_unwind(error); - } - } - }) - .collect() - } + (seed..seed + num_iterations as u64) + .map(|seed| { + let mut unwind_safe_f = AssertUnwindSafe(&mut f); + if interactive { + eprintln!("Running seed: {seed}"); + } + match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) { + Ok(result) => result, + Err(error) => { + eprintln!("\x1b[31mFailing Seed: {seed}\x1b[0m"); + panic::resume_unwind(error); + } + } + }) + .collect() + } - fn with_seed(seed: u64, f: impl AsyncFnOnce(Arc) -> R) -> R { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed))); - let future = f(scheduler.clone()); - let result = scheduler.foreground().block_on(future); - scheduler.run(); // Ensure spawned tasks finish up before returning in tests - result - } + fn with_seed(seed: u64, f: impl AsyncFnOnce(Arc) -> R) -> R { + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed))); + let future = f(scheduler.clone()); + let result = scheduler.foreground().block_on(future); + scheduler.run(); // Ensure spawned tasks finish up before returning in tests + result + } - pub fn new(config: TestSchedulerConfig) -> Self { - Self { - rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))), - state: Arc::new(Mutex::new(SchedulerState { - runnables: VecDeque::new(), - timers: Vec::new(), - blocked_sessions: Vec::new(), - randomize_order: config.randomize_order, - allow_parking: config.allow_parking, - timeout_ticks: config.timeout_ticks, - next_session_id: SessionId(0), - capture_pending_traces: config.capture_pending_traces, - pending_traces: BTreeMap::new(), - next_trace_id: TraceId(0), - is_main_thread: true, - non_determinism_error: None, - finished: false, - parking_allowed_once: false, - unparked: false, - })), - clock: Arc::new(TestClock::new()), - thread: thread::current(), - } - } + pub fn new(config: TestSchedulerConfig) -> Self { + Self { + rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))), + state: Arc::new(Mutex::new(SchedulerState { + runnables: VecDeque::new(), + timers: Vec::new(), + blocked_sessions: Vec::new(), + randomize_order: config.randomize_order, + allow_parking: config.allow_parking, + timeout_ticks: config.timeout_ticks, + next_session_id: SessionId(0), + capture_pending_traces: config.capture_pending_traces, + pending_traces: BTreeMap::new(), + next_trace_id: TraceId(0), + is_main_thread: true, + non_determinism_error: None, + finished: false, + parking_allowed_once: false, + unparked: false, + })), + clock: Arc::new(TestClock::new()), + thread: thread::current(), + } + } - pub fn end_test(&self) { - let mut state = self.state.lock(); - if let Some((message, backtrace)) = &state.non_determinism_error { - if cfg!(miri) { - // miri cannot debug print backtraces with `miri-disable-isolation` enabled - panic!("{}", message) - } else { - panic!("{}\n{:?}", message, backtrace) - } - } - state.finished = true; - } + pub fn end_test(&self) { + let mut state = self.state.lock(); + if let Some((message, backtrace)) = &state.non_determinism_error { + if cfg!(miri) { + // miri cannot debug print backtraces with `miri-disable-isolation` enabled + panic!("{}", message) + } else { + panic!("{}\n{:?}", message, backtrace) + } + } + state.finished = true; + } - pub fn clock(&self) -> Arc { - self.clock.clone() - } + pub fn clock(&self) -> Arc { + self.clock.clone() + } - pub fn rng(&self) -> SharedRng { - SharedRng(self.rng.clone()) - } + pub fn rng(&self) -> SharedRng { + SharedRng(self.rng.clone()) + } - pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive) { - self.state.lock().timeout_ticks = timeout_ticks; - } + pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive) { + self.state.lock().timeout_ticks = timeout_ticks; + } - pub fn allow_parking(&self) { - let mut state = self.state.lock(); - state.allow_parking = true; - state.parking_allowed_once = true; - } + pub fn allow_parking(&self) { + let mut state = self.state.lock(); + state.allow_parking = true; + state.parking_allowed_once = true; + } - pub fn forbid_parking(&self) { - self.state.lock().allow_parking = false; - } + pub fn forbid_parking(&self) { + self.state.lock().allow_parking = false; + } - pub fn parking_allowed(&self) -> bool { - self.state.lock().allow_parking - } + pub fn parking_allowed(&self) -> bool { + self.state.lock().allow_parking + } - pub fn is_main_thread(&self) -> bool { - self.state.lock().is_main_thread - } + pub fn is_main_thread(&self) -> bool { + self.state.lock().is_main_thread + } - pub fn allocate_session_id(&self) -> SessionId { - let mut state = self.state.lock(); - state.next_session_id.0 += 1; - state.next_session_id - } + pub fn allocate_session_id(&self) -> SessionId { + let mut state = self.state.lock(); + state.next_session_id.0 += 1; + state.next_session_id + } - /// Create a local executor for this scheduler. - pub fn foreground(self: &Arc) -> LocalExecutor { - let session_id = self.allocate_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); - } - }) - } + /// Create a local executor for this scheduler. + pub fn foreground(self: &Arc) -> LocalExecutor { + let session_id = self.allocate_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); + } + }) + } - /// Create a background executor for this scheduler - pub fn background(self: &Arc) -> BackgroundExecutor { - BackgroundExecutor::new(self.clone()) - } + /// Create a background executor for this scheduler + pub fn background(self: &Arc) -> BackgroundExecutor { + BackgroundExecutor::new(self.clone()) + } - pub fn yield_random(&self) -> Yield { - let rng = &mut *self.rng.lock(); - if rng.random_bool(0.1) { - Yield(rng.random_range(10..20)) - } else { - Yield(rng.random_range(0..2)) - } - } + pub fn yield_random(&self) -> Yield { + let rng = &mut *self.rng.lock(); + if rng.random_bool(0.1) { + Yield(rng.random_range(10..20)) + } else { + Yield(rng.random_range(0..2)) + } + } - pub fn run(&self) { - while self.step() { - // Continue until no work remains - } - } + pub fn run(&self) { + while self.step() { + // Continue until no work remains + } + } - pub fn run_with_clock_advancement(&self) { - while self.step() || self.advance_clock_to_next_timer() { - // Continue until no work remains - } - } + pub fn run_with_clock_advancement(&self) { + while self.step() || self.advance_clock_to_next_timer() { + // Continue until no work remains + } + } - /// Execute one tick of the scheduler, processing expired timers and running - /// at most one task. Returns true if any work was done. - /// - /// This is the public interface for GPUI's TestDispatcher to drive task execution. - pub fn tick(&self) -> bool { - self.step_filtered(false) - } + /// Execute one tick of the scheduler, processing expired timers and running + /// at most one task. Returns true if any work was done. + /// + /// This is the public interface for GPUI's TestDispatcher to drive task execution. + pub fn tick(&self) -> bool { + self.step_filtered(false) + } - /// Execute one tick, but only run background tasks (no foreground/session tasks). - /// Returns true if any work was done. - pub fn tick_background_only(&self) -> bool { - self.step_filtered(true) - } + /// Execute one tick, but only run background tasks (no foreground/session tasks). + /// Returns true if any work was done. + pub fn tick_background_only(&self) -> bool { + self.step_filtered(true) + } - /// Check if there are any pending tasks or timers that could run. - pub fn has_pending_tasks(&self) -> bool { - let state = self.state.lock(); - !state.runnables.is_empty() || !state.timers.is_empty() - } + /// Check if there are any pending tasks or timers that could run. + pub fn has_pending_tasks(&self) -> bool { + let state = self.state.lock(); + !state.runnables.is_empty() || !state.timers.is_empty() + } - /// Returns counts of (foreground_tasks, background_tasks) currently queued. - /// Foreground tasks are those with a session_id, background tasks have none. - pub fn pending_task_counts(&self) -> (usize, usize) { - let state = self.state.lock(); - let foreground = state - .runnables - .iter() - .filter(|r| r.session_id.is_some()) - .count(); - let background = state - .runnables - .iter() - .filter(|r| r.session_id.is_none()) - .count(); - (foreground, background) - } + /// Returns counts of (foreground_tasks, background_tasks) currently queued. + /// Foreground tasks are those with a session_id, background tasks have none. + pub fn pending_task_counts(&self) -> (usize, usize) { + let state = self.state.lock(); + let foreground = state + .runnables + .iter() + .filter(|r| r.session_id.is_some()) + .count(); + let background = state + .runnables + .iter() + .filter(|r| r.session_id.is_none()) + .count(); + (foreground, background) + } - fn step(&self) -> bool { - self.step_filtered(false) - } + fn step(&self) -> bool { + self.step_filtered(false) + } - fn step_filtered(&self, background_only: bool) -> bool { - let (elapsed_count, runnables_before) = { - let mut state = self.state.lock(); - let end_ix = state - .timers - .partition_point(|timer| timer.expiration <= self.clock.now()); - let elapsed: Vec<_> = state.timers.drain(..end_ix).collect(); - let count = elapsed.len(); - let runnables = state.runnables.len(); - drop(state); - // Dropping elapsed timers here wakes the waiting futures - drop(elapsed); - (count, runnables) - }; + fn step_filtered(&self, background_only: bool) -> bool { + let (elapsed_count, runnables_before) = { + let mut state = self.state.lock(); + let end_ix = state + .timers + .partition_point(|timer| timer.expiration <= self.clock.now()); + let elapsed: Vec<_> = state.timers.drain(..end_ix).collect(); + let count = elapsed.len(); + let runnables = state.runnables.len(); + drop(state); + // Dropping elapsed timers here wakes the waiting futures + drop(elapsed); + (count, runnables) + }; - if elapsed_count > 0 { - let runnables_after = self.state.lock().runnables.len(); - if std::env::var("DEBUG_SCHEDULER").is_ok() { - eprintln!( - "[scheduler] Expired {} timers at {:?}, runnables: {} -> {}", - elapsed_count, - self.clock.now(), - runnables_before, - runnables_after - ); - } - return true; - } + if elapsed_count > 0 { + let runnables_after = self.state.lock().runnables.len(); + if std::env::var("DEBUG_SCHEDULER").is_ok() { + eprintln!( + "[scheduler] Expired {} timers at {:?}, runnables: {} -> {}", + elapsed_count, + self.clock.now(), + runnables_before, + runnables_after + ); + } + return true; + } - let runnable = { - let state = &mut *self.state.lock(); + let runnable = { + let state = &mut *self.state.lock(); - // Find candidate tasks: - // - For foreground tasks (with session_id), only the first task from each session - // is a candidate (to preserve intra-session ordering) - // - For background tasks (no session_id), all are candidates - // - Tasks from blocked sessions are excluded - // - If background_only is true, skip foreground tasks entirely - let mut seen_sessions = HashSet::new(); - let candidate_indices: Vec = state - .runnables - .iter() - .enumerate() - .filter(|(_, runnable)| { - if let Some(session_id) = runnable.session_id { - // Skip foreground tasks if background_only mode - if background_only { - return false; - } - // Exclude tasks from blocked sessions - if state.blocked_sessions.contains(&session_id) { - return false; - } - // Only include first task from each session (insert returns true if new) - seen_sessions.insert(session_id) - } else { - // Background tasks are always candidates - true - } - }) - .map(|(ix, _)| ix) - .collect(); + // Find candidate tasks: + // - For foreground tasks (with session_id), only the first task from each session + // is a candidate (to preserve intra-session ordering) + // - For background tasks (no session_id), all are candidates + // - Tasks from blocked sessions are excluded + // - If background_only is true, skip foreground tasks entirely + let mut seen_sessions = HashSet::new(); + let candidate_indices: Vec = state + .runnables + .iter() + .enumerate() + .filter(|(_, runnable)| { + if let Some(session_id) = runnable.session_id { + // Skip foreground tasks if background_only mode + if background_only { + return false; + } + // Exclude tasks from blocked sessions + if state.blocked_sessions.contains(&session_id) { + return false; + } + // Only include first task from each session (insert returns true if new) + seen_sessions.insert(session_id) + } else { + // Background tasks are always candidates + true + } + }) + .map(|(ix, _)| ix) + .collect(); - if candidate_indices.is_empty() { - None - } else if state.randomize_order { - // Use priority-weighted random selection - let weights: Vec = candidate_indices - .iter() - .map(|&ix| state.runnables[ix].priority.weight()) - .collect(); - let total_weight: u32 = weights.iter().sum(); + if candidate_indices.is_empty() { + None + } else if state.randomize_order { + // Use priority-weighted random selection + let weights: Vec = candidate_indices + .iter() + .map(|&ix| state.runnables[ix].priority.weight()) + .collect(); + let total_weight: u32 = weights.iter().sum(); - if total_weight == 0 { - // Fallback to uniform random if all weights are zero - let choice = self.rng.lock().random_range(0..candidate_indices.len()); - state.runnables.remove(candidate_indices[choice]) - } else { - let mut target = self.rng.lock().random_range(0..total_weight); - let mut selected_idx = 0; - for (i, &weight) in weights.iter().enumerate() { - if target < weight { - selected_idx = i; - break; - } - target -= weight; - } - state.runnables.remove(candidate_indices[selected_idx]) - } - } else { - // Non-randomized: just take the first candidate task - state.runnables.remove(candidate_indices[0]) - } - }; + if total_weight == 0 { + // Fallback to uniform random if all weights are zero + let choice = self.rng.lock().random_range(0..candidate_indices.len()); + state.runnables.remove(candidate_indices[choice]) + } else { + let mut target = self.rng.lock().random_range(0..total_weight); + let mut selected_idx = 0; + for (i, &weight) in weights.iter().enumerate() { + if target < weight { + selected_idx = i; + break; + } + target -= weight; + } + state.runnables.remove(candidate_indices[selected_idx]) + } + } else { + // Non-randomized: just take the first candidate task + state.runnables.remove(candidate_indices[0]) + } + }; - if let Some(runnable) = runnable { - let is_foreground = runnable.session_id.is_some(); - let was_main_thread = self.state.lock().is_main_thread; - self.state.lock().is_main_thread = is_foreground; - runnable.run(); - self.state.lock().is_main_thread = was_main_thread; - return true; - } + if let Some(runnable) = runnable { + let is_foreground = runnable.session_id.is_some(); + let was_main_thread = self.state.lock().is_main_thread; + self.state.lock().is_main_thread = is_foreground; + runnable.run(); + self.state.lock().is_main_thread = was_main_thread; + return true; + } - false - } + false + } - /// Drops all runnable tasks from the scheduler. - /// - /// This is used by the leak detector to ensure that all tasks have been dropped as tasks may keep entities alive otherwise. - /// Why do we even have tasks left when tests finish you may ask. The reason for that is simple, the scheduler itself is the executor and it retains the scheduled runnables. - /// A lot of tasks, including every foreground task contain an executor handle that keeps the test scheduler alive, causing a reference cycle, thus the need for this function right now. - pub fn drain_tasks(&self) { - // dropping runnables may reschedule tasks - // due to drop impls with executors in them - // so drop until we reach a fixpoint - loop { - let mut state = self.state.lock(); - if state.runnables.is_empty() && state.timers.is_empty() { - break; - } - let runnables = std::mem::take(&mut state.runnables); - let timers = std::mem::take(&mut state.timers); - drop(state); - drop(timers); - drop(runnables); - } - } + /// Drops all runnable tasks from the scheduler. + /// + /// This is used by the leak detector to ensure that all tasks have been dropped as tasks may keep entities alive otherwise. + /// Why do we even have tasks left when tests finish you may ask. The reason for that is simple, the scheduler itself is the executor and it retains the scheduled runnables. + /// A lot of tasks, including every foreground task contain an executor handle that keeps the test scheduler alive, causing a reference cycle, thus the need for this function right now. + pub fn drain_tasks(&self) { + // dropping runnables may reschedule tasks + // due to drop impls with executors in them + // so drop until we reach a fixpoint + loop { + let mut state = self.state.lock(); + if state.runnables.is_empty() && state.timers.is_empty() { + break; + } + let runnables = std::mem::take(&mut state.runnables); + let timers = std::mem::take(&mut state.timers); + drop(state); + drop(timers); + drop(runnables); + } + } - pub fn advance_clock_to_next_timer(&self) -> bool { - if let Some(timer) = self.state.lock().timers.first() { - self.clock.advance(timer.expiration - self.clock.now()); - true - } else { - false - } - } + pub fn advance_clock_to_next_timer(&self) -> bool { + if let Some(timer) = self.state.lock().timers.first() { + self.clock.advance(timer.expiration - self.clock.now()); + true + } else { + false + } + } - pub fn advance_clock(&self, duration: Duration) { - let debug = std::env::var("DEBUG_SCHEDULER").is_ok(); - let start = self.clock.now(); - let next_now = start + duration; - if debug { - let timer_count = self.state.lock().timers.len(); - eprintln!( - "[scheduler] advance_clock({:?}) from {:?}, {} pending timers", - duration, start, timer_count - ); - } - loop { - self.run(); - if let Some(timer) = self.state.lock().timers.first() - && timer.expiration <= next_now - { - let advance_to = timer.expiration; - if debug { - eprintln!( - "[scheduler] Advancing clock {:?} -> {:?} for timer", - self.clock.now(), - advance_to - ); - } - self.clock.advance(advance_to - self.clock.now()); - } else { - break; - } - } - self.clock.advance(next_now - self.clock.now()); - if debug { - eprintln!( - "[scheduler] advance_clock done, now at {:?}", - self.clock.now() - ); - } - } + pub fn advance_clock(&self, duration: Duration) { + let debug = std::env::var("DEBUG_SCHEDULER").is_ok(); + let start = self.clock.now(); + let next_now = start + duration; + if debug { + let timer_count = self.state.lock().timers.len(); + eprintln!( + "[scheduler] advance_clock({:?}) from {:?}, {} pending timers", + duration, start, timer_count + ); + } + loop { + self.run(); + if let Some(timer) = self.state.lock().timers.first() + && timer.expiration <= next_now + { + let advance_to = timer.expiration; + if debug { + eprintln!( + "[scheduler] Advancing clock {:?} -> {:?} for timer", + self.clock.now(), + advance_to + ); + } + self.clock.advance(advance_to - self.clock.now()); + } else { + break; + } + } + self.clock.advance(next_now - self.clock.now()); + if debug { + eprintln!( + "[scheduler] advance_clock done, now at {:?}", + self.clock.now() + ); + } + } - fn park(&self, deadline: Option) -> bool { - if self.state.lock().allow_parking { - let start = Instant::now(); - // Enforce a hard timeout to prevent tests from hanging indefinitely - let hard_deadline = start + Duration::from_secs(15); + fn park(&self, deadline: Option) -> bool { + if self.state.lock().allow_parking { + let start = Instant::now(); + // Enforce a hard timeout to prevent tests from hanging indefinitely + let hard_deadline = start + Duration::from_secs(15); - // Use the earlier of the provided deadline or the hard timeout deadline - let effective_deadline = deadline - .map(|d| d.min(hard_deadline)) - .unwrap_or(hard_deadline); + // Use the earlier of the provided deadline or the hard timeout deadline + let effective_deadline = deadline + .map(|d| d.min(hard_deadline)) + .unwrap_or(hard_deadline); - // Park in small intervals to allow checking both deadlines - const PARK_INTERVAL: Duration = Duration::from_millis(100); - loop { - let now = Instant::now(); - if now >= effective_deadline { - // Check if we hit the hard timeout - if now >= hard_deadline { - panic!( - "Test timed out after 15 seconds while parking. \ + // Park in small intervals to allow checking both deadlines + const PARK_INTERVAL: Duration = Duration::from_millis(100); + loop { + let now = Instant::now(); + if now >= effective_deadline { + // Check if we hit the hard timeout + if now >= hard_deadline { + panic!( + "Test timed out after 15 seconds while parking. \ This may indicate a deadlock or missing waker.", - ); - } - // Hit the provided deadline - return false; - } + ); + } + // Hit the provided deadline + return false; + } - let remaining = effective_deadline.saturating_duration_since(now); - let park_duration = remaining.min(PARK_INTERVAL); - let before_park = Instant::now(); - thread::park_timeout(park_duration); - let elapsed = before_park.elapsed(); + let remaining = effective_deadline.saturating_duration_since(now); + let park_duration = remaining.min(PARK_INTERVAL); + let before_park = Instant::now(); + thread::park_timeout(park_duration); + let elapsed = before_park.elapsed(); - // Advance the test clock by the real elapsed time while parking - self.clock.advance(elapsed); + // Advance the test clock by the real elapsed time while parking + self.clock.advance(elapsed); - // Check if any timers have expired after advancing the clock. - // If so, return so the caller can process them. - if self - .state - .lock() - .timers - .first() - .is_some_and(|t| t.expiration <= self.clock.now()) - { - return true; - } + // Check if any timers have expired after advancing the clock. + // If so, return so the caller can process them. + if self + .state + .lock() + .timers + .first() + .is_some_and(|t| t.expiration <= self.clock.now()) + { + return true; + } - // Check if we were woken up by a different thread. - // We use a flag because timing-based detection is unreliable: - // OS scheduling delays can cause elapsed >= park_duration even when - // we were woken early by unpark(). - if std::mem::take(&mut self.state.lock().unparked) { - return true; - } - } - } else if deadline.is_some() { - false - } else if cfg!(miri) { - // miri cannot debug print backtraces with `miri-disable-isolation` enabled - panic!("Parking forbidden."); - } else if self.state.lock().capture_pending_traces { - let mut pending_traces = String::new(); - for (_, trace) in mem::take(&mut self.state.lock().pending_traces) { - writeln!(pending_traces, "{:?}", exclude_wakers_from_trace(trace)).unwrap(); - } - panic!("Parking forbidden. Pending traces:\n{}", pending_traces); - } else { - panic!( - "Parking forbidden. Re-run with {PENDING_TRACES_VAR_NAME}=1 to show pending traces" - ); - } - } + // Check if we were woken up by a different thread. + // We use a flag because timing-based detection is unreliable: + // OS scheduling delays can cause elapsed >= park_duration even when + // we were woken early by unpark(). + if std::mem::take(&mut self.state.lock().unparked) { + return true; + } + } + } else if deadline.is_some() { + false + } else if cfg!(miri) { + // miri cannot debug print backtraces with `miri-disable-isolation` enabled + panic!("Parking forbidden."); + } else if self.state.lock().capture_pending_traces { + let mut pending_traces = String::new(); + for (_, trace) in mem::take(&mut self.state.lock().pending_traces) { + writeln!(pending_traces, "{:?}", exclude_wakers_from_trace(trace)).unwrap(); + } + panic!("Parking forbidden. Pending traces:\n{}", pending_traces); + } else { + panic!( + "Parking forbidden. Re-run with {PENDING_TRACES_VAR_NAME}=1 to show pending traces" + ); + } + } } fn assert_correct_thread(expected: &Thread, state: &Arc>) { - let current_thread = thread::current(); - let mut state = state.lock(); - if state.parking_allowed_once { - return; - } - if current_thread.id() == expected.id() { - return; - } + let current_thread = thread::current(); + let mut state = state.lock(); + if state.parking_allowed_once { + return; + } + if current_thread.id() == expected.id() { + return; + } - let message = format!( - "Detected activity on thread {:?} {:?}, but test scheduler is running on {:?} {:?}. Your test is not deterministic.", - current_thread.name(), - current_thread.id(), - expected.name(), - expected.id(), - ); - let backtrace = Backtrace::new(); - if state.finished { - panic!("{}", message); - } else { - state.non_determinism_error = Some((message, backtrace)) - } + let message = format!( + "Detected activity on thread {:?} {:?}, but test scheduler is running on {:?} {:?}. Your test is not deterministic.", + current_thread.name(), + current_thread.id(), + expected.name(), + expected.id(), + ); + let backtrace = Backtrace::new(); + if state.finished { + panic!("{}", message); + } else { + state.non_determinism_error = Some((message, backtrace)) + } } impl Scheduler for TestScheduler { - /// Block until the given future completes, with an optional timeout. If the - /// future is unable to make progress at any moment before the timeout and - /// no other tasks or timers remain, we panic unless parking is allowed. If - /// parking is allowed, we block up to the timeout or indefinitely if none - /// is provided. This is to allow testing a mix of deterministic and - /// non-deterministic async behavior, such as when interacting with I/O in - /// an otherwise deterministic test. - fn block( - &self, - session_id: Option, - mut future: Pin<&mut dyn Future>, - timeout: Option, - ) -> bool { - if let Some(session_id) = session_id { - self.state.lock().blocked_sessions.push(session_id); - } + /// Block until the given future completes, with an optional timeout. If the + /// future is unable to make progress at any moment before the timeout and + /// no other tasks or timers remain, we panic unless parking is allowed. If + /// parking is allowed, we block up to the timeout or indefinitely if none + /// is provided. This is to allow testing a mix of deterministic and + /// non-deterministic async behavior, such as when interacting with I/O in + /// an otherwise deterministic test. + fn block( + &self, + session_id: Option, + mut future: Pin<&mut dyn Future>, + timeout: Option, + ) -> bool { + if let Some(session_id) = session_id { + self.state.lock().blocked_sessions.push(session_id); + } - let deadline = timeout.map(|timeout| Instant::now() + timeout); - let awoken = Arc::new(AtomicBool::new(false)); - let waker = Box::new(TracingWaker { - id: None, - awoken: awoken.clone(), - thread: self.thread.clone(), - state: self.state.clone(), - }); - let waker = unsafe { Waker::new(Box::into_raw(waker) as *const (), &WAKER_VTABLE) }; - let max_ticks = if timeout.is_some() { - self.rng - .lock() - .random_range(self.state.lock().timeout_ticks.clone()) - } else { - usize::MAX - }; - let mut cx = Context::from_waker(&waker); + let deadline = timeout.map(|timeout| Instant::now() + timeout); + let awoken = Arc::new(AtomicBool::new(false)); + let waker = Box::new(TracingWaker { + id: None, + awoken: awoken.clone(), + thread: self.thread.clone(), + state: self.state.clone(), + }); + let waker = unsafe { Waker::new(Box::into_raw(waker) as *const (), &WAKER_VTABLE) }; + let max_ticks = if timeout.is_some() { + self.rng + .lock() + .random_range(self.state.lock().timeout_ticks.clone()) + } else { + usize::MAX + }; + let mut cx = Context::from_waker(&waker); - let mut completed = false; - for _ in 0..max_ticks { - match future.as_mut().poll(&mut cx) { - Poll::Ready(()) => { - completed = true; - break; - } - Poll::Pending => {} - } + let mut completed = false; + for _ in 0..max_ticks { + match future.as_mut().poll(&mut cx) { + Poll::Ready(()) => { + completed = true; + break; + } + Poll::Pending => {} + } - let mut stepped = None; - while self.rng.lock().random() { - let stepped = stepped.get_or_insert(false); - if self.step() { - *stepped = true; - } else { - break; - } - } + let mut stepped = None; + while self.rng.lock().random() { + let stepped = stepped.get_or_insert(false); + if self.step() { + *stepped = true; + } else { + break; + } + } - let stepped = stepped.unwrap_or(true); - let awoken = awoken.swap(false, SeqCst); - if !stepped && !awoken { - let parking_allowed = self.state.lock().allow_parking; - // In deterministic mode (parking forbidden), instantly jump to the next timer. - // In non-deterministic mode (parking allowed), let real time pass instead. - let advanced_to_timer = !parking_allowed && self.advance_clock_to_next_timer(); - if !advanced_to_timer && !self.park(deadline) { - break; - } - } - } + let stepped = stepped.unwrap_or(true); + let awoken = awoken.swap(false, SeqCst); + if !stepped && !awoken { + let parking_allowed = self.state.lock().allow_parking; + // In deterministic mode (parking forbidden), instantly jump to the next timer. + // In non-deterministic mode (parking allowed), let real time pass instead. + let advanced_to_timer = !parking_allowed && self.advance_clock_to_next_timer(); + if !advanced_to_timer && !self.park(deadline) { + break; + } + } + } - if session_id.is_some() { - self.state.lock().blocked_sessions.pop(); - } + if session_id.is_some() { + self.state.lock().blocked_sessions.pop(); + } - completed - } + completed + } - fn schedule_local(&self, session_id: SessionId, runnable: Runnable) { - assert_correct_thread(&self.thread, &self.state); - let mut state = self.state.lock(); - let ix = if state.randomize_order { - let start_ix = state - .runnables - .iter() - .rposition(|task| task.session_id == Some(session_id)) - .map_or(0, |ix| ix + 1); - self.rng - .lock() - .random_range(start_ix..=state.runnables.len()) - } else { - state.runnables.len() - }; - state.runnables.insert( - ix, - ScheduledRunnable { - session_id: Some(session_id), - priority: Priority::default(), - runnable, - }, - ); - state.unparked = true; - drop(state); - self.thread.unpark(); - } + fn schedule_local(&self, session_id: SessionId, runnable: Runnable) { + assert_correct_thread(&self.thread, &self.state); + let mut state = self.state.lock(); + let ix = if state.randomize_order { + let start_ix = state + .runnables + .iter() + .rposition(|task| task.session_id == Some(session_id)) + .map_or(0, |ix| ix + 1); + self.rng + .lock() + .random_range(start_ix..=state.runnables.len()) + } else { + state.runnables.len() + }; + state.runnables.insert( + ix, + ScheduledRunnable { + session_id: Some(session_id), + priority: Priority::default(), + runnable, + }, + ); + state.unparked = true; + drop(state); + self.thread.unpark(); + } - fn schedule_background_with_priority( - &self, - runnable: Runnable, - priority: Priority, - ) { - assert_correct_thread(&self.thread, &self.state); - let mut state = self.state.lock(); - let ix = if state.randomize_order { - self.rng.lock().random_range(0..=state.runnables.len()) - } else { - state.runnables.len() - }; - state.runnables.insert( - ix, - ScheduledRunnable { - session_id: None, - priority, - runnable, - }, - ); - state.unparked = true; - drop(state); - self.thread.unpark(); - } + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ) { + assert_correct_thread(&self.thread, &self.state); + let mut state = self.state.lock(); + let ix = if state.randomize_order { + self.rng.lock().random_range(0..=state.runnables.len()) + } else { + state.runnables.len() + }; + state.runnables.insert( + ix, + ScheduledRunnable { + session_id: None, + priority, + runnable, + }, + ); + state.unparked = true; + drop(state); + self.thread.unpark(); + } - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - f(); - }); - } + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + f(); + }); + } - #[track_caller] - fn timer(&self, duration: Duration) -> Timer { - let (tx, rx) = oneshot::channel(); - let state = &mut *self.state.lock(); - state.timers.push(ScheduledTimer { - expiration: self.clock.now() + duration, - _notify: tx, - }); - state.timers.sort_by_key(|timer| timer.expiration); - Timer(rx) - } + #[track_caller] + fn timer(&self, duration: Duration) -> Timer { + let (tx, rx) = oneshot::channel(); + let state = &mut *self.state.lock(); + state.timers.push(ScheduledTimer { + expiration: self.clock.now() + duration, + _notify: tx, + }); + state.timers.sort_by_key(|timer| timer.expiration); + Timer(rx) + } - fn clock(&self) -> Arc { - self.clock.clone() - } + fn clock(&self) -> Arc { + self.clock.clone() + } - /// In the test world, dedicated work is just a fresh local session driven - /// by the test scheduler's run loop alongside everything else. No real - /// thread is spawned, so determinism under `TestScheduler::many` is - /// preserved. - fn spawn_dedicated( - self: Arc, - f: Box< - dyn FnOnce( - LocalExecutor, - ) - -> Pin> + 'static>> - + Send - + 'static, - >, - ) -> Task> { - let session_id = self.allocate_session_id(); - let scheduler = Arc::downgrade(&self); - let executor = LocalExecutor::new(session_id, self, move |runnable| { - if let Some(scheduler) = scheduler.upgrade() { - scheduler.schedule_local(session_id, runnable); - } - }); - executor.spawn(f(executor.clone())) - } + /// In the test world, dedicated work is just a fresh local session driven + /// by the test scheduler's run loop alongside everything else. No real + /// thread is spawned, so determinism under `TestScheduler::many` is + /// preserved. + fn spawn_dedicated( + self: Arc, + f: Box< + dyn FnOnce( + LocalExecutor, + ) -> Pin> + 'static>> + + Send + + 'static, + >, + ) -> Task> { + let session_id = self.allocate_session_id(); + let scheduler = Arc::downgrade(&self); + let executor = LocalExecutor::new(session_id, self, move |runnable| { + if let Some(scheduler) = scheduler.upgrade() { + scheduler.schedule_local(session_id, runnable); + } + }); + executor.spawn(f(executor.clone())) + } - fn as_test(&self) -> Option<&TestScheduler> { - Some(self) - } + fn as_test(&self) -> Option<&TestScheduler> { + Some(self) + } } #[derive(Clone, Debug)] pub struct TestSchedulerConfig { - pub seed: u64, - pub randomize_order: bool, - pub allow_parking: bool, - pub capture_pending_traces: bool, - pub timeout_ticks: RangeInclusive, + pub seed: u64, + pub randomize_order: bool, + pub allow_parking: bool, + pub capture_pending_traces: bool, + pub timeout_ticks: RangeInclusive, } impl TestSchedulerConfig { - pub fn with_seed(seed: u64) -> Self { - Self { - seed, - ..Default::default() - } - } + pub fn with_seed(seed: u64) -> Self { + Self { + seed, + ..Default::default() + } + } } impl Default for TestSchedulerConfig { - fn default() -> Self { - Self { - seed: 0, - randomize_order: true, - allow_parking: false, - capture_pending_traces: env::var(PENDING_TRACES_VAR_NAME) - .is_ok_and(|var| var == "1" || var == "true"), - timeout_ticks: 1..=1000, - } - } + fn default() -> Self { + Self { + seed: 0, + randomize_order: true, + allow_parking: false, + capture_pending_traces: env::var(PENDING_TRACES_VAR_NAME) + .is_ok_and(|var| var == "1" || var == "true"), + timeout_ticks: 1..=1000, + } + } } struct ScheduledRunnable { - session_id: Option, - priority: Priority, - runnable: Runnable, + session_id: Option, + priority: Priority, + runnable: Runnable, } impl ScheduledRunnable { - fn run(self) { - self.runnable.run(); - } + fn run(self) { + self.runnable.run(); + } } struct ScheduledTimer { - expiration: Instant, - _notify: oneshot::Sender<()>, + expiration: Instant, + _notify: oneshot::Sender<()>, } struct SchedulerState { - runnables: VecDeque, - timers: Vec, - blocked_sessions: Vec, - randomize_order: bool, - allow_parking: bool, - timeout_ticks: RangeInclusive, - next_session_id: SessionId, - capture_pending_traces: bool, - next_trace_id: TraceId, - pending_traces: BTreeMap, - is_main_thread: bool, - non_determinism_error: Option<(String, Backtrace)>, - parking_allowed_once: bool, - finished: bool, - unparked: bool, + runnables: VecDeque, + timers: Vec, + blocked_sessions: Vec, + randomize_order: bool, + allow_parking: bool, + timeout_ticks: RangeInclusive, + next_session_id: SessionId, + capture_pending_traces: bool, + next_trace_id: TraceId, + pending_traces: BTreeMap, + is_main_thread: bool, + non_determinism_error: Option<(String, Backtrace)>, + parking_allowed_once: bool, + finished: bool, + unparked: bool, } const WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( - TracingWaker::clone_raw, - TracingWaker::wake_raw, - TracingWaker::wake_by_ref_raw, - TracingWaker::drop_raw, + TracingWaker::clone_raw, + TracingWaker::wake_raw, + TracingWaker::wake_by_ref_raw, + TracingWaker::drop_raw, ); #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] struct TraceId(usize); struct TracingWaker { - id: Option, - awoken: Arc, - thread: Thread, - state: Arc>, + id: Option, + awoken: Arc, + thread: Thread, + state: Arc>, } impl Clone for TracingWaker { - fn clone(&self) -> Self { - let mut state = self.state.lock(); - let id = if state.capture_pending_traces { - let id = state.next_trace_id; - state.next_trace_id.0 += 1; - state.pending_traces.insert(id, Backtrace::new_unresolved()); - Some(id) - } else { - None - }; - Self { - id, - awoken: self.awoken.clone(), - thread: self.thread.clone(), - state: self.state.clone(), - } - } + fn clone(&self) -> Self { + let mut state = self.state.lock(); + let id = if state.capture_pending_traces { + let id = state.next_trace_id; + state.next_trace_id.0 += 1; + state.pending_traces.insert(id, Backtrace::new_unresolved()); + Some(id) + } else { + None + }; + Self { + id, + awoken: self.awoken.clone(), + thread: self.thread.clone(), + state: self.state.clone(), + } + } } impl Drop for TracingWaker { - fn drop(&mut self) { - assert_correct_thread(&self.thread, &self.state); + fn drop(&mut self) { + assert_correct_thread(&self.thread, &self.state); - if let Some(id) = self.id { - self.state.lock().pending_traces.remove(&id); - } - } + if let Some(id) = self.id { + self.state.lock().pending_traces.remove(&id); + } + } } impl TracingWaker { - fn wake(self) { - self.wake_by_ref(); - } + fn wake(self) { + self.wake_by_ref(); + } - fn wake_by_ref(&self) { - assert_correct_thread(&self.thread, &self.state); + fn wake_by_ref(&self) { + assert_correct_thread(&self.thread, &self.state); - let mut state = self.state.lock(); - if let Some(id) = self.id { - state.pending_traces.remove(&id); - } - state.unparked = true; - drop(state); - self.awoken.store(true, SeqCst); - self.thread.unpark(); - } + let mut state = self.state.lock(); + if let Some(id) = self.id { + state.pending_traces.remove(&id); + } + state.unparked = true; + drop(state); + self.awoken.store(true, SeqCst); + self.thread.unpark(); + } - fn clone_raw(waker: *const ()) -> RawWaker { - let waker = waker as *const TracingWaker; - let waker = unsafe { &*waker }; - RawWaker::new( - Box::into_raw(Box::new(waker.clone())) as *const (), - &WAKER_VTABLE, - ) - } + fn clone_raw(waker: *const ()) -> RawWaker { + let waker = waker as *const TracingWaker; + let waker = unsafe { &*waker }; + RawWaker::new( + Box::into_raw(Box::new(waker.clone())) as *const (), + &WAKER_VTABLE, + ) + } - fn wake_raw(waker: *const ()) { - let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; - waker.wake(); - } + fn wake_raw(waker: *const ()) { + let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; + waker.wake(); + } - fn wake_by_ref_raw(waker: *const ()) { - let waker = waker as *const TracingWaker; - let waker = unsafe { &*waker }; - waker.wake_by_ref(); - } + fn wake_by_ref_raw(waker: *const ()) { + let waker = waker as *const TracingWaker; + let waker = unsafe { &*waker }; + waker.wake_by_ref(); + } - fn drop_raw(waker: *const ()) { - let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; - drop(waker); - } + fn drop_raw(waker: *const ()) { + let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; + drop(waker); + } } pub struct Yield(usize); @@ -859,68 +858,68 @@ pub struct Yield(usize); pub struct SharedRng(Arc>); impl SharedRng { - /// Lock the inner RNG for direct access. Use this when you need multiple - /// random operations without re-locking between each one. - pub fn lock(&self) -> MutexGuard<'_, StdRng> { - self.0.lock() - } + /// Lock the inner RNG for direct access. Use this when you need multiple + /// random operations without re-locking between each one. + pub fn lock(&self) -> MutexGuard<'_, StdRng> { + self.0.lock() + } - /// Generate a random value in the given range. - pub fn random_range(&self, range: R) -> T - where - T: SampleUniform, - R: SampleRange, - { - self.0.lock().random_range(range) - } + /// Generate a random value in the given range. + pub fn random_range(&self, range: R) -> T + where + T: SampleUniform, + R: SampleRange, + { + self.0.lock().random_range(range) + } - /// Generate a random boolean with the given probability of being true. - pub fn random_bool(&self, p: f64) -> bool { - self.0.lock().random_bool(p) - } + /// Generate a random boolean with the given probability of being true. + pub fn random_bool(&self, p: f64) -> bool { + self.0.lock().random_bool(p) + } - /// Generate a random value of the given type. - pub fn random(&self) -> T - where - StandardUniform: Distribution, - { - self.0.lock().random() - } + /// Generate a random value of the given type. + pub fn random(&self) -> T + where + StandardUniform: Distribution, + { + self.0.lock().random() + } - /// Generate a random ratio - true with probability `numerator/denominator`. - pub fn random_ratio(&self, numerator: u32, denominator: u32) -> bool { - self.0.lock().random_ratio(numerator, denominator) - } + /// Generate a random ratio - true with probability `numerator/denominator`. + pub fn random_ratio(&self, numerator: u32, denominator: u32) -> bool { + self.0.lock().random_ratio(numerator, denominator) + } } impl Future for Yield { - type Output = (); + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - if self.0 == 0 { - Poll::Ready(()) - } else { - self.0 -= 1; - cx.waker().wake_by_ref(); - Poll::Pending - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + if self.0 == 0 { + Poll::Ready(()) + } else { + self.0 -= 1; + cx.waker().wake_by_ref(); + Poll::Pending + } + } } fn exclude_wakers_from_trace(mut trace: Backtrace) -> Backtrace { - trace.resolve(); - let mut frames: Vec = trace.into(); - let waker_clone_frame_ix = frames.iter().position(|frame| { - frame.symbols().iter().any(|symbol| { - symbol - .name() - .is_some_and(|name| format!("{name:#?}") == type_name_of_val(&Waker::clone)) - }) - }); + trace.resolve(); + let mut frames: Vec = trace.into(); + let waker_clone_frame_ix = frames.iter().position(|frame| { + frame.symbols().iter().any(|symbol| { + symbol + .name() + .is_some_and(|name| format!("{name:#?}") == type_name_of_val(&Waker::clone)) + }) + }); - if let Some(waker_clone_frame_ix) = waker_clone_frame_ix { - frames.drain(..waker_clone_frame_ix + 1); - } + if let Some(waker_clone_frame_ix) = waker_clone_frame_ix { + frames.drain(..waker_clone_frame_ix + 1); + } - Backtrace::from(frames) + Backtrace::from(frames) } diff --git a/crates/gpui_scheduler/src/tests.rs b/crates/gpui_scheduler/src/tests.rs index bff1bff963..f0a99be8fd 100644 --- a/crates/gpui_scheduler/src/tests.rs +++ b/crates/gpui_scheduler/src/tests.rs @@ -1,959 +1,959 @@ use super::*; use futures::{ - FutureExt, - channel::{mpsc, oneshot}, - executor::block_on, - future, - sink::SinkExt, - stream::{FuturesUnordered, StreamExt}, + FutureExt, + channel::{mpsc, oneshot}, + executor::block_on, + future, + sink::SinkExt, + stream::{FuturesUnordered, StreamExt}, }; use std::{ - cell::RefCell, - collections::{BTreeSet, HashSet}, - pin::Pin, - rc::Rc, - sync::Arc, - task::{Context, Poll, Waker}, + cell::RefCell, + collections::{BTreeSet, HashSet}, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll, Waker}, }; #[test] fn test_foreground_executor_spawn() { - let result = TestScheduler::once(async |scheduler| { - let task = scheduler.foreground().spawn(async move { 42 }); - task.await - }); - assert_eq!(result, 42); + let result = TestScheduler::once(async |scheduler| { + let task = scheduler.foreground().spawn(async move { 42 }); + task.await + }); + assert_eq!(result, 42); } #[test] fn test_background_executor_spawn() { - TestScheduler::once(async |scheduler| { - let task = scheduler.background().spawn(async move { 42 }); - let result = task.await; - assert_eq!(result, 42); - }); + TestScheduler::once(async |scheduler| { + let task = scheduler.background().spawn(async move { 42 }); + let result = task.await; + assert_eq!(result, 42); + }); } #[test] fn test_scheduler_drops_with_stalled_detached_foreground_task() { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); - let weak_scheduler = Arc::downgrade(&scheduler); - let (sender, receiver) = oneshot::channel::<()>(); + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); + let weak_scheduler = Arc::downgrade(&scheduler); + let (sender, receiver) = oneshot::channel::<()>(); - scheduler - .foreground() - .spawn(async move { - receiver.await.ok(); - }) - .detach(); - scheduler.run(); + scheduler + .foreground() + .spawn(async move { + receiver.await.ok(); + }) + .detach(); + scheduler.run(); - drop(scheduler); - assert!(weak_scheduler.upgrade().is_none()); - drop(sender); + drop(scheduler); + assert!(weak_scheduler.upgrade().is_none()); + drop(sender); } #[test] fn test_scheduler_drops_with_stalled_detached_background_task() { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); - let weak_scheduler = Arc::downgrade(&scheduler); - let (sender, receiver) = oneshot::channel::<()>(); + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); + let weak_scheduler = Arc::downgrade(&scheduler); + let (sender, receiver) = oneshot::channel::<()>(); - scheduler - .background() - .spawn(async move { - receiver.await.ok(); - }) - .detach(); - scheduler.run(); + scheduler + .background() + .spawn(async move { + receiver.await.ok(); + }) + .detach(); + scheduler.run(); - drop(scheduler); - assert!(weak_scheduler.upgrade().is_none()); - drop(sender); + drop(scheduler); + assert!(weak_scheduler.upgrade().is_none()); + drop(sender); } #[test] fn test_foreground_ordering() { - let mut traces = HashSet::new(); + let mut traces = HashSet::new(); - TestScheduler::many(if cfg!(miri) { 5 } else { 100 }, async |scheduler| { - #[derive(Hash, PartialEq, Eq)] - struct TraceEntry { - session: usize, - task: usize, - } + TestScheduler::many(if cfg!(miri) { 5 } else { 100 }, async |scheduler| { + #[derive(Hash, PartialEq, Eq)] + struct TraceEntry { + session: usize, + task: usize, + } - let trace = Rc::new(RefCell::new(Vec::new())); + let trace = Rc::new(RefCell::new(Vec::new())); - let foreground_1 = scheduler.foreground(); - for task in 0..10 { - foreground_1 - .spawn({ - let trace = trace.clone(); - async move { - trace.borrow_mut().push(TraceEntry { session: 0, task }); - } - }) - .detach(); - } + let foreground_1 = scheduler.foreground(); + for task in 0..10 { + foreground_1 + .spawn({ + let trace = trace.clone(); + async move { + trace.borrow_mut().push(TraceEntry { session: 0, task }); + } + }) + .detach(); + } - let foreground_2 = scheduler.foreground(); - for task in 0..10 { - foreground_2 - .spawn({ - let trace = trace.clone(); - async move { - trace.borrow_mut().push(TraceEntry { session: 1, task }); - } - }) - .detach(); - } + let foreground_2 = scheduler.foreground(); + for task in 0..10 { + foreground_2 + .spawn({ + let trace = trace.clone(); + async move { + trace.borrow_mut().push(TraceEntry { session: 1, task }); + } + }) + .detach(); + } - scheduler.run(); + scheduler.run(); - assert_eq!( - trace - .borrow() - .iter() - .filter(|entry| entry.session == 0) - .map(|entry| entry.task) - .collect::>(), - (0..10).collect::>() - ); - assert_eq!( - trace - .borrow() - .iter() - .filter(|entry| entry.session == 1) - .map(|entry| entry.task) - .collect::>(), - (0..10).collect::>() - ); + assert_eq!( + trace + .borrow() + .iter() + .filter(|entry| entry.session == 0) + .map(|entry| entry.task) + .collect::>(), + (0..10).collect::>() + ); + assert_eq!( + trace + .borrow() + .iter() + .filter(|entry| entry.session == 1) + .map(|entry| entry.task) + .collect::>(), + (0..10).collect::>() + ); - traces.insert(trace.take()); - }); + traces.insert(trace.take()); + }); - assert!(traces.len() > 1, "Expected at least two traces"); + assert!(traces.len() > 1, "Expected at least two traces"); } #[test] fn test_timer_ordering() { - TestScheduler::many(1, async |scheduler| { - let background = scheduler.background(); - let futures = FuturesUnordered::new(); - futures.push( - async { - background.timer(Duration::from_millis(100)).await; - 2 - } - .boxed(), - ); - futures.push( - async { - background.timer(Duration::from_millis(50)).await; - 1 - } - .boxed(), - ); - futures.push( - async { - background.timer(Duration::from_millis(150)).await; - 3 - } - .boxed(), - ); - assert_eq!(futures.collect::>().await, vec![1, 2, 3]); - }); + TestScheduler::many(1, async |scheduler| { + let background = scheduler.background(); + let futures = FuturesUnordered::new(); + futures.push( + async { + background.timer(Duration::from_millis(100)).await; + 2 + } + .boxed(), + ); + futures.push( + async { + background.timer(Duration::from_millis(50)).await; + 1 + } + .boxed(), + ); + futures.push( + async { + background.timer(Duration::from_millis(150)).await; + 3 + } + .boxed(), + ); + assert_eq!(futures.collect::>().await, vec![1, 2, 3]); + }); } #[test] fn test_foreground_task_can_hold_mut_borrow_across_await() { - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let (sender, mut receiver) = mpsc::unbounded::<()>(); + TestScheduler::once(async |scheduler| { + let foreground = scheduler.foreground(); + let (sender, mut receiver) = mpsc::unbounded::<()>(); - foreground - .spawn(async move { - receiver.next().await; - }) - .detach(); + foreground + .spawn(async move { + receiver.next().await; + }) + .detach(); - scheduler.run(); - sender.unbounded_send(()).unwrap(); - scheduler.run(); - }); + scheduler.run(); + sender.unbounded_send(()).unwrap(); + scheduler.run(); + }); } #[test] fn test_send_from_bg_to_fg() { - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let background = scheduler.background(); + TestScheduler::once(async |scheduler| { + let foreground = scheduler.foreground(); + let background = scheduler.background(); - let (sender, receiver) = oneshot::channel::(); + let (sender, receiver) = oneshot::channel::(); - background - .spawn(async move { - sender.send(42).unwrap(); - }) - .detach(); + background + .spawn(async move { + sender.send(42).unwrap(); + }) + .detach(); - let task = foreground.spawn(async move { receiver.await.unwrap() }); - let result = task.await; - assert_eq!(result, 42); - }); + let task = foreground.spawn(async move { receiver.await.unwrap() }); + let result = task.await; + assert_eq!(result, 42); + }); } #[test] fn test_randomize_order() { - // Test deterministic mode: different seeds should produce same execution order - let mut deterministic_results = HashSet::new(); - for seed in 0..10 { - let config = TestSchedulerConfig { - seed, - randomize_order: false, - ..Default::default() - }; - let order = block_on(capture_execution_order(config)); - assert_eq!(order.len(), 6); - deterministic_results.insert(order); - } + // Test deterministic mode: different seeds should produce same execution order + let mut deterministic_results = HashSet::new(); + for seed in 0..10 { + let config = TestSchedulerConfig { + seed, + randomize_order: false, + ..Default::default() + }; + let order = block_on(capture_execution_order(config)); + assert_eq!(order.len(), 6); + deterministic_results.insert(order); + } - // All deterministic runs should produce the same result - assert_eq!( - deterministic_results.len(), - 1, - "Deterministic mode should always produce same execution order" - ); + // All deterministic runs should produce the same result + assert_eq!( + deterministic_results.len(), + 1, + "Deterministic mode should always produce same execution order" + ); - // Test randomized mode: different seeds can produce different execution orders - let mut randomized_results = HashSet::new(); - for seed in 0..20 { - let config = TestSchedulerConfig::with_seed(seed); - let order = block_on(capture_execution_order(config)); - assert_eq!(order.len(), 6); - randomized_results.insert(order); - } + // Test randomized mode: different seeds can produce different execution orders + let mut randomized_results = HashSet::new(); + for seed in 0..20 { + let config = TestSchedulerConfig::with_seed(seed); + let order = block_on(capture_execution_order(config)); + assert_eq!(order.len(), 6); + randomized_results.insert(order); + } - // Randomized mode should produce multiple different execution orders - assert!( - randomized_results.len() > 1, - "Randomized mode should produce multiple different orders" - ); + // Randomized mode should produce multiple different execution orders + assert!( + randomized_results.len() > 1, + "Randomized mode should produce multiple different orders" + ); } async fn capture_execution_order(config: TestSchedulerConfig) -> Vec { - let scheduler = Arc::new(TestScheduler::new(config)); - let foreground = scheduler.foreground(); - let background = scheduler.background(); + let scheduler = Arc::new(TestScheduler::new(config)); + let foreground = scheduler.foreground(); + let background = scheduler.background(); - let (sender, receiver) = mpsc::unbounded::(); + let (sender, receiver) = mpsc::unbounded::(); - // Spawn foreground tasks - for i in 0..3 { - let mut sender = sender.clone(); - foreground - .spawn(async move { - sender.send(format!("fg-{}", i)).await.ok(); - }) - .detach(); - } + // Spawn foreground tasks + for i in 0..3 { + let mut sender = sender.clone(); + foreground + .spawn(async move { + sender.send(format!("fg-{}", i)).await.ok(); + }) + .detach(); + } - // Spawn background tasks - for i in 0..3 { - let mut sender = sender.clone(); - background - .spawn(async move { - sender.send(format!("bg-{}", i)).await.ok(); - }) - .detach(); - } + // Spawn background tasks + for i in 0..3 { + let mut sender = sender.clone(); + background + .spawn(async move { + sender.send(format!("bg-{}", i)).await.ok(); + }) + .detach(); + } - drop(sender); // Close sender to signal no more messages - scheduler.run(); + drop(sender); // Close sender to signal no more messages + scheduler.run(); - receiver.collect().await + receiver.collect().await } #[test] fn test_block() { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); - let (tx, rx) = oneshot::channel(); + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); + let (tx, rx) = oneshot::channel(); - // Spawn background task to send value - scheduler - .background() - .spawn(async move { - tx.send(42).unwrap(); - }) - .detach(); + // Spawn background task to send value + scheduler + .background() + .spawn(async move { + tx.send(42).unwrap(); + }) + .detach(); - // Block on receiving the value - let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); - assert_eq!(result, 42); + // Block on receiving the value + let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); + assert_eq!(result, 42); } #[test] #[should_panic(expected = "Parking forbidden.")] fn test_parking_panics() { - let config = TestSchedulerConfig { - capture_pending_traces: true, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); - scheduler.foreground().block_on(async { - let (_tx, rx) = oneshot::channel::<()>(); - rx.await.unwrap(); // This will never complete - }); + let config = TestSchedulerConfig { + capture_pending_traces: true, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + scheduler.foreground().block_on(async { + let (_tx, rx) = oneshot::channel::<()>(); + rx.await.unwrap(); // This will never complete + }); } #[test] fn test_block_with_parking() { - let config = TestSchedulerConfig { - allow_parking: true, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); - let (tx, rx) = oneshot::channel(); + let config = TestSchedulerConfig { + allow_parking: true, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + let (tx, rx) = oneshot::channel(); - // Spawn background task to send value - scheduler - .background() - .spawn(async move { - tx.send(42).unwrap(); - }) - .detach(); + // Spawn background task to send value + scheduler + .background() + .spawn(async move { + tx.send(42).unwrap(); + }) + .detach(); - // Block on receiving the value (will park if needed) - let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); - assert_eq!(result, 42); + // Block on receiving the value (will park if needed) + let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); + assert_eq!(result, 42); } #[test] fn test_helper_methods() { - // Test the once method - let result = TestScheduler::once(async |scheduler: Arc| { - let background = scheduler.background(); - background.spawn(async { 42 }).await - }); - assert_eq!(result, 42); + // Test the once method + let result = TestScheduler::once(async |scheduler: Arc| { + let background = scheduler.background(); + background.spawn(async { 42 }).await + }); + assert_eq!(result, 42); - // Test the many method - let results = TestScheduler::many(3, async |scheduler: Arc| { - let background = scheduler.background(); - background.spawn(async { 10 }).await - }); - assert_eq!(results, vec![10, 10, 10]); + // Test the many method + let results = TestScheduler::many(3, async |scheduler: Arc| { + let background = scheduler.background(); + background.spawn(async { 10 }).await + }); + assert_eq!(results, vec![10, 10, 10]); } #[test] fn test_many_with_arbitrary_seed() { - for seed in [0u64, 1, 5, 42] { - let mut seeds_seen = Vec::new(); - let iterations = 3usize; + for seed in [0u64, 1, 5, 42] { + let mut seeds_seen = Vec::new(); + let iterations = 3usize; - for current_seed in seed..seed + iterations as u64 { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed( - current_seed, - ))); - let captured_seed = current_seed; - scheduler - .foreground() - .block_on(async { seeds_seen.push(captured_seed) }); - scheduler.run(); - } + for current_seed in seed..seed + iterations as u64 { + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed( + current_seed, + ))); + let captured_seed = current_seed; + scheduler + .foreground() + .block_on(async { seeds_seen.push(captured_seed) }); + scheduler.run(); + } - assert_eq!( - seeds_seen, - (seed..seed + iterations as u64).collect::>(), - "Expected {iterations} iterations starting at seed {seed}" - ); - } + assert_eq!( + seeds_seen, + (seed..seed + iterations as u64).collect::>(), + "Expected {iterations} iterations starting at seed {seed}" + ); + } } #[test] fn test_block_with_timeout() { - // Test case: future completes within timeout - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let future = future::ready(42); - let output = foreground.block_with_timeout(Duration::from_millis(100), future); - assert_eq!(output.ok(), Some(42)); - }); + // Test case: future completes within timeout + TestScheduler::once(async |scheduler| { + let foreground = scheduler.foreground(); + let future = future::ready(42); + let output = foreground.block_with_timeout(Duration::from_millis(100), future); + assert_eq!(output.ok(), Some(42)); + }); - // Test case: future times out - TestScheduler::once(async |scheduler| { - // Make timeout behavior deterministic by forcing the timeout tick budget to be exactly 0. - // This prevents `block_with_timeout` from making progress via extra scheduler stepping and - // accidentally completing work that we expect to time out. - scheduler.set_timeout_ticks(0..=0); + // Test case: future times out + TestScheduler::once(async |scheduler| { + // Make timeout behavior deterministic by forcing the timeout tick budget to be exactly 0. + // This prevents `block_with_timeout` from making progress via extra scheduler stepping and + // accidentally completing work that we expect to time out. + scheduler.set_timeout_ticks(0..=0); - let foreground = scheduler.foreground(); - let future = future::pending::<()>(); - let output = foreground.block_with_timeout(Duration::from_millis(50), future); - assert!(output.is_err(), "future should not have finished"); - }); + let foreground = scheduler.foreground(); + let future = future::pending::<()>(); + let output = foreground.block_with_timeout(Duration::from_millis(50), future); + assert!(output.is_err(), "future should not have finished"); + }); - // Test case: future makes progress via timer but still times out - let mut results = BTreeSet::new(); - TestScheduler::many(if cfg!(miri) { 5 } else { 100 }, async |scheduler| { - // Keep the existing probabilistic behavior here (do not force 0 ticks), since this subtest - // is explicitly checking that some seeds/timeouts can complete while others can time out. - let task = scheduler.background().spawn(async move { - Yield { polls: 10 }.await; - 42 - }); - let output = scheduler - .foreground() - .block_with_timeout(Duration::from_millis(50), task); - results.insert(output.ok()); - }); - assert_eq!( - results.into_iter().collect::>(), - if cfg!(miri) { - vec![Some(42)] - } else { - vec![None, Some(42)] - } - ); + // Test case: future makes progress via timer but still times out + let mut results = BTreeSet::new(); + TestScheduler::many(if cfg!(miri) { 5 } else { 100 }, async |scheduler| { + // Keep the existing probabilistic behavior here (do not force 0 ticks), since this subtest + // is explicitly checking that some seeds/timeouts can complete while others can time out. + let task = scheduler.background().spawn(async move { + Yield { polls: 10 }.await; + 42 + }); + let output = scheduler + .foreground() + .block_with_timeout(Duration::from_millis(50), task); + results.insert(output.ok()); + }); + assert_eq!( + results.into_iter().collect::>(), + if cfg!(miri) { + vec![Some(42)] + } else { + vec![None, Some(42)] + } + ); - // Regression test: - // A timed-out future must not be cancelled. The returned future should still be - // pollable to completion later. We also want to ensure time only advances when we - // explicitly advance it (not by yielding). - TestScheduler::once(async |scheduler| { - // Force immediate timeout: the timeout tick budget is 0 so we will not step or - // advance timers inside `block_with_timeout`. - scheduler.set_timeout_ticks(0..=0); + // Regression test: + // A timed-out future must not be cancelled. The returned future should still be + // pollable to completion later. We also want to ensure time only advances when we + // explicitly advance it (not by yielding). + TestScheduler::once(async |scheduler| { + // Force immediate timeout: the timeout tick budget is 0 so we will not step or + // advance timers inside `block_with_timeout`. + scheduler.set_timeout_ticks(0..=0); - let background = scheduler.background(); + let background = scheduler.background(); - // This task should only complete once time is explicitly advanced. - let task = background.spawn({ - let scheduler = scheduler.clone(); - async move { - scheduler.timer(Duration::from_millis(100)).await; - 123 - } - }); + // This task should only complete once time is explicitly advanced. + let task = background.spawn({ + let scheduler = scheduler.clone(); + async move { + scheduler.timer(Duration::from_millis(100)).await; + 123 + } + }); - // This should time out before we advance time enough for the timer to fire. - let timed_out = scheduler - .foreground() - .block_with_timeout(Duration::from_millis(50), task); - assert!( - timed_out.is_err(), - "expected timeout before advancing the clock enough for the timer" - ); + // This should time out before we advance time enough for the timer to fire. + let timed_out = scheduler + .foreground() + .block_with_timeout(Duration::from_millis(50), task); + assert!( + timed_out.is_err(), + "expected timeout before advancing the clock enough for the timer" + ); - // Now explicitly advance time and ensure the returned future can complete. - let mut task = timed_out.err().unwrap(); - scheduler.advance_clock(Duration::from_millis(100)); - scheduler.run(); + // Now explicitly advance time and ensure the returned future can complete. + let mut task = timed_out.err().unwrap(); + scheduler.advance_clock(Duration::from_millis(100)); + scheduler.run(); - let output = scheduler.foreground().block_on(&mut task); - assert_eq!(output, 123); - }); + let output = scheduler.foreground().block_on(&mut task); + assert_eq!(output, 123); + }); } // When calling block, we shouldn't make progress on foreground-spawned futures with the same session id. #[test] fn test_block_does_not_progress_same_session_foreground() { - let mut task2_made_progress_once = false; - TestScheduler::many(if cfg!(miri) { 5 } else { 1000 }, async |scheduler| { - let foreground1 = scheduler.foreground(); - let foreground2 = scheduler.foreground(); + let mut task2_made_progress_once = false; + TestScheduler::many(if cfg!(miri) { 5 } else { 1000 }, async |scheduler| { + let foreground1 = scheduler.foreground(); + let foreground2 = scheduler.foreground(); - let task1 = foreground1.spawn(async move {}); - let task2 = foreground2.spawn(async move {}); + let task1 = foreground1.spawn(async move {}); + let task2 = foreground2.spawn(async move {}); - foreground1.block_on(async { - scheduler.yield_random().await; - assert!(!task1.is_ready()); - task2_made_progress_once |= task2.is_ready(); - }); + foreground1.block_on(async { + scheduler.yield_random().await; + assert!(!task1.is_ready()); + task2_made_progress_once |= task2.is_ready(); + }); - task1.await; - task2.await; - }); + task1.await; + task2.await; + }); - assert!( - task2_made_progress_once, - "Expected task from different foreground executor to make progress (at least once)" - ); + assert!( + task2_made_progress_once, + "Expected task from different foreground executor to make progress (at least once)" + ); } struct Yield { - polls: usize, + polls: usize, } impl Future for Yield { - type Output = (); + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.polls -= 1; - if self.polls == 0 { - Poll::Ready(()) - } else { - cx.waker().wake_by_ref(); - Poll::Pending - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.polls -= 1; + if self.polls == 0 { + Poll::Ready(()) + } else { + cx.waker().wake_by_ref(); + Poll::Pending + } + } } #[test] fn test_nondeterministic_wake_detection() { - let config = TestSchedulerConfig { - allow_parking: false, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); + let config = TestSchedulerConfig { + allow_parking: false, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); - // A future that captures its waker and sends it to an external thread - struct SendWakerToThread { - waker_tx: Option>, - } + // A future that captures its waker and sends it to an external thread + struct SendWakerToThread { + waker_tx: Option>, + } - impl Future for SendWakerToThread { - type Output = (); + impl Future for SendWakerToThread { + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if let Some(tx) = self.waker_tx.take() { - tx.send(cx.waker().clone()).ok(); - } - Poll::Ready(()) - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Ready(()) + } + } - let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); - // Get a waker by running a future that sends it - scheduler.foreground().block_on(SendWakerToThread { - waker_tx: Some(waker_tx), - }); + // Get a waker by running a future that sends it + scheduler.foreground().block_on(SendWakerToThread { + waker_tx: Some(waker_tx), + }); - // Spawn a real OS thread that will call wake() on the waker - let handle = std::thread::spawn(move || { - if let Ok(waker) = waker_rx.recv() { - // This should trigger the non-determinism detection - waker.wake(); - } - }); + // Spawn a real OS thread that will call wake() on the waker + let handle = std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // This should trigger the non-determinism detection + waker.wake(); + } + }); - // Wait for the spawned thread to complete - handle.join().ok(); + // Wait for the spawned thread to complete + handle.join().ok(); - // The non-determinism error should be detected when end_test is called - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - scheduler.end_test(); - })); - assert!(result.is_err(), "Expected end_test to panic"); - let panic_payload = result.unwrap_err(); - let panic_message = panic_payload - .downcast_ref::() - .map(|s| s.as_str()) - .or_else(|| panic_payload.downcast_ref::<&str>().copied()) - .unwrap_or(""); - assert!( - panic_message.contains("Your test is not deterministic"), - "Expected panic message to contain non-determinism error, got: {}", - panic_message - ); + // The non-determinism error should be detected when end_test is called + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + scheduler.end_test(); + })); + assert!(result.is_err(), "Expected end_test to panic"); + let panic_payload = result.unwrap_err(); + let panic_message = panic_payload + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic_payload.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + panic_message.contains("Your test is not deterministic"), + "Expected panic message to contain non-determinism error, got: {}", + panic_message + ); } #[test] fn test_nondeterministic_wake_allowed_with_parking() { - let config = TestSchedulerConfig { - allow_parking: true, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); + let config = TestSchedulerConfig { + allow_parking: true, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); - // A future that captures its waker and sends it to an external thread - struct WakeFromExternalThread { - waker_sent: bool, - waker_tx: Option>, - } + // A future that captures its waker and sends it to an external thread + struct WakeFromExternalThread { + waker_sent: bool, + waker_tx: Option>, + } - impl Future for WakeFromExternalThread { - type Output = (); + impl Future for WakeFromExternalThread { + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if !self.waker_sent { - self.waker_sent = true; - if let Some(tx) = self.waker_tx.take() { - tx.send(cx.waker().clone()).ok(); - } - Poll::Pending - } else { - Poll::Ready(()) - } - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if !self.waker_sent { + self.waker_sent = true; + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Pending + } else { + Poll::Ready(()) + } + } + } - let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); - // Spawn a real OS thread that will call wake() on the waker - std::thread::spawn(move || { - if let Ok(waker) = waker_rx.recv() { - // With allow_parking, this should NOT panic - waker.wake(); - } - }); + // Spawn a real OS thread that will call wake() on the waker + std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // With allow_parking, this should NOT panic + waker.wake(); + } + }); - // This should complete without panicking - scheduler.foreground().block_on(WakeFromExternalThread { - waker_sent: false, - waker_tx: Some(waker_tx), - }); + // This should complete without panicking + scheduler.foreground().block_on(WakeFromExternalThread { + waker_sent: false, + waker_tx: Some(waker_tx), + }); } #[test] fn test_nondeterministic_waker_drop_detection() { - let config = TestSchedulerConfig { - allow_parking: false, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); + let config = TestSchedulerConfig { + allow_parking: false, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); - // A future that captures its waker and sends it to an external thread - struct SendWakerToThread { - waker_tx: Option>, - } + // A future that captures its waker and sends it to an external thread + struct SendWakerToThread { + waker_tx: Option>, + } - impl Future for SendWakerToThread { - type Output = (); + impl Future for SendWakerToThread { + type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if let Some(tx) = self.waker_tx.take() { - tx.send(cx.waker().clone()).ok(); - } - Poll::Ready(()) - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Ready(()) + } + } - let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); - // Get a waker by running a future that sends it - scheduler.foreground().block_on(SendWakerToThread { - waker_tx: Some(waker_tx), - }); + // Get a waker by running a future that sends it + scheduler.foreground().block_on(SendWakerToThread { + waker_tx: Some(waker_tx), + }); - // Spawn a real OS thread that will drop the waker without calling wake - let handle = std::thread::spawn(move || { - if let Ok(waker) = waker_rx.recv() { - // This should trigger the non-determinism detection on drop - drop(waker); - } - }); + // Spawn a real OS thread that will drop the waker without calling wake + let handle = std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // This should trigger the non-determinism detection on drop + drop(waker); + } + }); - // Wait for the spawned thread to complete - handle.join().ok(); + // Wait for the spawned thread to complete + handle.join().ok(); - // The non-determinism error should be detected when end_test is called - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - scheduler.end_test(); - })); - assert!(result.is_err(), "Expected end_test to panic"); - let panic_payload = result.unwrap_err(); - let panic_message = panic_payload - .downcast_ref::() - .map(|s| s.as_str()) - .or_else(|| panic_payload.downcast_ref::<&str>().copied()) - .unwrap_or(""); - assert!( - panic_message.contains("Your test is not deterministic"), - "Expected panic message to contain non-determinism error, got: {}", - panic_message - ); + // The non-determinism error should be detected when end_test is called + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + scheduler.end_test(); + })); + assert!(result.is_err(), "Expected end_test to panic"); + let panic_payload = result.unwrap_err(); + let panic_message = panic_payload + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic_payload.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + panic_message.contains("Your test is not deterministic"), + "Expected panic message to contain non-determinism error, got: {}", + panic_message + ); } #[test] fn test_background_priority_scheduling() { - use parking_lot::Mutex; + use parking_lot::Mutex; - // Run many iterations to get statistical significance - let mut high_before_low_count = 0; - let iterations = if cfg!(miri) { 5 } else { 100 }; + // Run many iterations to get statistical significance + let mut high_before_low_count = 0; + let iterations = if cfg!(miri) { 5 } else { 100 }; - for seed in 0..iterations { - let config = TestSchedulerConfig::with_seed(seed); - let scheduler = Arc::new(TestScheduler::new(config)); - let background = scheduler.background(); + for seed in 0..iterations { + let config = TestSchedulerConfig::with_seed(seed); + let scheduler = Arc::new(TestScheduler::new(config)); + let background = scheduler.background(); - let execution_order = Arc::new(Mutex::new(Vec::new())); + let execution_order = Arc::new(Mutex::new(Vec::new())); - // Spawn low priority tasks first - for i in 0..3 { - let order = execution_order.clone(); - background - .spawn_with_priority(Priority::Low, async move { - order.lock().push(format!("low-{}", i)); - }) - .detach(); - } + // Spawn low priority tasks first + for i in 0..3 { + let order = execution_order.clone(); + background + .spawn_with_priority(Priority::Low, async move { + order.lock().push(format!("low-{}", i)); + }) + .detach(); + } - // Spawn high priority tasks second - for i in 0..3 { - let order = execution_order.clone(); - background - .spawn_with_priority(Priority::High, async move { - order.lock().push(format!("high-{}", i)); - }) - .detach(); - } + // Spawn high priority tasks second + for i in 0..3 { + let order = execution_order.clone(); + background + .spawn_with_priority(Priority::High, async move { + order.lock().push(format!("high-{}", i)); + }) + .detach(); + } - scheduler.run(); + scheduler.run(); - // Count how many high priority tasks ran in the first half - let order = execution_order.lock(); - let high_in_first_half = order - .iter() - .take(3) - .filter(|s| s.starts_with("high")) - .count(); + // Count how many high priority tasks ran in the first half + let order = execution_order.lock(); + let high_in_first_half = order + .iter() + .take(3) + .filter(|s| s.starts_with("high")) + .count(); - if high_in_first_half >= 2 { - high_before_low_count += 1; - } - } + if high_in_first_half >= 2 { + high_before_low_count += 1; + } + } - // High priority tasks should tend to run before low priority tasks - // With weights of 60 vs 10, high priority should dominate early execution - assert!( - high_before_low_count > iterations / 2, - "Expected high priority tasks to run before low priority tasks more often. \ + // High priority tasks should tend to run before low priority tasks + // With weights of 60 vs 10, high priority should dominate early execution + assert!( + high_before_low_count > iterations / 2, + "Expected high priority tasks to run before low priority tasks more often. \ Got {} out of {} iterations", - high_before_low_count, - iterations - ); + high_before_low_count, + iterations + ); } #[test] fn test_spawn_dedicated_basic_round_trip() { - let result = TestScheduler::once(async |scheduler| { - scheduler - .background() - .spawn_dedicated(|_executor| async { 42 }) - .await - }); - assert_eq!(result, 42); + let result = TestScheduler::once(async |scheduler| { + scheduler + .background() + .spawn_dedicated(|_executor| async { 42 }) + .await + }); + assert_eq!(result, 42); } #[test] fn test_spawn_dedicated_not_send_future() { - let result = TestScheduler::once(async |scheduler| { - scheduler - .background() - .spawn_dedicated(|_executor| async move { - // `Rc>` is `!Send`. If `spawn_dedicated` required - // the returned future to be `Send`, this wouldn't compile. - let state = Rc::new(RefCell::new(0_i32)); - for _ in 0..5 { - *state.borrow_mut() += 1; - } - *state.borrow() - }) - .await - }); - assert_eq!(result, 5); + let result = TestScheduler::once(async |scheduler| { + scheduler + .background() + .spawn_dedicated(|_executor| async move { + // `Rc>` is `!Send`. If `spawn_dedicated` required + // the returned future to be `Send`, this wouldn't compile. + let state = Rc::new(RefCell::new(0_i32)); + for _ in 0..5 { + *state.borrow_mut() += 1; + } + *state.borrow() + }) + .await + }); + assert_eq!(result, 5); } #[test] fn test_spawn_dedicated_send_closure_captures() { - use parking_lot::Mutex; + use parking_lot::Mutex; - let observed = TestScheduler::once(async |scheduler| { - let shared = Arc::new(Mutex::new(0_i32)); - let shared_for_closure = shared.clone(); - let returned = scheduler - .background() - .spawn_dedicated(move |_executor| { - // `shared_for_closure` crossed the `Send` boundary of the - // closure; we then mutate it from inside the !Send future. - let local = shared_for_closure; - async move { - *local.lock() = 7; - } - }) - .await; - let _: () = returned; - *shared.lock() - }); - assert_eq!(observed, 7); + let observed = TestScheduler::once(async |scheduler| { + let shared = Arc::new(Mutex::new(0_i32)); + let shared_for_closure = shared.clone(); + let returned = scheduler + .background() + .spawn_dedicated(move |_executor| { + // `shared_for_closure` crossed the `Send` boundary of the + // closure; we then mutate it from inside the !Send future. + let local = shared_for_closure; + async move { + *local.lock() = 7; + } + }) + .await; + let _: () = returned; + *shared.lock() + }); + assert_eq!(observed, 7); } #[test] fn test_spawn_dedicated_inner_spawn_local() { - let result = TestScheduler::once(async |scheduler| { - scheduler - .background() - .spawn_dedicated(|executor| async move { - // The provided executor can spawn additional `!Send` work - // onto the same dedicated session. - let inner = Rc::new(RefCell::new(0_i32)); - let inner_for_child = inner.clone(); - let child = executor.spawn(async move { - *inner_for_child.borrow_mut() = 99; - *inner_for_child.borrow() - }); - child.await - }) - .await - }); - assert_eq!(result, 99); + let result = TestScheduler::once(async |scheduler| { + scheduler + .background() + .spawn_dedicated(|executor| async move { + // The provided executor can spawn additional `!Send` work + // onto the same dedicated session. + let inner = Rc::new(RefCell::new(0_i32)); + let inner_for_child = inner.clone(); + let child = executor.spawn(async move { + *inner_for_child.borrow_mut() = 99; + *inner_for_child.borrow() + }); + child.await + }) + .await + }); + assert_eq!(result, 99); } #[test] fn test_spawn_dedicated_determinism_under_many() { - use parking_lot::Mutex; + use parking_lot::Mutex; - let outcomes = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| { - let trace = Arc::new(Mutex::new(Vec::::new())); + let outcomes = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| { + let trace = Arc::new(Mutex::new(Vec::::new())); - let background = scheduler.background(); - let mut tasks = Vec::new(); - for id in 0..4_u32 { - let trace = trace.clone(); - let task = background.spawn_dedicated(move |executor| async move { - for step in 0..3 { - trace.lock().push(id * 100 + step); - executor.spawn(async {}).await; - } - id - }); - tasks.push(task); - } + let background = scheduler.background(); + let mut tasks = Vec::new(); + for id in 0..4_u32 { + let trace = trace.clone(); + let task = background.spawn_dedicated(move |executor| async move { + for step in 0..3 { + trace.lock().push(id * 100 + step); + executor.spawn(async {}).await; + } + id + }); + tasks.push(task); + } - let mut outputs = Vec::new(); - for task in tasks { - outputs.push(task.await); - } + let mut outputs = Vec::new(); + for task in tasks { + outputs.push(task.await); + } - (trace.lock().clone(), outputs) - }); + (trace.lock().clone(), outputs) + }); - // Re-running with the same seed should produce the same trace. Run a - // second pass with identical seeds and compare to the first. - let outcomes_replay = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| { - let trace = Arc::new(Mutex::new(Vec::::new())); + // Re-running with the same seed should produce the same trace. Run a + // second pass with identical seeds and compare to the first. + let outcomes_replay = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| { + let trace = Arc::new(Mutex::new(Vec::::new())); - let background = scheduler.background(); - let mut tasks = Vec::new(); - for id in 0..4_u32 { - let trace = trace.clone(); - let task = background.spawn_dedicated(move |executor| async move { - for step in 0..3 { - trace.lock().push(id * 100 + step); - executor.spawn(async {}).await; - } - id - }); - tasks.push(task); - } + let background = scheduler.background(); + let mut tasks = Vec::new(); + for id in 0..4_u32 { + let trace = trace.clone(); + let task = background.spawn_dedicated(move |executor| async move { + for step in 0..3 { + trace.lock().push(id * 100 + step); + executor.spawn(async {}).await; + } + id + }); + tasks.push(task); + } - let mut outputs = Vec::new(); - for task in tasks { - outputs.push(task.await); - } + let mut outputs = Vec::new(); + for task in tasks { + outputs.push(task.await); + } - (trace.lock().clone(), outputs) - }); + (trace.lock().clone(), outputs) + }); - assert_eq!( - outcomes, outcomes_replay, - "per-seed outcomes should be reproducible" - ); + assert_eq!( + outcomes, outcomes_replay, + "per-seed outcomes should be reproducible" + ); - // Sanity: at least one seed produced a non-monotonic trace, - // demonstrating that dedicated tasks really do interleave under the - // scheduler's randomization. - let any_interleaved = outcomes.iter().any(|(trace, _)| { - trace - .windows(2) - .any(|window| window[0] / 100 != window[1] / 100) - }); - assert!( - any_interleaved, - "expected at least one seed to interleave dedicated tasks" - ); + // Sanity: at least one seed produced a non-monotonic trace, + // demonstrating that dedicated tasks really do interleave under the + // scheduler's randomization. + let any_interleaved = outcomes.iter().any(|(trace, _)| { + trace + .windows(2) + .any(|window| window[0] / 100 != window[1] / 100) + }); + assert!( + any_interleaved, + "expected at least one seed to interleave dedicated tasks" + ); } #[test] fn test_spawn_dedicated_dropping_task_cancels_future() { - use parking_lot::Mutex; + use parking_lot::Mutex; - let counter_after = TestScheduler::once(async |scheduler| { - let counter = Arc::new(Mutex::new(0_u32)); - let (resume_tx, resume_rx) = oneshot::channel::<()>(); + let counter_after = TestScheduler::once(async |scheduler| { + let counter = Arc::new(Mutex::new(0_u32)); + let (resume_tx, resume_rx) = oneshot::channel::<()>(); - let task = { - let counter = counter.clone(); - scheduler - .background() - .spawn_dedicated(move |_executor| async move { - *counter.lock() = 1; - // Park here until the test resumes us. If the task is - // dropped before this resolves, the second assignment - // below must never happen. - let _ = resume_rx.await; - *counter.lock() = 2; - }) - }; + let task = { + let counter = counter.clone(); + scheduler + .background() + .spawn_dedicated(move |_executor| async move { + *counter.lock() = 1; + // Park here until the test resumes us. If the task is + // dropped before this resolves, the second assignment + // below must never happen. + let _ = resume_rx.await; + *counter.lock() = 2; + }) + }; - // Let the dedicated future make its first observable step. - scheduler.run(); - assert_eq!(*counter.lock(), 1); + // Let the dedicated future make its first observable step. + scheduler.run(); + assert_eq!(*counter.lock(), 1); - // Cancel by dropping the root task, then unblock the parked oneshot. - // The future must not advance past the await: counter stays at 1. - drop(task); - let _ = resume_tx.send(()); - scheduler.run(); + // Cancel by dropping the root task, then unblock the parked oneshot. + // The future must not advance past the await: counter stays at 1. + drop(task); + let _ = resume_tx.send(()); + scheduler.run(); - *counter.lock() - }); + *counter.lock() + }); - assert_eq!( - counter_after, 1, - "dropping the dedicated task must cancel the root future before its second write" - ); + assert_eq!( + counter_after, 1, + "dropping the dedicated task must cancel the root future before its second write" + ); } #[test] fn test_spawn_dedicated_detached_child_runs_after_root_completes() { - use parking_lot::Mutex; + use parking_lot::Mutex; - let child_ran = TestScheduler::once(async |scheduler| { - let child_ran = Arc::new(Mutex::new(false)); + let child_ran = TestScheduler::once(async |scheduler| { + let child_ran = Arc::new(Mutex::new(false)); - let task = { - let child_ran = child_ran.clone(); - scheduler - .background() - .spawn_dedicated(move |executor| async move { - executor - .spawn(async move { - *child_ran.lock() = true; - }) - .detach(); - // Root returns immediately, before the child has had a - // chance to run. - }) - }; + let task = { + let child_ran = child_ran.clone(); + scheduler + .background() + .spawn_dedicated(move |executor| async move { + executor + .spawn(async move { + *child_ran.lock() = true; + }) + .detach(); + // Root returns immediately, before the child has had a + // chance to run. + }) + }; - task.await; + task.await; - // Drain the dedicated session. The detached child must run. - scheduler.run(); + // Drain the dedicated session. The detached child must run. + scheduler.run(); - *child_ran.lock() - }); + *child_ran.lock() + }); - assert!( - child_ran, - "detached child must complete after the root, not be cancelled with it" - ); + assert!( + child_ran, + "detached child must complete after the root, not be cancelled with it" + ); } // The production smoke test for `spawn_dedicated` lives in the `gpui` crate diff --git a/crates/gpui_shared_string/gpui_shared_string.rs b/crates/gpui_shared_string/gpui_shared_string.rs index 4395065b9f..a32fd10098 100644 --- a/crates/gpui_shared_string/gpui_shared_string.rs +++ b/crates/gpui_shared_string/gpui_shared_string.rs @@ -1,6 +1,6 @@ use std::{ - borrow::{Borrow, Cow}, - sync::Arc, + borrow::{Borrow, Cow}, + sync::Arc, }; use schemars::JsonSchema; @@ -14,190 +14,190 @@ use smol_str::SmolStr; pub struct SharedString(SmolStr); impl std::ops::Deref for SharedString { - type Target = str; + type Target = str; - fn deref(&self) -> &Self::Target { - self.0.as_str() - } + fn deref(&self) -> &Self::Target { + self.0.as_str() + } } impl SharedString { - /// Creates a static [`SharedString`] from a `&'static str`. - pub const fn new_static(str: &'static str) -> Self { - Self(SmolStr::new_static(str)) - } + /// Creates a static [`SharedString`] from a `&'static str`. + pub const fn new_static(str: &'static str) -> Self { + Self(SmolStr::new_static(str)) + } - /// Creates a [`SharedString`]. - pub fn new(str: impl AsRef) -> Self { - SharedString(SmolStr::new(str)) - } + /// Creates a [`SharedString`]. + pub fn new(str: impl AsRef) -> Self { + SharedString(SmolStr::new(str)) + } - /// Get a &str from the underlying string. - pub fn as_str(&self) -> &str { - &self.0 - } + /// Get a &str from the underlying string. + pub fn as_str(&self) -> &str { + &self.0 + } } impl JsonSchema for SharedString { - fn inline_schema() -> bool { - String::inline_schema() - } + fn inline_schema() -> bool { + String::inline_schema() + } - fn schema_name() -> Cow<'static, str> { - String::schema_name() - } + fn schema_name() -> Cow<'static, str> { + String::schema_name() + } - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - String::json_schema(generator) - } + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + String::json_schema(generator) + } } impl Default for SharedString { - fn default() -> Self { - Self::new_static("") - } + fn default() -> Self { + Self::new_static("") + } } impl AsRef for SharedString { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { + &self.0 + } } impl Borrow for SharedString { - fn borrow(&self) -> &str { - self.as_ref() - } + fn borrow(&self) -> &str { + self.as_ref() + } } impl std::fmt::Debug for SharedString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } } impl std::fmt::Display for SharedString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.as_str()) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } } impl PartialEq for SharedString { - fn eq(&self, other: &String) -> bool { - self.as_ref() == other - } + fn eq(&self, other: &String) -> bool { + self.as_ref() == other + } } impl PartialEq for String { - fn eq(&self, other: &SharedString) -> bool { - self == other.as_ref() - } + fn eq(&self, other: &SharedString) -> bool { + self == other.as_ref() + } } impl PartialEq for SharedString { - fn eq(&self, other: &str) -> bool { - self.as_ref() == other - } + fn eq(&self, other: &str) -> bool { + self.as_ref() == other + } } impl<'a> PartialEq<&'a str> for SharedString { - fn eq(&self, other: &&'a str) -> bool { - self.as_ref() == *other - } + fn eq(&self, other: &&'a str) -> bool { + self.as_ref() == *other + } } impl From<&SharedString> for SharedString { - #[inline] - fn from(s: &SharedString) -> SharedString { - s.clone() - } + #[inline] + fn from(s: &SharedString) -> SharedString { + s.clone() + } } impl From<&str> for SharedString { - #[inline] - fn from(s: &str) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: &str) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From<&mut str> for SharedString { - #[inline] - fn from(s: &mut str) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: &mut str) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From<&String> for SharedString { - #[inline] - fn from(s: &String) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: &String) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From for SharedString { - #[inline(always)] - fn from(text: String) -> Self { - SharedString(SmolStr::from(text)) - } + #[inline(always)] + fn from(text: String) -> Self { + SharedString(SmolStr::from(text)) + } } impl From> for SharedString { - #[inline] - fn from(s: Box) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: Box) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From> for SharedString { - #[inline] - fn from(s: Arc) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: Arc) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From<&Arc> for SharedString { - #[inline] - fn from(s: &Arc) -> SharedString { - SharedString(SmolStr::from(s.clone())) - } + #[inline] + fn from(s: &Arc) -> SharedString { + SharedString(SmolStr::from(s.clone())) + } } impl<'a> From> for SharedString { - #[inline] - fn from(s: Cow<'a, str>) -> SharedString { - SharedString(SmolStr::from(s)) - } + #[inline] + fn from(s: Cow<'a, str>) -> SharedString { + SharedString(SmolStr::from(s)) + } } impl From for Arc { - #[inline(always)] - fn from(text: SharedString) -> Self { - text.0.into() - } + #[inline(always)] + fn from(text: SharedString) -> Self { + text.0.into() + } } impl From for String { - #[inline(always)] - fn from(text: SharedString) -> Self { - text.0.into() - } + #[inline(always)] + fn from(text: SharedString) -> Self { + text.0.into() + } } impl Serialize for SharedString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.as_ref()) - } + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_ref()) + } } impl<'de> Deserialize<'de> for SharedString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(SharedString::new(&s)) - } + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(SharedString::new(&s)) + } } diff --git a/crates/gpui_sum_tree/src/cursor.rs b/crates/gpui_sum_tree/src/cursor.rs index 536e035747..1048b6704f 100644 --- a/crates/gpui_sum_tree/src/cursor.rs +++ b/crates/gpui_sum_tree/src/cursor.rs @@ -5,857 +5,857 @@ use tracing::instrument; #[derive(Clone)] struct StackEntry<'a, T: Item, D> { - tree: &'a SumTree, - index: u32, - position: D, + tree: &'a SumTree, + index: u32, + position: D, } impl<'a, T: Item, D> StackEntry<'a, T, D> { - #[inline] - fn index(&self) -> usize { - self.index as usize - } + #[inline] + fn index(&self) -> usize { + self.index as usize + } } impl fmt::Debug for StackEntry<'_, T, D> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StackEntry") - .field("index", &self.index) - .field("position", &self.position) - .finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StackEntry") + .field("index", &self.index) + .field("position", &self.position) + .finish() + } } #[derive(Clone)] pub struct Cursor<'a, 'b, T: Item, D> { - tree: &'a SumTree, - stack: ArrayVec, 16, u8>, - pub position: D, - did_seek: bool, - at_end: bool, - cx: ::Context<'b>, + tree: &'a SumTree, + stack: ArrayVec, 16, u8>, + pub position: D, + did_seek: bool, + at_end: bool, + cx: ::Context<'b>, } impl fmt::Debug for Cursor<'_, '_, T, D> where - T::Summary: fmt::Debug, + T::Summary: fmt::Debug, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Cursor") - .field("tree", &self.tree) - .field("stack", &self.stack) - .field("position", &self.position) - .field("did_seek", &self.did_seek) - .field("at_end", &self.at_end) - .finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Cursor") + .field("tree", &self.tree) + .field("stack", &self.stack) + .field("position", &self.position) + .field("did_seek", &self.did_seek) + .field("at_end", &self.at_end) + .finish() + } } pub struct Iter<'a, T: Item> { - tree: &'a SumTree, - stack: ArrayVec, 16, u8>, + tree: &'a SumTree, + stack: ArrayVec, 16, u8>, } impl<'a, 'b, T, D> Cursor<'a, 'b, T, D> where - T: Item, - D: Dimension<'a, T::Summary>, + T: Item, + D: Dimension<'a, T::Summary>, { - pub fn new(tree: &'a SumTree, cx: ::Context<'b>) -> Self { - Self { - tree, - stack: ArrayVec::new(), - position: D::zero(cx), - did_seek: false, - at_end: tree.is_empty(), - cx, - } - } + pub fn new(tree: &'a SumTree, cx: ::Context<'b>) -> Self { + Self { + tree, + stack: ArrayVec::new(), + position: D::zero(cx), + did_seek: false, + at_end: tree.is_empty(), + cx, + } + } - pub fn reset(&mut self) { - self.did_seek = false; - self.at_end = self.tree.is_empty(); - self.stack.truncate(0); - self.position = D::zero(self.cx); - } + pub fn reset(&mut self) { + self.did_seek = false; + self.at_end = self.tree.is_empty(); + self.stack.truncate(0); + self.position = D::zero(self.cx); + } - pub fn start(&self) -> &D { - &self.position - } + pub fn start(&self) -> &D { + &self.position + } - #[track_caller] - pub fn end(&self) -> D { - if let Some(item_summary) = self.item_summary() { - let mut end = self.start().clone(); - end.add_summary(item_summary, self.cx); - end - } else { - self.start().clone() - } - } + #[track_caller] + pub fn end(&self) -> D { + if let Some(item_summary) = self.item_summary() { + let mut end = self.start().clone(); + end.add_summary(item_summary, self.cx); + end + } else { + self.start().clone() + } + } - /// Item is None, when the list is empty, or this cursor is at the end of the list. - #[track_caller] - pub fn item(&self) -> Option<&'a T> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - match *entry.tree.0 { - Node::Leaf { ref items, .. } => { - if entry.index() == items.len() { - None - } else { - Some(&items[entry.index()]) - } - } - _ => unreachable!(), - } - } else { - None - } - } + /// Item is None, when the list is empty, or this cursor is at the end of the list. + #[track_caller] + pub fn item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => { + if entry.index() == items.len() { + None + } else { + Some(&items[entry.index()]) + } + } + _ => unreachable!(), + } + } else { + None + } + } - #[track_caller] - pub fn item_summary(&self) -> Option<&'a T::Summary> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - match *entry.tree.0 { - Node::Leaf { - ref item_summaries, .. - } => { - if entry.index() == item_summaries.len() { - None - } else { - Some(&item_summaries[entry.index()]) - } - } - _ => unreachable!(), - } - } else { - None - } - } + #[track_caller] + pub fn item_summary(&self) -> Option<&'a T::Summary> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + match *entry.tree.0 { + Node::Leaf { + ref item_summaries, .. + } => { + if entry.index() == item_summaries.len() { + None + } else { + Some(&item_summaries[entry.index()]) + } + } + _ => unreachable!(), + } + } else { + None + } + } - #[track_caller] - pub fn next_item(&self) -> Option<&'a T> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - if entry.index() == entry.tree.0.items().len() - 1 { - if let Some(next_leaf) = self.next_leaf() { - Some(next_leaf.0.items().first().unwrap()) - } else { - None - } - } else { - match *entry.tree.0 { - Node::Leaf { ref items, .. } => Some(&items[entry.index() + 1]), - _ => unreachable!(), - } - } - } else if self.at_end { - None - } else { - self.tree.first() - } - } + #[track_caller] + pub fn next_item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + if entry.index() == entry.tree.0.items().len() - 1 { + if let Some(next_leaf) = self.next_leaf() { + Some(next_leaf.0.items().first().unwrap()) + } else { + None + } + } else { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => Some(&items[entry.index() + 1]), + _ => unreachable!(), + } + } + } else if self.at_end { + None + } else { + self.tree.first() + } + } - #[track_caller] - fn next_leaf(&self) -> Option<&'a SumTree> { - for entry in self.stack.iter().rev().skip(1) { - if entry.index() < entry.tree.0.child_trees().len() - 1 { - match *entry.tree.0 { - Node::Internal { - ref child_trees, .. - } => return Some(child_trees[entry.index() + 1].leftmost_leaf()), - Node::Leaf { .. } => unreachable!(), - }; - } - } - None - } + #[track_caller] + fn next_leaf(&self) -> Option<&'a SumTree> { + for entry in self.stack.iter().rev().skip(1) { + if entry.index() < entry.tree.0.child_trees().len() - 1 { + match *entry.tree.0 { + Node::Internal { + ref child_trees, .. + } => return Some(child_trees[entry.index() + 1].leftmost_leaf()), + Node::Leaf { .. } => unreachable!(), + }; + } + } + None + } - #[track_caller] - pub fn prev_item(&self) -> Option<&'a T> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - if entry.index() == 0 { - if let Some(prev_leaf) = self.prev_leaf() { - Some(prev_leaf.0.items().last().unwrap()) - } else { - None - } - } else { - match *entry.tree.0 { - Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]), - _ => unreachable!(), - } - } - } else if self.at_end { - self.tree.last() - } else { - None - } - } + #[track_caller] + pub fn prev_item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + if entry.index() == 0 { + if let Some(prev_leaf) = self.prev_leaf() { + Some(prev_leaf.0.items().last().unwrap()) + } else { + None + } + } else { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]), + _ => unreachable!(), + } + } + } else if self.at_end { + self.tree.last() + } else { + None + } + } - #[track_caller] - fn prev_leaf(&self) -> Option<&'a SumTree> { - for entry in self.stack.iter().rev().skip(1) { - if entry.index() != 0 { - match *entry.tree.0 { - Node::Internal { - ref child_trees, .. - } => return Some(child_trees[entry.index() - 1].rightmost_leaf()), - Node::Leaf { .. } => unreachable!(), - }; - } - } - None - } + #[track_caller] + fn prev_leaf(&self) -> Option<&'a SumTree> { + for entry in self.stack.iter().rev().skip(1) { + if entry.index() != 0 { + match *entry.tree.0 { + Node::Internal { + ref child_trees, .. + } => return Some(child_trees[entry.index() - 1].rightmost_leaf()), + Node::Leaf { .. } => unreachable!(), + }; + } + } + None + } - #[track_caller] - #[instrument(skip_all)] - pub fn prev(&mut self) { - self.search_backward(|_| true) - } + #[track_caller] + #[instrument(skip_all)] + pub fn prev(&mut self) { + self.search_backward(|_| true) + } - #[track_caller] - pub fn search_backward(&mut self, mut filter_node: F) - where - F: FnMut(&T::Summary) -> bool, - { - if !self.did_seek { - self.did_seek = true; - self.at_end = true; - } + #[track_caller] + pub fn search_backward(&mut self, mut filter_node: F) + where + F: FnMut(&T::Summary) -> bool, + { + if !self.did_seek { + self.did_seek = true; + self.at_end = true; + } - if self.at_end { - self.position = D::zero(self.cx); - self.at_end = self.tree.is_empty(); - if !self.tree.is_empty() { - self.stack - .push(StackEntry { - tree: self.tree, - index: self.tree.0.child_summaries().len() as u32, - position: D::from_summary(self.tree.summary(), self.cx), - }) - .unwrap_oob(); - } - } + if self.at_end { + self.position = D::zero(self.cx); + self.at_end = self.tree.is_empty(); + if !self.tree.is_empty() { + self.stack + .push(StackEntry { + tree: self.tree, + index: self.tree.0.child_summaries().len() as u32, + position: D::from_summary(self.tree.summary(), self.cx), + }) + .unwrap_oob(); + } + } - let mut descending = false; - while !self.stack.is_empty() { - if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) { - self.position = position.clone(); - } else { - self.position = D::zero(self.cx); - } + let mut descending = false; + while !self.stack.is_empty() { + if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) { + self.position = position.clone(); + } else { + self.position = D::zero(self.cx); + } - let entry = self.stack.last_mut().unwrap(); - if !descending { - if entry.index() == 0 { - self.stack.pop(); - continue; - } else { - entry.index -= 1; - } - } + let entry = self.stack.last_mut().unwrap(); + if !descending { + if entry.index() == 0 { + self.stack.pop(); + continue; + } else { + entry.index -= 1; + } + } - for summary in &entry.tree.0.child_summaries()[..entry.index()] { - self.position.add_summary(summary, self.cx); - } - entry.position = self.position.clone(); + for summary in &entry.tree.0.child_summaries()[..entry.index()] { + self.position.add_summary(summary, self.cx); + } + entry.position = self.position.clone(); - descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]); - match entry.tree.0.as_ref() { - Node::Internal { child_trees, .. } => { - if descending { - let tree = &child_trees[entry.index()]; - self.stack - .push(StackEntry { - position: D::zero(self.cx), - tree, - index: tree.0.child_summaries().len() as u32 - 1, - }) - .unwrap_oob(); - } - } - Node::Leaf { .. } => { - if descending { - break; - } - } - } - } - } + descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]); + match entry.tree.0.as_ref() { + Node::Internal { child_trees, .. } => { + if descending { + let tree = &child_trees[entry.index()]; + self.stack + .push(StackEntry { + position: D::zero(self.cx), + tree, + index: tree.0.child_summaries().len() as u32 - 1, + }) + .unwrap_oob(); + } + } + Node::Leaf { .. } => { + if descending { + break; + } + } + } + } + } - #[track_caller] - pub fn next(&mut self) { - self.search_forward(|_| true) - } + #[track_caller] + pub fn next(&mut self) { + self.search_forward(|_| true) + } - #[track_caller] - pub fn search_forward(&mut self, mut filter_node: F) - where - F: FnMut(&T::Summary) -> bool, - { - let mut descend = false; + #[track_caller] + pub fn search_forward(&mut self, mut filter_node: F) + where + F: FnMut(&T::Summary) -> bool, + { + let mut descend = false; - if self.stack.is_empty() { - if !self.at_end { - self.stack - .push(StackEntry { - tree: self.tree, - index: 0, - position: D::zero(self.cx), - }) - .unwrap_oob(); - descend = true; - } - self.did_seek = true; - } + if self.stack.is_empty() { + if !self.at_end { + self.stack + .push(StackEntry { + tree: self.tree, + index: 0, + position: D::zero(self.cx), + }) + .unwrap_oob(); + descend = true; + } + self.did_seek = true; + } - while !self.stack.is_empty() { - let new_subtree = { - let entry = self.stack.last_mut().unwrap(); - match entry.tree.0.as_ref() { - Node::Internal { - child_trees, - child_summaries, - .. - } => { - if !descend { - entry.index += 1; - entry.position = self.position.clone(); - } + while !self.stack.is_empty() { + let new_subtree = { + let entry = self.stack.last_mut().unwrap(); + match entry.tree.0.as_ref() { + Node::Internal { + child_trees, + child_summaries, + .. + } => { + if !descend { + entry.index += 1; + entry.position = self.position.clone(); + } - while entry.index() < child_summaries.len() { - let next_summary = &child_summaries[entry.index()]; - if filter_node(next_summary) { - break; - } else { - entry.index += 1; - entry.position.add_summary(next_summary, self.cx); - self.position.add_summary(next_summary, self.cx); - } - } + while entry.index() < child_summaries.len() { + let next_summary = &child_summaries[entry.index()]; + if filter_node(next_summary) { + break; + } else { + entry.index += 1; + entry.position.add_summary(next_summary, self.cx); + self.position.add_summary(next_summary, self.cx); + } + } - child_trees.get(entry.index()) - } - Node::Leaf { item_summaries, .. } => { - if !descend { - let item_summary = &item_summaries[entry.index()]; - entry.index += 1; - entry.position.add_summary(item_summary, self.cx); - self.position.add_summary(item_summary, self.cx); - } + child_trees.get(entry.index()) + } + Node::Leaf { item_summaries, .. } => { + if !descend { + let item_summary = &item_summaries[entry.index()]; + entry.index += 1; + entry.position.add_summary(item_summary, self.cx); + self.position.add_summary(item_summary, self.cx); + } - loop { - if let Some(next_item_summary) = item_summaries.get(entry.index()) { - if filter_node(next_item_summary) { - return; - } else { - entry.index += 1; - entry.position.add_summary(next_item_summary, self.cx); - self.position.add_summary(next_item_summary, self.cx); - } - } else { - break None; - } - } - } - } - }; + loop { + if let Some(next_item_summary) = item_summaries.get(entry.index()) { + if filter_node(next_item_summary) { + return; + } else { + entry.index += 1; + entry.position.add_summary(next_item_summary, self.cx); + self.position.add_summary(next_item_summary, self.cx); + } + } else { + break None; + } + } + } + } + }; - if let Some(subtree) = new_subtree { - descend = true; - self.stack - .push(StackEntry { - tree: subtree, - index: 0, - position: self.position.clone(), - }) - .unwrap_oob(); - } else { - descend = false; - self.stack.pop(); - } - } + if let Some(subtree) = new_subtree { + descend = true; + self.stack + .push(StackEntry { + tree: subtree, + index: 0, + position: self.position.clone(), + }) + .unwrap_oob(); + } else { + descend = false; + self.stack.pop(); + } + } - self.at_end = self.stack.is_empty(); - debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); - } + self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); + } - #[track_caller] - fn assert_did_seek(&self) { - assert!( - self.did_seek, - "Must call `seek`, `next` or `prev` before calling this method" - ); - } + #[track_caller] + fn assert_did_seek(&self) { + assert!( + self.did_seek, + "Must call `seek`, `next` or `prev` before calling this method" + ); + } - pub fn did_seek(&self) -> bool { - self.did_seek - } + pub fn did_seek(&self) -> bool { + self.did_seek + } } impl<'a, 'b, T, D> Cursor<'a, 'b, T, D> where - T: Item, - D: Dimension<'a, T::Summary>, + T: Item, + D: Dimension<'a, T::Summary>, { - /// Returns whether we found the item you were seeking for. - #[track_caller] - #[instrument(skip_all)] - pub fn seek(&mut self, pos: &Target, bias: Bias) -> bool - where - Target: SeekTarget<'a, T::Summary, D>, - { - self.reset(); - self.seek_internal(pos, bias, &mut ()) - } + /// Returns whether we found the item you were seeking for. + #[track_caller] + #[instrument(skip_all)] + pub fn seek(&mut self, pos: &Target, bias: Bias) -> bool + where + Target: SeekTarget<'a, T::Summary, D>, + { + self.reset(); + self.seek_internal(pos, bias, &mut ()) + } - /// Returns whether we found the item you were seeking for. - /// - /// # Panics - /// - /// If we did not seek before, use seek instead in that case. - #[track_caller] - #[instrument(skip_all)] - pub fn seek_forward(&mut self, pos: &Target, bias: Bias) -> bool - where - Target: SeekTarget<'a, T::Summary, D>, - { - self.seek_internal(pos, bias, &mut ()) - } + /// Returns whether we found the item you were seeking for. + /// + /// # Panics + /// + /// If we did not seek before, use seek instead in that case. + #[track_caller] + #[instrument(skip_all)] + pub fn seek_forward(&mut self, pos: &Target, bias: Bias) -> bool + where + Target: SeekTarget<'a, T::Summary, D>, + { + self.seek_internal(pos, bias, &mut ()) + } - /// Advances the cursor and returns traversed items as a tree. - #[track_caller] - pub fn slice(&mut self, end: &Target, bias: Bias) -> SumTree - where - Target: SeekTarget<'a, T::Summary, D>, - { - let mut slice = SliceSeekAggregate { - tree: SumTree::new(self.cx), - leaf_items: ArrayVec::new(), - leaf_item_summaries: ArrayVec::new(), - leaf_summary: ::zero(self.cx), - }; - self.seek_internal(end, bias, &mut slice); - slice.tree - } + /// Advances the cursor and returns traversed items as a tree. + #[track_caller] + pub fn slice(&mut self, end: &Target, bias: Bias) -> SumTree + where + Target: SeekTarget<'a, T::Summary, D>, + { + let mut slice = SliceSeekAggregate { + tree: SumTree::new(self.cx), + leaf_items: ArrayVec::new(), + leaf_item_summaries: ArrayVec::new(), + leaf_summary: ::zero(self.cx), + }; + self.seek_internal(end, bias, &mut slice); + slice.tree + } - #[track_caller] - pub fn suffix(&mut self) -> SumTree { - self.slice(&End::new(), Bias::Right) - } + #[track_caller] + pub fn suffix(&mut self) -> SumTree { + self.slice(&End::new(), Bias::Right) + } - #[track_caller] - pub fn summary(&mut self, end: &Target, bias: Bias) -> Output - where - Target: SeekTarget<'a, T::Summary, D>, - Output: Dimension<'a, T::Summary>, - { - let mut summary = SummarySeekAggregate(Output::zero(self.cx)); - self.seek_internal(end, bias, &mut summary); - summary.0 - } + #[track_caller] + pub fn summary(&mut self, end: &Target, bias: Bias) -> Output + where + Target: SeekTarget<'a, T::Summary, D>, + Output: Dimension<'a, T::Summary>, + { + let mut summary = SummarySeekAggregate(Output::zero(self.cx)); + self.seek_internal(end, bias, &mut summary); + summary.0 + } - /// Returns whether we found the item you were seeking for. - #[track_caller] - #[instrument(skip_all)] - fn seek_internal( - &mut self, - target: &dyn SeekTarget<'a, T::Summary, D>, - bias: Bias, - aggregate: &mut dyn SeekAggregate<'a, T>, - ) -> bool { - assert!( - target.cmp(&self.position, self.cx).is_ge(), - "cannot seek backward", - ); + /// Returns whether we found the item you were seeking for. + #[track_caller] + #[instrument(skip_all)] + fn seek_internal( + &mut self, + target: &dyn SeekTarget<'a, T::Summary, D>, + bias: Bias, + aggregate: &mut dyn SeekAggregate<'a, T>, + ) -> bool { + assert!( + target.cmp(&self.position, self.cx).is_ge(), + "cannot seek backward", + ); - if !self.did_seek { - self.did_seek = true; - self.stack - .push(StackEntry { - tree: self.tree, - index: 0, - position: D::zero(self.cx), - }) - .unwrap_oob(); - } + if !self.did_seek { + self.did_seek = true; + self.stack + .push(StackEntry { + tree: self.tree, + index: 0, + position: D::zero(self.cx), + }) + .unwrap_oob(); + } - let mut ascending = false; - 'outer: while let Some(entry) = self.stack.last_mut() { - match *entry.tree.0 { - Node::Internal { - ref child_summaries, - ref child_trees, - .. - } => { - if ascending { - entry.index += 1; - entry.position = self.position.clone(); - } + let mut ascending = false; + 'outer: while let Some(entry) = self.stack.last_mut() { + match *entry.tree.0 { + Node::Internal { + ref child_summaries, + ref child_trees, + .. + } => { + if ascending { + entry.index += 1; + entry.position = self.position.clone(); + } - for (child_tree, child_summary) in child_trees[entry.index()..] - .iter() - .zip(&child_summaries[entry.index()..]) - { - let mut child_end = self.position.clone(); - child_end.add_summary(child_summary, self.cx); + for (child_tree, child_summary) in child_trees[entry.index()..] + .iter() + .zip(&child_summaries[entry.index()..]) + { + let mut child_end = self.position.clone(); + child_end.add_summary(child_summary, self.cx); - let comparison = target.cmp(&child_end, self.cx); - if comparison == Ordering::Greater - || (comparison == Ordering::Equal && bias == Bias::Right) - { - self.position = child_end; - aggregate.push_tree(child_tree, child_summary, self.cx); - entry.index += 1; - entry.position = self.position.clone(); - } else { - self.stack - .push(StackEntry { - tree: child_tree, - index: 0, - position: self.position.clone(), - }) - .unwrap_oob(); - ascending = false; - continue 'outer; - } - } - } - Node::Leaf { - ref items, - ref item_summaries, - .. - } => { - aggregate.begin_leaf(); + let comparison = target.cmp(&child_end, self.cx); + if comparison == Ordering::Greater + || (comparison == Ordering::Equal && bias == Bias::Right) + { + self.position = child_end; + aggregate.push_tree(child_tree, child_summary, self.cx); + entry.index += 1; + entry.position = self.position.clone(); + } else { + self.stack + .push(StackEntry { + tree: child_tree, + index: 0, + position: self.position.clone(), + }) + .unwrap_oob(); + ascending = false; + continue 'outer; + } + } + } + Node::Leaf { + ref items, + ref item_summaries, + .. + } => { + aggregate.begin_leaf(); - for (item, item_summary) in items[entry.index()..] - .iter() - .zip(&item_summaries[entry.index()..]) - { - let mut child_end = self.position.clone(); - child_end.add_summary(item_summary, self.cx); + for (item, item_summary) in items[entry.index()..] + .iter() + .zip(&item_summaries[entry.index()..]) + { + let mut child_end = self.position.clone(); + child_end.add_summary(item_summary, self.cx); - let comparison = target.cmp(&child_end, self.cx); - if comparison == Ordering::Greater - || (comparison == Ordering::Equal && bias == Bias::Right) - { - self.position = child_end; - aggregate.push_item(item, item_summary, self.cx); - entry.index += 1; - } else { - aggregate.end_leaf(self.cx); - break 'outer; - } - } + let comparison = target.cmp(&child_end, self.cx); + if comparison == Ordering::Greater + || (comparison == Ordering::Equal && bias == Bias::Right) + { + self.position = child_end; + aggregate.push_item(item, item_summary, self.cx); + entry.index += 1; + } else { + aggregate.end_leaf(self.cx); + break 'outer; + } + } - aggregate.end_leaf(self.cx); - } - } + aggregate.end_leaf(self.cx); + } + } - self.stack.pop(); - ascending = true; - } + self.stack.pop(); + ascending = true; + } - self.at_end = self.stack.is_empty(); - debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); + self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); - let mut end = self.position.clone(); - if bias == Bias::Left - && let Some(summary) = self.item_summary() - { - end.add_summary(summary, self.cx); - } + let mut end = self.position.clone(); + if bias == Bias::Left + && let Some(summary) = self.item_summary() + { + end.add_summary(summary, self.cx); + } - target.cmp(&end, self.cx) == Ordering::Equal - } + target.cmp(&end, self.cx) == Ordering::Equal + } } impl<'a, T: Item> Iter<'a, T> { - pub(crate) fn new(tree: &'a SumTree) -> Self { - Self { - tree, - stack: Default::default(), - } - } + pub(crate) fn new(tree: &'a SumTree) -> Self { + Self { + tree, + stack: Default::default(), + } + } } impl<'a, T: Item> Iterator for Iter<'a, T> { - type Item = &'a T; + type Item = &'a T; - fn next(&mut self) -> Option { - let mut descend = false; + fn next(&mut self) -> Option { + let mut descend = false; - if self.stack.is_empty() { - self.stack - .push(StackEntry { - tree: self.tree, - index: 0, - position: (), - }) - .unwrap_oob(); - descend = true; - } + if self.stack.is_empty() { + self.stack + .push(StackEntry { + tree: self.tree, + index: 0, + position: (), + }) + .unwrap_oob(); + descend = true; + } - while let Some(entry) = self.stack.last_mut() { - let new_subtree = { - match entry.tree.0.as_ref() { - Node::Internal { child_trees, .. } => { - if !descend { - entry.index += 1; - } - child_trees.get(entry.index()) - } - Node::Leaf { items, .. } => { - if !descend { - entry.index += 1; - } + while let Some(entry) = self.stack.last_mut() { + let new_subtree = { + match entry.tree.0.as_ref() { + Node::Internal { child_trees, .. } => { + if !descend { + entry.index += 1; + } + child_trees.get(entry.index()) + } + Node::Leaf { items, .. } => { + if !descend { + entry.index += 1; + } - if let Some(next_item) = items.get(entry.index()) { - return Some(next_item); - } else { - None - } - } - } - }; + if let Some(next_item) = items.get(entry.index()) { + return Some(next_item); + } else { + None + } + } + } + }; - if let Some(subtree) = new_subtree { - descend = true; - self.stack - .push(StackEntry { - tree: subtree, - index: 0, - position: (), - }) - .unwrap_oob(); - } else { - descend = false; - self.stack.pop(); - } - } + if let Some(subtree) = new_subtree { + descend = true; + self.stack + .push(StackEntry { + tree: subtree, + index: 0, + position: (), + }) + .unwrap_oob(); + } else { + descend = false; + self.stack.pop(); + } + } - None - } + None + } - fn last(mut self) -> Option { - self.stack.clear(); - self.tree.rightmost_leaf().last() - } + fn last(mut self) -> Option { + self.stack.clear(); + self.tree.rightmost_leaf().last() + } - fn size_hint(&self) -> (usize, Option) { - let lower_bound = match self.stack.last() { - Some(top) => top.tree.0.child_summaries().len() - top.index as usize, - None => self.tree.0.child_summaries().len(), - }; + fn size_hint(&self) -> (usize, Option) { + let lower_bound = match self.stack.last() { + Some(top) => top.tree.0.child_summaries().len() - top.index as usize, + None => self.tree.0.child_summaries().len(), + }; - (lower_bound, None) - } + (lower_bound, None) + } } impl<'a, 'b, T: Item, D> Iterator for Cursor<'a, 'b, T, D> where - D: Dimension<'a, T::Summary>, + D: Dimension<'a, T::Summary>, { - type Item = &'a T; + type Item = &'a T; - fn next(&mut self) -> Option { - if !self.did_seek { - self.next(); - } + fn next(&mut self) -> Option { + if !self.did_seek { + self.next(); + } - if let Some(item) = self.item() { - self.next(); - Some(item) - } else { - None - } - } + if let Some(item) = self.item() { + self.next(); + Some(item) + } else { + None + } + } } pub struct FilterCursor<'a, 'b, F, T: Item, D> { - cursor: Cursor<'a, 'b, T, D>, - filter_node: F, + cursor: Cursor<'a, 'b, T, D>, + filter_node: F, } impl<'a, 'b, F, T: Item, D> FilterCursor<'a, 'b, F, T, D> where - F: FnMut(&T::Summary) -> bool, - T: Item, - D: Dimension<'a, T::Summary>, + F: FnMut(&T::Summary) -> bool, + T: Item, + D: Dimension<'a, T::Summary>, { - pub fn new( - tree: &'a SumTree, - cx: ::Context<'b>, - filter_node: F, - ) -> Self { - let cursor = tree.cursor::(cx); - Self { - cursor, - filter_node, - } - } + pub fn new( + tree: &'a SumTree, + cx: ::Context<'b>, + filter_node: F, + ) -> Self { + let cursor = tree.cursor::(cx); + Self { + cursor, + filter_node, + } + } - pub fn start(&self) -> &D { - self.cursor.start() - } + pub fn start(&self) -> &D { + self.cursor.start() + } - pub fn end(&self) -> D { - self.cursor.end() - } + pub fn end(&self) -> D { + self.cursor.end() + } - pub fn item(&self) -> Option<&'a T> { - self.cursor.item() - } + pub fn item(&self) -> Option<&'a T> { + self.cursor.item() + } - pub fn item_summary(&self) -> Option<&'a T::Summary> { - self.cursor.item_summary() - } + pub fn item_summary(&self) -> Option<&'a T::Summary> { + self.cursor.item_summary() + } - pub fn next(&mut self) { - self.cursor.search_forward(&mut self.filter_node); - } + pub fn next(&mut self) { + self.cursor.search_forward(&mut self.filter_node); + } - pub fn prev(&mut self) { - self.cursor.search_backward(&mut self.filter_node); - } + pub fn prev(&mut self) { + self.cursor.search_backward(&mut self.filter_node); + } } impl<'a, 'b, F, T: Item, U> Iterator for FilterCursor<'a, 'b, F, T, U> where - F: FnMut(&T::Summary) -> bool, - U: Dimension<'a, T::Summary>, + F: FnMut(&T::Summary) -> bool, + U: Dimension<'a, T::Summary>, { - type Item = &'a T; + type Item = &'a T; - fn next(&mut self) -> Option { - if !self.cursor.did_seek { - self.next(); - } + fn next(&mut self) -> Option { + if !self.cursor.did_seek { + self.next(); + } - if let Some(item) = self.item() { - self.cursor.search_forward(&mut self.filter_node); - Some(item) - } else { - None - } - } + if let Some(item) = self.item() { + self.cursor.search_forward(&mut self.filter_node); + Some(item) + } else { + None + } + } } trait SeekAggregate<'a, T: Item> { - fn begin_leaf(&mut self); - fn end_leaf(&mut self, cx: ::Context<'_>); - fn push_item( - &mut self, - item: &'a T, - summary: &'a T::Summary, - cx: ::Context<'_>, - ); - fn push_tree( - &mut self, - tree: &'a SumTree, - summary: &'a T::Summary, - cx: ::Context<'_>, - ); + fn begin_leaf(&mut self); + fn end_leaf(&mut self, cx: ::Context<'_>); + fn push_item( + &mut self, + item: &'a T, + summary: &'a T::Summary, + cx: ::Context<'_>, + ); + fn push_tree( + &mut self, + tree: &'a SumTree, + summary: &'a T::Summary, + cx: ::Context<'_>, + ); } struct SliceSeekAggregate { - tree: SumTree, - leaf_items: ArrayVec, - leaf_item_summaries: ArrayVec, - leaf_summary: T::Summary, + tree: SumTree, + leaf_items: ArrayVec, + leaf_item_summaries: ArrayVec, + leaf_summary: T::Summary, } struct SummarySeekAggregate(D); impl SeekAggregate<'_, T> for () { - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, _: ::Context<'_>) {} - fn push_item(&mut self, _: &T, _: &T::Summary, _: ::Context<'_>) {} - fn push_tree( - &mut self, - _: &SumTree, - _: &T::Summary, - _: ::Context<'_>, - ) { - } + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, _: ::Context<'_>) {} + fn push_item(&mut self, _: &T, _: &T::Summary, _: ::Context<'_>) {} + fn push_tree( + &mut self, + _: &SumTree, + _: &T::Summary, + _: ::Context<'_>, + ) { + } } impl SeekAggregate<'_, T> for SliceSeekAggregate { - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, cx: ::Context<'_>) { - self.tree.append( - SumTree(Arc::new(Node::Leaf { - summary: mem::replace(&mut self.leaf_summary, ::zero(cx)), - items: mem::take(&mut self.leaf_items), - item_summaries: mem::take(&mut self.leaf_item_summaries), - })), - cx, - ); - } - fn push_item( - &mut self, - item: &T, - summary: &T::Summary, - cx: ::Context<'_>, - ) { - self.leaf_items.push(item.clone()).unwrap_oob(); - self.leaf_item_summaries.push(summary.clone()).unwrap_oob(); - Summary::add_summary(&mut self.leaf_summary, summary, cx); - } - fn push_tree( - &mut self, - tree: &SumTree, - _: &T::Summary, - cx: ::Context<'_>, - ) { - self.tree.append(tree.clone(), cx); - } + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, cx: ::Context<'_>) { + self.tree.append( + SumTree(Arc::new(Node::Leaf { + summary: mem::replace(&mut self.leaf_summary, ::zero(cx)), + items: mem::take(&mut self.leaf_items), + item_summaries: mem::take(&mut self.leaf_item_summaries), + })), + cx, + ); + } + fn push_item( + &mut self, + item: &T, + summary: &T::Summary, + cx: ::Context<'_>, + ) { + self.leaf_items.push(item.clone()).unwrap_oob(); + self.leaf_item_summaries.push(summary.clone()).unwrap_oob(); + Summary::add_summary(&mut self.leaf_summary, summary, cx); + } + fn push_tree( + &mut self, + tree: &SumTree, + _: &T::Summary, + cx: ::Context<'_>, + ) { + self.tree.append(tree.clone(), cx); + } } impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate where - D: Dimension<'a, T::Summary>, + D: Dimension<'a, T::Summary>, { - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, _: ::Context<'_>) {} - fn push_item( - &mut self, - _: &T, - summary: &'a T::Summary, - cx: ::Context<'_>, - ) { - self.0.add_summary(summary, cx); - } - fn push_tree( - &mut self, - _: &SumTree, - summary: &'a T::Summary, - cx: ::Context<'_>, - ) { - self.0.add_summary(summary, cx); - } + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, _: ::Context<'_>) {} + fn push_item( + &mut self, + _: &T, + summary: &'a T::Summary, + cx: ::Context<'_>, + ) { + self.0.add_summary(summary, cx); + } + fn push_tree( + &mut self, + _: &SumTree, + summary: &'a T::Summary, + cx: ::Context<'_>, + ) { + self.0.add_summary(summary, cx); + } } struct End(PhantomData); impl End { - fn new() -> Self { - Self(PhantomData) - } + fn new() -> Self { + Self(PhantomData) + } } impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End { - fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering { - Ordering::Greater - } + fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering { + Ordering::Greater + } } impl fmt::Debug for End { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("End").finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("End").finish() + } } diff --git a/crates/gpui_sum_tree/src/property_test.rs b/crates/gpui_sum_tree/src/property_test.rs index d6c6bd76f9..f08a377064 100644 --- a/crates/gpui_sum_tree/src/property_test.rs +++ b/crates/gpui_sum_tree/src/property_test.rs @@ -6,17 +6,17 @@ use crate::{Item, SumTree, Summary}; impl Arbitrary for SumTree where - T: Debug + Arbitrary + Item + 'static, - T::Summary: Debug + Summary = ()>, + T: Debug + Arbitrary + Item + 'static, + T::Summary: Debug + Summary = ()>, { - type Parameters = (); - type Strategy = BoxedStrategy; + type Parameters = (); + type Strategy = BoxedStrategy; - fn arbitrary_with((): Self::Parameters) -> Self::Strategy { - any::>() - .prop_map(|vec| SumTree::from_iter(vec, ())) - .boxed() - } + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::>() + .prop_map(|vec| SumTree::from_iter(vec, ())) + .boxed() + } } /// A strategy for producing a [`SumTree`] with a given size. @@ -24,9 +24,9 @@ where /// Equivalent to [`proptest::collection::vec`]. pub fn sum_tree(values: S, size: impl Into) -> impl Strategy> where - T: Debug + Arbitrary + Item + 'static, - T::Summary: Debug + Summary = ()>, - S: Strategy, + T: Debug + Arbitrary + Item + 'static, + T::Summary: Debug + Summary = ()>, + S: Strategy, { - proptest::collection::vec(values, size).prop_map(|vec| SumTree::from_iter(vec, ())) + proptest::collection::vec(values, size).prop_map(|vec| SumTree::from_iter(vec, ())) } diff --git a/crates/gpui_sum_tree/src/sum_tree.rs b/crates/gpui_sum_tree/src/sum_tree.rs index 8b20e605c0..e45058e108 100644 --- a/crates/gpui_sum_tree/src/sum_tree.rs +++ b/crates/gpui_sum_tree/src/sum_tree.rs @@ -19,29 +19,29 @@ pub const TREE_BASE: usize = 6; // Helper for when we cannot use ArrayVec::::push().unwrap() as T doesn't impl Debug trait CapacityResultExt { - fn unwrap_oob(self); + fn unwrap_oob(self); } impl CapacityResultExt for Result<(), T> { - fn unwrap_oob(self) { - self.unwrap_or_else(|_| panic!("item should fit into fixed size ArrayVec")) - } + fn unwrap_oob(self) { + self.unwrap_or_else(|_| panic!("item should fit into fixed size ArrayVec")) + } } /// An item that can be stored in a [`SumTree`] /// /// Must be summarized by a type that implements [`Summary`] pub trait Item: Clone { - type Summary: Summary; + type Summary: Summary; - fn summary(&self, cx: ::Context<'_>) -> Self::Summary; + fn summary(&self, cx: ::Context<'_>) -> Self::Summary; } /// An [`Item`] whose summary has a specific key that can be used to identify it pub trait KeyedItem: Item { - type Key: for<'a> Dimension<'a, Self::Summary> + Ord; + type Key: for<'a> Dimension<'a, Self::Summary> + Ord; - fn key(&self) -> Self::Key; + fn key(&self) -> Self::Key; } /// A type that describes the Sum of all [`Item`]s in a subtree of the [`SumTree`] @@ -49,26 +49,26 @@ pub trait KeyedItem: Item { /// Each Summary type can have multiple [`Dimension`]s that it measures, /// which can be used to navigate the tree pub trait Summary: Clone { - type Context<'a>: Copy; - fn zero<'a>(cx: Self::Context<'a>) -> Self; - fn add_summary<'a>(&mut self, summary: &Self, cx: Self::Context<'a>); + type Context<'a>: Copy; + fn zero<'a>(cx: Self::Context<'a>) -> Self; + fn add_summary<'a>(&mut self, summary: &Self, cx: Self::Context<'a>); } pub trait ContextLessSummary: Clone { - fn zero() -> Self; - fn add_summary(&mut self, summary: &Self); + fn zero() -> Self; + fn add_summary(&mut self, summary: &Self); } impl Summary for T { - type Context<'a> = (); + type Context<'a> = (); - fn zero<'a>((): ()) -> Self { - T::zero() - } + fn zero<'a>((): ()) -> Self { + T::zero() + } - fn add_summary<'a>(&mut self, summary: &Self, (): ()) { - T::add_summary(self, summary) - } + fn add_summary<'a>(&mut self, summary: &Self, (): ()) { + T::add_summary(self, summary) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -78,11 +78,11 @@ pub struct NoSummary; /// We implement it on a `NoSummary` instead of re-using `()`, as that avoids blanket impl collisions with `impl Dimension for T` /// (as we also need unit type to be a fill-in dimension) impl ContextLessSummary for NoSummary { - fn zero() -> Self { - NoSummary - } + fn zero() -> Self { + NoSummary + } - fn add_summary(&mut self, _: &Self) {} + fn add_summary(&mut self, _: &Self) {} } /// Each [`Summary`] type can have more than one [`Dimension`] type that it measures. @@ -93,75 +93,75 @@ impl ContextLessSummary for NoSummary { /// Zed's rope has a `TextSummary` type that summarizes lines, characters, and bytes. /// Each of these are different dimensions we may want to seek to pub trait Dimension<'a, S: Summary>: Clone { - fn zero(cx: S::Context<'_>) -> Self; + fn zero(cx: S::Context<'_>) -> Self; - fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>); - #[must_use] - fn with_added_summary(mut self, summary: &'a S, cx: S::Context<'_>) -> Self { - self.add_summary(summary, cx); - self - } + fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>); + #[must_use] + fn with_added_summary(mut self, summary: &'a S, cx: S::Context<'_>) -> Self { + self.add_summary(summary, cx); + self + } - fn from_summary(summary: &'a S, cx: S::Context<'_>) -> Self { - let mut dimension = Self::zero(cx); - dimension.add_summary(summary, cx); - dimension - } + fn from_summary(summary: &'a S, cx: S::Context<'_>) -> Self { + let mut dimension = Self::zero(cx); + dimension.add_summary(summary, cx); + dimension + } } impl<'a, T: Summary> Dimension<'a, T> for T { - fn zero(cx: T::Context<'_>) -> Self { - Summary::zero(cx) - } + fn zero(cx: T::Context<'_>) -> Self { + Summary::zero(cx) + } - fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { - Summary::add_summary(self, summary, cx); - } + fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { + Summary::add_summary(self, summary, cx); + } } pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>> { - fn cmp(&self, cursor_location: &D, cx: S::Context<'_>) -> Ordering; + fn cmp(&self, cursor_location: &D, cx: S::Context<'_>) -> Ordering; } impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { - fn cmp(&self, cursor_location: &Self, _: S::Context<'_>) -> Ordering { - Ord::cmp(self, cursor_location) - } + fn cmp(&self, cursor_location: &Self, _: S::Context<'_>) -> Ordering { + Ord::cmp(self, cursor_location) + } } impl<'a, T: Summary> Dimension<'a, T> for () { - fn zero(_: T::Context<'_>) -> Self {} + fn zero(_: T::Context<'_>) -> Self {} - fn add_summary(&mut self, _: &'a T, _: T::Context<'_>) {} + fn add_summary(&mut self, _: &'a T, _: T::Context<'_>) {} } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Dimensions(pub D1, pub D2, pub D3); impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>, D3: Dimension<'a, T>> - Dimension<'a, T> for Dimensions + Dimension<'a, T> for Dimensions { - fn zero(cx: T::Context<'_>) -> Self { - Dimensions(D1::zero(cx), D2::zero(cx), D3::zero(cx)) - } + fn zero(cx: T::Context<'_>) -> Self { + Dimensions(D1::zero(cx), D2::zero(cx), D3::zero(cx)) + } - fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { - self.0.add_summary(summary, cx); - self.1.add_summary(summary, cx); - self.2.add_summary(summary, cx); - } + fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { + self.0.add_summary(summary, cx); + self.1.add_summary(summary, cx); + self.2.add_summary(summary, cx); + } } impl<'a, S, D1, D2, D3> SeekTarget<'a, S, Dimensions> for D1 where - S: Summary, - D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, - D2: Dimension<'a, S>, - D3: Dimension<'a, S>, + S: Summary, + D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, + D2: Dimension<'a, S>, + D3: Dimension<'a, S>, { - fn cmp(&self, cursor_location: &Dimensions, cx: S::Context<'_>) -> Ordering { - self.cmp(&cursor_location.0, cx) - } + fn cmp(&self, cursor_location: &Dimensions, cx: S::Context<'_>) -> Ordering { + self.cmp(&cursor_location.0, cx) + } } /// Bias is used to settle ambiguities when determining positions in an ordered sequence. @@ -187,20 +187,20 @@ where /// and the buffer offset would be the offset of the first character of the folded region #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)] pub enum Bias { - /// Attach to the character on the left - #[default] - Left, - /// Attach to the character on the right - Right, + /// Attach to the character on the left + #[default] + Left, + /// Attach to the character on the right + Right, } impl Bias { - pub fn invert(self) -> Self { - match self { - Self::Left => Self::Right, - Self::Right => Self::Left, - } - } + pub fn invert(self) -> Self { + match self { + Self::Left => Self::Right, + Self::Right => Self::Left, + } + } } /// A B+ tree in which each leaf node contains `Item`s of type `T` and a `Summary`s for each `Item`. @@ -214,1685 +214,1685 @@ pub struct SumTree(Arc>); impl fmt::Debug for SumTree where - T: fmt::Debug + Item, - T::Summary: fmt::Debug, + T: fmt::Debug + Item, + T::Summary: fmt::Debug, { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("SumTree").field(&self.0).finish() - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("SumTree").field(&self.0).finish() + } } impl SumTree { - pub fn new(cx: ::Context<'_>) -> Self { - SumTree(Arc::new(Node::Leaf { - summary: ::zero(cx), - items: ArrayVec::new(), - item_summaries: ArrayVec::new(), - })) - } - - /// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context. - pub fn from_summary(summary: T::Summary) -> Self { - SumTree(Arc::new(Node::Leaf { - summary, - items: ArrayVec::new(), - item_summaries: ArrayVec::new(), - })) - } - - pub fn from_item(item: T, cx: ::Context<'_>) -> Self { - let mut tree = Self::new(cx); - tree.push(item, cx); - tree - } - - pub fn from_iter>( - iter: I, - cx: ::Context<'_>, - ) -> Self { - let mut nodes = Vec::new(); - - let mut iter = iter.into_iter().fuse().peekable(); - while iter.peek().is_some() { - let items: ArrayVec = - iter.by_ref().take(2 * TREE_BASE).collect(); - let item_summaries: ArrayVec = - items.iter().map(|item| item.summary(cx)).collect(); - - let mut summary = item_summaries[0].clone(); - for item_summary in &item_summaries[1..] { - ::add_summary(&mut summary, item_summary, cx); - } - - nodes.push(SumTree(Arc::new(Node::Leaf { - summary, - items, - item_summaries, - }))); - } - - let mut parent_nodes = Vec::new(); - let mut height = 0; - while nodes.len() > 1 { - height += 1; - let mut current_parent_node = None; - for child_node in nodes.drain(..) { - let parent_node = current_parent_node.get_or_insert_with(|| { - SumTree(Arc::new(Node::Internal { - summary: ::zero(cx), - height, - child_summaries: ArrayVec::new(), - child_trees: ArrayVec::new(), - })) - }); - let Node::Internal { - summary, - child_summaries, - child_trees, - .. - } = Arc::get_mut(&mut parent_node.0).unwrap() - else { - unreachable!() - }; - let child_summary = child_node.summary(); - ::add_summary(summary, child_summary, cx); - child_summaries.push(child_summary.clone()).unwrap_oob(); - child_trees.push(child_node.clone()).unwrap_oob(); - - if child_trees.len() == 2 * TREE_BASE { - parent_nodes.extend(current_parent_node.take()); - } - } - parent_nodes.extend(current_parent_node.take()); - mem::swap(&mut nodes, &mut parent_nodes); - } - - if nodes.is_empty() { - Self::new(cx) - } else { - debug_assert_eq!(nodes.len(), 1); - nodes.pop().unwrap() - } - } - - pub fn from_par_iter(iter: I, cx: ::Context<'_>) -> Self - where - I: IntoParallelIterator, - Iter: IndexedParallelIterator, - T: Send + Sync, - T::Summary: Send + Sync, - for<'a> ::Context<'a>: Sync, - { - let mut nodes = iter - .into_par_iter() - .chunks(2 * TREE_BASE) - .map(|items| { - let items: ArrayVec = items.into_iter().collect(); - let item_summaries: ArrayVec = - items.iter().map(|item| item.summary(cx)).collect(); - let mut summary = item_summaries[0].clone(); - for item_summary in &item_summaries[1..] { - ::add_summary(&mut summary, item_summary, cx); - } - SumTree(Arc::new(Node::Leaf { - summary, - items, - item_summaries, - })) - }) - .collect::>(); - - let mut height = 0; - while nodes.len() > 1 { - height += 1; - nodes = nodes - .into_par_iter() - .chunks(2 * TREE_BASE) - .map(|child_nodes| { - let child_trees: ArrayVec, { 2 * TREE_BASE }, u8> = - child_nodes.into_iter().collect(); - let child_summaries: ArrayVec = child_trees - .iter() - .map(|child_tree| child_tree.summary().clone()) - .collect(); - let mut summary = child_summaries[0].clone(); - for child_summary in &child_summaries[1..] { - ::add_summary(&mut summary, child_summary, cx); - } - SumTree(Arc::new(Node::Internal { - height, - summary, - child_summaries, - child_trees, - })) - }) - .collect::>(); - } - - if nodes.is_empty() { - Self::new(cx) - } else { - debug_assert_eq!(nodes.len(), 1); - nodes.pop().unwrap() - } - } - - #[allow(unused)] - pub fn items<'a>(&'a self, cx: ::Context<'a>) -> Vec { - let mut items = Vec::new(); - let mut cursor = self.cursor::<()>(cx); - cursor.next(); - while let Some(item) = cursor.item() { - items.push(item.clone()); - cursor.next(); - } - items - } - - pub fn iter(&self) -> Iter<'_, T> { - Iter::new(self) - } - - /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`. - /// - /// Only returns the item that exactly has the target match. - #[instrument(skip_all)] - pub fn find_exact<'a, 'slf, D, Target>( - &'slf self, - cx: ::Context<'a>, - target: &Target, - bias: Bias, - ) -> (D, D, Option<&'slf T>) - where - D: Dimension<'slf, T::Summary>, - Target: SeekTarget<'slf, T::Summary, D>, - { - let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); - let comparison = target.cmp(&tree_end, cx); - if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) - { - return (tree_end.clone(), tree_end, None); - } - - let mut pos = D::zero(cx); - return match Self::find_iterate::<_, _, true>(cx, target, bias, &mut pos, self) { - Some((item, end)) => (pos, end, Some(item)), - None => (pos.clone(), pos, None), - }; - } - - /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` - #[instrument(skip_all)] - pub fn find<'a, 'slf, D, Target>( - &'slf self, - cx: ::Context<'a>, - target: &Target, - bias: Bias, - ) -> (D, D, Option<&'slf T>) - where - D: Dimension<'slf, T::Summary>, - Target: SeekTarget<'slf, T::Summary, D>, - { - let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); - let comparison = target.cmp(&tree_end, cx); - if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) - { - return (tree_end.clone(), tree_end, None); - } - - let mut pos = D::zero(cx); - return match Self::find_iterate::<_, _, false>(cx, target, bias, &mut pos, self) { - Some((item, end)) => (pos, end, Some(item)), - None => (pos.clone(), pos, None), - }; - } - - fn find_iterate<'tree, 'a, D, Target, const EXACT: bool>( - cx: ::Context<'a>, - target: &Target, - bias: Bias, - position: &mut D, - mut this: &'tree SumTree, - ) -> Option<(&'tree T, D)> - where - D: Dimension<'tree, T::Summary>, - Target: SeekTarget<'tree, T::Summary, D>, - { - 'iterate: loop { - match &*this.0 { - Node::Internal { - child_summaries, - child_trees, - .. - } => { - for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { - let child_end = position.clone().with_added_summary(child_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let target_in_child = comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left); - if target_in_child { - this = child_tree; - continue 'iterate; - } - *position = child_end; - } - } - Node::Leaf { - items, - item_summaries, - .. - } => { - for (item, item_summary) in items.iter().zip(item_summaries) { - let mut child_end = position.clone(); - child_end.add_summary(item_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let entry_found = if EXACT { - comparison == Ordering::Equal - } else { - comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left) - }; - if entry_found { - return Some((item, child_end)); - } - - *position = child_end; - } - } - } - return None; - } - } - - /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` - #[instrument(skip_all)] - pub fn find_with_prev<'a, 'slf, D, Target>( - &'slf self, - cx: ::Context<'a>, - target: &Target, - bias: Bias, - ) -> (D, D, Option<(Option<&'slf T>, &'slf T)>) - where - D: Dimension<'slf, T::Summary>, - Target: SeekTarget<'slf, T::Summary, D>, - { - let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); - let comparison = target.cmp(&tree_end, cx); - if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) - { - return (tree_end.clone(), tree_end, None); - } - - let mut pos = D::zero(cx); - return match Self::find_with_prev_iterate::<_, _, false>(cx, target, bias, &mut pos, self) { - Some((prev, item, end)) => (pos, end, Some((prev, item))), - None => (pos.clone(), pos, None), - }; - } - - fn find_with_prev_iterate<'tree, 'a, D, Target, const EXACT: bool>( - cx: ::Context<'a>, - target: &Target, - bias: Bias, - position: &mut D, - mut this: &'tree SumTree, - ) -> Option<(Option<&'tree T>, &'tree T, D)> - where - D: Dimension<'tree, T::Summary>, - Target: SeekTarget<'tree, T::Summary, D>, - { - let mut prev = None; - 'iterate: loop { - match &*this.0 { - Node::Internal { - child_summaries, - child_trees, - .. - } => { - for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { - let child_end = position.clone().with_added_summary(child_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let target_in_child = comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left); - if target_in_child { - this = child_tree; - continue 'iterate; - } - prev = child_tree.last(); - *position = child_end; - } - } - Node::Leaf { - items, - item_summaries, - .. - } => { - for (item, item_summary) in items.iter().zip(item_summaries) { - let mut child_end = position.clone(); - child_end.add_summary(item_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let entry_found = if EXACT { - comparison == Ordering::Equal - } else { - comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left) - }; - if entry_found { - return Some((prev, item, child_end)); - } - - prev = Some(item); - *position = child_end; - } - } - } - return None; - } - } - - pub fn cursor<'a, 'b, D>( - &'a self, - cx: ::Context<'b>, - ) -> Cursor<'a, 'b, T, D> - where - D: Dimension<'a, T::Summary>, - { - Cursor::new(self, cx) - } - - /// Note: If the summary type requires a non `()` context, then the filter cursor - /// that is returned cannot be used with Rust's iterators. - pub fn filter<'a, 'b, F, U>( - &'a self, - cx: ::Context<'b>, - filter_node: F, - ) -> FilterCursor<'a, 'b, F, T, U> - where - F: FnMut(&T::Summary) -> bool, - U: Dimension<'a, T::Summary>, - { - FilterCursor::new(self, cx, filter_node) - } - - #[allow(dead_code)] - pub fn first(&self) -> Option<&T> { - self.leftmost_leaf().0.items().first() - } - - pub fn last(&self) -> Option<&T> { - self.rightmost_leaf().0.items().last() - } - - pub fn last_summary(&self) -> Option<&T::Summary> { - self.rightmost_leaf().0.child_summaries().last() - } - - pub fn update_last( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) { - self.update_last_recursive(f, cx); - } - - fn update_last_recursive( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) -> Option { - match Arc::make_mut(&mut self.0) { - Node::Internal { - summary, - child_summaries, - child_trees, - .. - } => { - let last_summary = child_summaries.last_mut().unwrap(); - let last_child = child_trees.last_mut().unwrap(); - *last_summary = last_child.update_last_recursive(f, cx).unwrap(); - *summary = sum(child_summaries.iter(), cx); - Some(summary.clone()) - } - Node::Leaf { - summary, - items, - item_summaries, - } => { - if let Some((item, item_summary)) = items.last_mut().zip(item_summaries.last_mut()) - { - (f)(item); - *item_summary = item.summary(cx); - *summary = sum(item_summaries.iter(), cx); - Some(summary.clone()) - } else { - None - } - } - } - } - - pub fn update_first( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) { - self.update_first_recursive(f, cx); - } - - fn update_first_recursive( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) -> Option { - match Arc::make_mut(&mut self.0) { - Node::Internal { - summary, - child_summaries, - child_trees, - .. - } => { - let first_summary = child_summaries.first_mut().unwrap(); - let first_child = child_trees.first_mut().unwrap(); - *first_summary = first_child.update_first_recursive(f, cx).unwrap(); - *summary = sum(child_summaries.iter(), cx); - Some(summary.clone()) - } - Node::Leaf { - summary, - items, - item_summaries, - } => { - if let Some((item, item_summary)) = - items.first_mut().zip(item_summaries.first_mut()) - { - (f)(item); - *item_summary = item.summary(cx); - *summary = sum(item_summaries.iter(), cx); - Some(summary.clone()) - } else { - None - } - } - } - } - - pub fn extent<'a, D: Dimension<'a, T::Summary>>( - &'a self, - cx: ::Context<'_>, - ) -> D { - let mut extent = D::zero(cx); - match self.0.as_ref() { - Node::Internal { summary, .. } | Node::Leaf { summary, .. } => { - extent.add_summary(summary, cx); - } - } - extent - } - - pub fn summary(&self) -> &T::Summary { - match self.0.as_ref() { - Node::Internal { summary, .. } => summary, - Node::Leaf { summary, .. } => summary, - } - } - - pub fn is_empty(&self) -> bool { - match self.0.as_ref() { - Node::Internal { .. } => false, - Node::Leaf { items, .. } => items.is_empty(), - } - } - - pub fn extend(&mut self, iter: I, cx: ::Context<'_>) - where - I: IntoIterator, - { - self.append(Self::from_iter(iter, cx), cx); - } - - pub fn par_extend(&mut self, iter: I, cx: ::Context<'_>) - where - I: IntoParallelIterator, - Iter: IndexedParallelIterator, - T: Send + Sync, - T::Summary: Send + Sync, - for<'a> ::Context<'a>: Sync, - { - self.append(Self::from_par_iter(iter, cx), cx); - } - - pub fn push(&mut self, item: T, cx: ::Context<'_>) { - let summary = item.summary(cx); - self.append( - SumTree(Arc::new(Node::Leaf { - summary: summary.clone(), - items: ArrayVec::from_iter(Some(item)), - item_summaries: ArrayVec::from_iter(Some(summary)), - })), - cx, - ); - } - - pub fn append(&mut self, mut other: Self, cx: ::Context<'_>) { - if self.is_empty() { - *self = other; - } else if !other.0.is_leaf() || !other.0.items().is_empty() { - if self.0.height() < other.0.height() { - if let Some(tree) = Self::append_large(self.clone(), &mut other, cx) { - *self = Self::from_child_trees(tree, other, cx); - } else { - *self = other; - } - } else if let Some(split_tree) = self.push_tree_recursive(other, cx) { - *self = Self::from_child_trees(self.clone(), split_tree, cx); - } - } - } - - fn push_tree_recursive( - &mut self, - other: SumTree, - cx: ::Context<'_>, - ) -> Option> { - match Arc::make_mut(&mut self.0) { - Node::Internal { - height, - summary, - child_summaries, - child_trees, - .. - } => { - let other_node = other.0.clone(); - ::add_summary(summary, other_node.summary(), cx); - - let height_delta = *height - other_node.height(); - let mut summaries_to_append = ArrayVec::::new(); - let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }, u8>::new(); - if height_delta == 0 { - summaries_to_append.extend(other_node.child_summaries().iter().cloned()); - trees_to_append.extend(other_node.child_trees().iter().cloned()); - } else if height_delta == 1 && !other_node.is_underflowing() { - summaries_to_append - .push(other_node.summary().clone()) - .unwrap_oob(); - trees_to_append.push(other).unwrap_oob(); - } else { - let tree_to_append = child_trees - .last_mut() - .unwrap() - .push_tree_recursive(other, cx); - *child_summaries.last_mut().unwrap() = - child_trees.last().unwrap().0.summary().clone(); - - if let Some(split_tree) = tree_to_append { - summaries_to_append - .push(split_tree.0.summary().clone()) - .unwrap_oob(); - trees_to_append.push(split_tree).unwrap_oob(); - } - } - - let child_count = child_trees.len() + trees_to_append.len(); - if child_count > 2 * TREE_BASE { - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8>; - let right_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8>; - let left_trees; - let right_trees; - - let midpoint = (child_count + child_count % 2) / 2; - { - let mut all_summaries = child_summaries - .iter() - .chain(summaries_to_append.iter()) - .cloned(); - left_summaries = all_summaries.by_ref().take(midpoint).collect(); - right_summaries = all_summaries.collect(); - let mut all_trees = - child_trees.iter().chain(trees_to_append.iter()).cloned(); - left_trees = all_trees.by_ref().take(midpoint).collect(); - right_trees = all_trees.collect(); - } - *summary = sum(left_summaries.iter(), cx); - *child_summaries = left_summaries; - *child_trees = left_trees; - - Some(SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(right_summaries.iter(), cx), - child_summaries: right_summaries, - child_trees: right_trees, - }))) - } else { - child_summaries.extend(summaries_to_append); - child_trees.extend(trees_to_append); - None - } - } - Node::Leaf { - summary, - items, - item_summaries, - } => { - let other_node = other.0; - - let child_count = items.len() + other_node.items().len(); - if child_count > 2 * TREE_BASE { - let left_items; - let right_items; - let left_summaries; - let right_summaries: ArrayVec; - - let midpoint = (child_count + child_count % 2) / 2; - { - let mut all_items = items.iter().chain(other_node.items().iter()).cloned(); - left_items = all_items.by_ref().take(midpoint).collect(); - right_items = all_items.collect(); - - let mut all_summaries = item_summaries - .iter() - .chain(other_node.child_summaries()) - .cloned(); - left_summaries = all_summaries.by_ref().take(midpoint).collect(); - right_summaries = all_summaries.collect(); - } - *items = left_items; - *item_summaries = left_summaries; - *summary = sum(item_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Leaf { - items: right_items, - summary: sum(right_summaries.iter(), cx), - item_summaries: right_summaries, - }))) - } else { - ::add_summary(summary, other_node.summary(), cx); - items.extend(other_node.items().iter().cloned()); - item_summaries.extend(other_node.child_summaries().iter().cloned()); - None - } - } - } - } - - // appends the `large` tree to a `small` tree, assumes small.height() <= large.height() - fn append_large( - small: Self, - large: &mut Self, - cx: ::Context<'_>, - ) -> Option { - if small.0.height() == large.0.height() { - if !small.0.is_underflowing() { - Some(small) - } else { - Self::merge_into_right(small, large, cx) - } - } else { - debug_assert!(small.0.height() < large.0.height()); - let Node::Internal { - height, - summary, - child_summaries, - child_trees, - } = Arc::make_mut(&mut large.0) - else { - unreachable!(); - }; - let mut full_summary = small.summary().clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - - let first = child_trees.first_mut().unwrap(); - let res = Self::append_large(small, first, cx); - *child_summaries.first_mut().unwrap() = first.summary().clone(); - if let Some(tree) = res { - if child_trees.len() < 2 * TREE_BASE { - child_summaries - .insert(0, tree.summary().clone()) - .unwrap_oob(); - child_trees.insert(0, tree).unwrap_oob(); - None - } else { - let new_child_summaries = { - let mut res = ArrayVec::from_iter([tree.summary().clone()]); - res.extend(child_summaries.drain(..TREE_BASE)); - res - }; - let tree = SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(new_child_summaries.iter(), cx), - child_summaries: new_child_summaries, - child_trees: { - let mut res = ArrayVec::from_iter([tree]); - res.extend(child_trees.drain(..TREE_BASE)); - res - }, - })); - - *summary = sum(child_summaries.iter(), cx); - Some(tree) - } - } else { - None - } - } - } - - // Merge two nodes into `large`. - // - // `large` will contain the contents of `small` followed by its own data. - // If the combined data exceed the node capacity, returns a new node that - // holds the first half of the merged items and `large` is left with the - // second half - // - // The nodes must be on the same height - // It only makes sense to call this when `small` is underflowing - fn merge_into_right( - small: Self, - large: &mut Self, - cx: <::Summary as Summary>::Context<'_>, - ) -> Option> { - debug_assert_eq!(small.0.height(), large.0.height()); - match (small.0.as_ref(), Arc::make_mut(&mut large.0)) { - ( - Node::Internal { - summary: small_summary, - child_summaries: small_child_summaries, - child_trees: small_child_trees, - .. - }, - Node::Internal { - summary, - child_summaries, - child_trees, - height, - }, - ) => { - let total_child_count = child_trees.len() + small_child_trees.len(); - if total_child_count <= 2 * TREE_BASE { - let mut all_trees = small_child_trees.clone(); - all_trees.extend(child_trees.drain(..)); - *child_trees = all_trees; - - let mut all_summaries = small_child_summaries.clone(); - all_summaries.extend(child_summaries.drain(..)); - *child_summaries = all_summaries; - - let mut full_summary = small_summary.clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - None - } else { - let midpoint = total_child_count.div_ceil(2); - let mut all_trees = small_child_trees.iter().chain(child_trees.iter()).cloned(); - let left_trees = all_trees.by_ref().take(midpoint).collect(); - *child_trees = all_trees.collect(); - - let mut all_summaries = small_child_summaries - .iter() - .chain(child_summaries.iter()) - .cloned(); - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8> = - all_summaries.by_ref().take(midpoint).collect(); - *child_summaries = all_summaries.collect(); - - *summary = sum(child_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(left_summaries.iter(), cx), - child_summaries: left_summaries, - child_trees: left_trees, - }))) - } - } - ( - Node::Leaf { - summary: small_summary, - items: small_items, - item_summaries: small_item_summaries, - }, - Node::Leaf { - summary, - items, - item_summaries, - }, - ) => { - let total_child_count = small_items.len() + items.len(); - if total_child_count <= 2 * TREE_BASE { - let mut all_items = small_items.clone(); - all_items.extend(items.drain(..)); - *items = all_items; - - let mut all_summaries = small_item_summaries.clone(); - all_summaries.extend(item_summaries.drain(..)); - *item_summaries = all_summaries; - - let mut full_summary = small_summary.clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - None - } else { - let midpoint = total_child_count.div_ceil(2); - let mut all_items = small_items.iter().chain(items.iter()).cloned(); - let left_items = all_items.by_ref().take(midpoint).collect(); - *items = all_items.collect(); - - let mut all_summaries = small_item_summaries - .iter() - .chain(item_summaries.iter()) - .cloned(); - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8> = - all_summaries.by_ref().take(midpoint).collect(); - *item_summaries = all_summaries.collect(); - - *summary = sum(item_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Leaf { - items: left_items, - summary: sum(left_summaries.iter(), cx), - item_summaries: left_summaries, - }))) - } - } - _ => unreachable!(), - } - } - - fn from_child_trees( - left: SumTree, - right: SumTree, - cx: ::Context<'_>, - ) -> Self { - let height = left.0.height() + 1; - let mut child_summaries = ArrayVec::new(); - child_summaries.push(left.0.summary().clone()).unwrap_oob(); - child_summaries.push(right.0.summary().clone()).unwrap_oob(); - let mut child_trees = ArrayVec::new(); - child_trees.push(left).unwrap_oob(); - child_trees.push(right).unwrap_oob(); - SumTree(Arc::new(Node::Internal { - height, - summary: sum(child_summaries.iter(), cx), - child_summaries, - child_trees, - })) - } - - fn leftmost_leaf(&self) -> &Self { - match *self.0 { - Node::Leaf { .. } => self, - Node::Internal { - ref child_trees, .. - } => child_trees.first().unwrap().leftmost_leaf(), - } - } - - fn rightmost_leaf(&self) -> &Self { - match *self.0 { - Node::Leaf { .. } => self, - Node::Internal { - ref child_trees, .. - } => child_trees.last().unwrap().rightmost_leaf(), - } - } + pub fn new(cx: ::Context<'_>) -> Self { + SumTree(Arc::new(Node::Leaf { + summary: ::zero(cx), + items: ArrayVec::new(), + item_summaries: ArrayVec::new(), + })) + } + + /// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context. + pub fn from_summary(summary: T::Summary) -> Self { + SumTree(Arc::new(Node::Leaf { + summary, + items: ArrayVec::new(), + item_summaries: ArrayVec::new(), + })) + } + + pub fn from_item(item: T, cx: ::Context<'_>) -> Self { + let mut tree = Self::new(cx); + tree.push(item, cx); + tree + } + + pub fn from_iter>( + iter: I, + cx: ::Context<'_>, + ) -> Self { + let mut nodes = Vec::new(); + + let mut iter = iter.into_iter().fuse().peekable(); + while iter.peek().is_some() { + let items: ArrayVec = + iter.by_ref().take(2 * TREE_BASE).collect(); + let item_summaries: ArrayVec = + items.iter().map(|item| item.summary(cx)).collect(); + + let mut summary = item_summaries[0].clone(); + for item_summary in &item_summaries[1..] { + ::add_summary(&mut summary, item_summary, cx); + } + + nodes.push(SumTree(Arc::new(Node::Leaf { + summary, + items, + item_summaries, + }))); + } + + let mut parent_nodes = Vec::new(); + let mut height = 0; + while nodes.len() > 1 { + height += 1; + let mut current_parent_node = None; + for child_node in nodes.drain(..) { + let parent_node = current_parent_node.get_or_insert_with(|| { + SumTree(Arc::new(Node::Internal { + summary: ::zero(cx), + height, + child_summaries: ArrayVec::new(), + child_trees: ArrayVec::new(), + })) + }); + let Node::Internal { + summary, + child_summaries, + child_trees, + .. + } = Arc::get_mut(&mut parent_node.0).unwrap() + else { + unreachable!() + }; + let child_summary = child_node.summary(); + ::add_summary(summary, child_summary, cx); + child_summaries.push(child_summary.clone()).unwrap_oob(); + child_trees.push(child_node.clone()).unwrap_oob(); + + if child_trees.len() == 2 * TREE_BASE { + parent_nodes.extend(current_parent_node.take()); + } + } + parent_nodes.extend(current_parent_node.take()); + mem::swap(&mut nodes, &mut parent_nodes); + } + + if nodes.is_empty() { + Self::new(cx) + } else { + debug_assert_eq!(nodes.len(), 1); + nodes.pop().unwrap() + } + } + + pub fn from_par_iter(iter: I, cx: ::Context<'_>) -> Self + where + I: IntoParallelIterator, + Iter: IndexedParallelIterator, + T: Send + Sync, + T::Summary: Send + Sync, + for<'a> ::Context<'a>: Sync, + { + let mut nodes = iter + .into_par_iter() + .chunks(2 * TREE_BASE) + .map(|items| { + let items: ArrayVec = items.into_iter().collect(); + let item_summaries: ArrayVec = + items.iter().map(|item| item.summary(cx)).collect(); + let mut summary = item_summaries[0].clone(); + for item_summary in &item_summaries[1..] { + ::add_summary(&mut summary, item_summary, cx); + } + SumTree(Arc::new(Node::Leaf { + summary, + items, + item_summaries, + })) + }) + .collect::>(); + + let mut height = 0; + while nodes.len() > 1 { + height += 1; + nodes = nodes + .into_par_iter() + .chunks(2 * TREE_BASE) + .map(|child_nodes| { + let child_trees: ArrayVec, { 2 * TREE_BASE }, u8> = + child_nodes.into_iter().collect(); + let child_summaries: ArrayVec = child_trees + .iter() + .map(|child_tree| child_tree.summary().clone()) + .collect(); + let mut summary = child_summaries[0].clone(); + for child_summary in &child_summaries[1..] { + ::add_summary(&mut summary, child_summary, cx); + } + SumTree(Arc::new(Node::Internal { + height, + summary, + child_summaries, + child_trees, + })) + }) + .collect::>(); + } + + if nodes.is_empty() { + Self::new(cx) + } else { + debug_assert_eq!(nodes.len(), 1); + nodes.pop().unwrap() + } + } + + #[allow(unused)] + pub fn items<'a>(&'a self, cx: ::Context<'a>) -> Vec { + let mut items = Vec::new(); + let mut cursor = self.cursor::<()>(cx); + cursor.next(); + while let Some(item) = cursor.item() { + items.push(item.clone()); + cursor.next(); + } + items + } + + pub fn iter(&self) -> Iter<'_, T> { + Iter::new(self) + } + + /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`. + /// + /// Only returns the item that exactly has the target match. + #[instrument(skip_all)] + pub fn find_exact<'a, 'slf, D, Target>( + &'slf self, + cx: ::Context<'a>, + target: &Target, + bias: Bias, + ) -> (D, D, Option<&'slf T>) + where + D: Dimension<'slf, T::Summary>, + Target: SeekTarget<'slf, T::Summary, D>, + { + let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); + let comparison = target.cmp(&tree_end, cx); + if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) + { + return (tree_end.clone(), tree_end, None); + } + + let mut pos = D::zero(cx); + return match Self::find_iterate::<_, _, true>(cx, target, bias, &mut pos, self) { + Some((item, end)) => (pos, end, Some(item)), + None => (pos.clone(), pos, None), + }; + } + + /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` + #[instrument(skip_all)] + pub fn find<'a, 'slf, D, Target>( + &'slf self, + cx: ::Context<'a>, + target: &Target, + bias: Bias, + ) -> (D, D, Option<&'slf T>) + where + D: Dimension<'slf, T::Summary>, + Target: SeekTarget<'slf, T::Summary, D>, + { + let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); + let comparison = target.cmp(&tree_end, cx); + if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) + { + return (tree_end.clone(), tree_end, None); + } + + let mut pos = D::zero(cx); + return match Self::find_iterate::<_, _, false>(cx, target, bias, &mut pos, self) { + Some((item, end)) => (pos, end, Some(item)), + None => (pos.clone(), pos, None), + }; + } + + fn find_iterate<'tree, 'a, D, Target, const EXACT: bool>( + cx: ::Context<'a>, + target: &Target, + bias: Bias, + position: &mut D, + mut this: &'tree SumTree, + ) -> Option<(&'tree T, D)> + where + D: Dimension<'tree, T::Summary>, + Target: SeekTarget<'tree, T::Summary, D>, + { + 'iterate: loop { + match &*this.0 { + Node::Internal { + child_summaries, + child_trees, + .. + } => { + for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { + let child_end = position.clone().with_added_summary(child_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let target_in_child = comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left); + if target_in_child { + this = child_tree; + continue 'iterate; + } + *position = child_end; + } + } + Node::Leaf { + items, + item_summaries, + .. + } => { + for (item, item_summary) in items.iter().zip(item_summaries) { + let mut child_end = position.clone(); + child_end.add_summary(item_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let entry_found = if EXACT { + comparison == Ordering::Equal + } else { + comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left) + }; + if entry_found { + return Some((item, child_end)); + } + + *position = child_end; + } + } + } + return None; + } + } + + /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` + #[instrument(skip_all)] + pub fn find_with_prev<'a, 'slf, D, Target>( + &'slf self, + cx: ::Context<'a>, + target: &Target, + bias: Bias, + ) -> (D, D, Option<(Option<&'slf T>, &'slf T)>) + where + D: Dimension<'slf, T::Summary>, + Target: SeekTarget<'slf, T::Summary, D>, + { + let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); + let comparison = target.cmp(&tree_end, cx); + if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) + { + return (tree_end.clone(), tree_end, None); + } + + let mut pos = D::zero(cx); + return match Self::find_with_prev_iterate::<_, _, false>(cx, target, bias, &mut pos, self) { + Some((prev, item, end)) => (pos, end, Some((prev, item))), + None => (pos.clone(), pos, None), + }; + } + + fn find_with_prev_iterate<'tree, 'a, D, Target, const EXACT: bool>( + cx: ::Context<'a>, + target: &Target, + bias: Bias, + position: &mut D, + mut this: &'tree SumTree, + ) -> Option<(Option<&'tree T>, &'tree T, D)> + where + D: Dimension<'tree, T::Summary>, + Target: SeekTarget<'tree, T::Summary, D>, + { + let mut prev = None; + 'iterate: loop { + match &*this.0 { + Node::Internal { + child_summaries, + child_trees, + .. + } => { + for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { + let child_end = position.clone().with_added_summary(child_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let target_in_child = comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left); + if target_in_child { + this = child_tree; + continue 'iterate; + } + prev = child_tree.last(); + *position = child_end; + } + } + Node::Leaf { + items, + item_summaries, + .. + } => { + for (item, item_summary) in items.iter().zip(item_summaries) { + let mut child_end = position.clone(); + child_end.add_summary(item_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let entry_found = if EXACT { + comparison == Ordering::Equal + } else { + comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left) + }; + if entry_found { + return Some((prev, item, child_end)); + } + + prev = Some(item); + *position = child_end; + } + } + } + return None; + } + } + + pub fn cursor<'a, 'b, D>( + &'a self, + cx: ::Context<'b>, + ) -> Cursor<'a, 'b, T, D> + where + D: Dimension<'a, T::Summary>, + { + Cursor::new(self, cx) + } + + /// Note: If the summary type requires a non `()` context, then the filter cursor + /// that is returned cannot be used with Rust's iterators. + pub fn filter<'a, 'b, F, U>( + &'a self, + cx: ::Context<'b>, + filter_node: F, + ) -> FilterCursor<'a, 'b, F, T, U> + where + F: FnMut(&T::Summary) -> bool, + U: Dimension<'a, T::Summary>, + { + FilterCursor::new(self, cx, filter_node) + } + + #[allow(dead_code)] + pub fn first(&self) -> Option<&T> { + self.leftmost_leaf().0.items().first() + } + + pub fn last(&self) -> Option<&T> { + self.rightmost_leaf().0.items().last() + } + + pub fn last_summary(&self) -> Option<&T::Summary> { + self.rightmost_leaf().0.child_summaries().last() + } + + pub fn update_last( + &mut self, + f: impl FnOnce(&mut T), + cx: ::Context<'_>, + ) { + self.update_last_recursive(f, cx); + } + + fn update_last_recursive( + &mut self, + f: impl FnOnce(&mut T), + cx: ::Context<'_>, + ) -> Option { + match Arc::make_mut(&mut self.0) { + Node::Internal { + summary, + child_summaries, + child_trees, + .. + } => { + let last_summary = child_summaries.last_mut().unwrap(); + let last_child = child_trees.last_mut().unwrap(); + *last_summary = last_child.update_last_recursive(f, cx).unwrap(); + *summary = sum(child_summaries.iter(), cx); + Some(summary.clone()) + } + Node::Leaf { + summary, + items, + item_summaries, + } => { + if let Some((item, item_summary)) = items.last_mut().zip(item_summaries.last_mut()) + { + (f)(item); + *item_summary = item.summary(cx); + *summary = sum(item_summaries.iter(), cx); + Some(summary.clone()) + } else { + None + } + } + } + } + + pub fn update_first( + &mut self, + f: impl FnOnce(&mut T), + cx: ::Context<'_>, + ) { + self.update_first_recursive(f, cx); + } + + fn update_first_recursive( + &mut self, + f: impl FnOnce(&mut T), + cx: ::Context<'_>, + ) -> Option { + match Arc::make_mut(&mut self.0) { + Node::Internal { + summary, + child_summaries, + child_trees, + .. + } => { + let first_summary = child_summaries.first_mut().unwrap(); + let first_child = child_trees.first_mut().unwrap(); + *first_summary = first_child.update_first_recursive(f, cx).unwrap(); + *summary = sum(child_summaries.iter(), cx); + Some(summary.clone()) + } + Node::Leaf { + summary, + items, + item_summaries, + } => { + if let Some((item, item_summary)) = + items.first_mut().zip(item_summaries.first_mut()) + { + (f)(item); + *item_summary = item.summary(cx); + *summary = sum(item_summaries.iter(), cx); + Some(summary.clone()) + } else { + None + } + } + } + } + + pub fn extent<'a, D: Dimension<'a, T::Summary>>( + &'a self, + cx: ::Context<'_>, + ) -> D { + let mut extent = D::zero(cx); + match self.0.as_ref() { + Node::Internal { summary, .. } | Node::Leaf { summary, .. } => { + extent.add_summary(summary, cx); + } + } + extent + } + + pub fn summary(&self) -> &T::Summary { + match self.0.as_ref() { + Node::Internal { summary, .. } => summary, + Node::Leaf { summary, .. } => summary, + } + } + + pub fn is_empty(&self) -> bool { + match self.0.as_ref() { + Node::Internal { .. } => false, + Node::Leaf { items, .. } => items.is_empty(), + } + } + + pub fn extend(&mut self, iter: I, cx: ::Context<'_>) + where + I: IntoIterator, + { + self.append(Self::from_iter(iter, cx), cx); + } + + pub fn par_extend(&mut self, iter: I, cx: ::Context<'_>) + where + I: IntoParallelIterator, + Iter: IndexedParallelIterator, + T: Send + Sync, + T::Summary: Send + Sync, + for<'a> ::Context<'a>: Sync, + { + self.append(Self::from_par_iter(iter, cx), cx); + } + + pub fn push(&mut self, item: T, cx: ::Context<'_>) { + let summary = item.summary(cx); + self.append( + SumTree(Arc::new(Node::Leaf { + summary: summary.clone(), + items: ArrayVec::from_iter(Some(item)), + item_summaries: ArrayVec::from_iter(Some(summary)), + })), + cx, + ); + } + + pub fn append(&mut self, mut other: Self, cx: ::Context<'_>) { + if self.is_empty() { + *self = other; + } else if !other.0.is_leaf() || !other.0.items().is_empty() { + if self.0.height() < other.0.height() { + if let Some(tree) = Self::append_large(self.clone(), &mut other, cx) { + *self = Self::from_child_trees(tree, other, cx); + } else { + *self = other; + } + } else if let Some(split_tree) = self.push_tree_recursive(other, cx) { + *self = Self::from_child_trees(self.clone(), split_tree, cx); + } + } + } + + fn push_tree_recursive( + &mut self, + other: SumTree, + cx: ::Context<'_>, + ) -> Option> { + match Arc::make_mut(&mut self.0) { + Node::Internal { + height, + summary, + child_summaries, + child_trees, + .. + } => { + let other_node = other.0.clone(); + ::add_summary(summary, other_node.summary(), cx); + + let height_delta = *height - other_node.height(); + let mut summaries_to_append = ArrayVec::::new(); + let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }, u8>::new(); + if height_delta == 0 { + summaries_to_append.extend(other_node.child_summaries().iter().cloned()); + trees_to_append.extend(other_node.child_trees().iter().cloned()); + } else if height_delta == 1 && !other_node.is_underflowing() { + summaries_to_append + .push(other_node.summary().clone()) + .unwrap_oob(); + trees_to_append.push(other).unwrap_oob(); + } else { + let tree_to_append = child_trees + .last_mut() + .unwrap() + .push_tree_recursive(other, cx); + *child_summaries.last_mut().unwrap() = + child_trees.last().unwrap().0.summary().clone(); + + if let Some(split_tree) = tree_to_append { + summaries_to_append + .push(split_tree.0.summary().clone()) + .unwrap_oob(); + trees_to_append.push(split_tree).unwrap_oob(); + } + } + + let child_count = child_trees.len() + trees_to_append.len(); + if child_count > 2 * TREE_BASE { + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8>; + let right_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8>; + let left_trees; + let right_trees; + + let midpoint = (child_count + child_count % 2) / 2; + { + let mut all_summaries = child_summaries + .iter() + .chain(summaries_to_append.iter()) + .cloned(); + left_summaries = all_summaries.by_ref().take(midpoint).collect(); + right_summaries = all_summaries.collect(); + let mut all_trees = + child_trees.iter().chain(trees_to_append.iter()).cloned(); + left_trees = all_trees.by_ref().take(midpoint).collect(); + right_trees = all_trees.collect(); + } + *summary = sum(left_summaries.iter(), cx); + *child_summaries = left_summaries; + *child_trees = left_trees; + + Some(SumTree(Arc::new(Node::Internal { + height: *height, + summary: sum(right_summaries.iter(), cx), + child_summaries: right_summaries, + child_trees: right_trees, + }))) + } else { + child_summaries.extend(summaries_to_append); + child_trees.extend(trees_to_append); + None + } + } + Node::Leaf { + summary, + items, + item_summaries, + } => { + let other_node = other.0; + + let child_count = items.len() + other_node.items().len(); + if child_count > 2 * TREE_BASE { + let left_items; + let right_items; + let left_summaries; + let right_summaries: ArrayVec; + + let midpoint = (child_count + child_count % 2) / 2; + { + let mut all_items = items.iter().chain(other_node.items().iter()).cloned(); + left_items = all_items.by_ref().take(midpoint).collect(); + right_items = all_items.collect(); + + let mut all_summaries = item_summaries + .iter() + .chain(other_node.child_summaries()) + .cloned(); + left_summaries = all_summaries.by_ref().take(midpoint).collect(); + right_summaries = all_summaries.collect(); + } + *items = left_items; + *item_summaries = left_summaries; + *summary = sum(item_summaries.iter(), cx); + Some(SumTree(Arc::new(Node::Leaf { + items: right_items, + summary: sum(right_summaries.iter(), cx), + item_summaries: right_summaries, + }))) + } else { + ::add_summary(summary, other_node.summary(), cx); + items.extend(other_node.items().iter().cloned()); + item_summaries.extend(other_node.child_summaries().iter().cloned()); + None + } + } + } + } + + // appends the `large` tree to a `small` tree, assumes small.height() <= large.height() + fn append_large( + small: Self, + large: &mut Self, + cx: ::Context<'_>, + ) -> Option { + if small.0.height() == large.0.height() { + if !small.0.is_underflowing() { + Some(small) + } else { + Self::merge_into_right(small, large, cx) + } + } else { + debug_assert!(small.0.height() < large.0.height()); + let Node::Internal { + height, + summary, + child_summaries, + child_trees, + } = Arc::make_mut(&mut large.0) + else { + unreachable!(); + }; + let mut full_summary = small.summary().clone(); + Summary::add_summary(&mut full_summary, summary, cx); + *summary = full_summary; + + let first = child_trees.first_mut().unwrap(); + let res = Self::append_large(small, first, cx); + *child_summaries.first_mut().unwrap() = first.summary().clone(); + if let Some(tree) = res { + if child_trees.len() < 2 * TREE_BASE { + child_summaries + .insert(0, tree.summary().clone()) + .unwrap_oob(); + child_trees.insert(0, tree).unwrap_oob(); + None + } else { + let new_child_summaries = { + let mut res = ArrayVec::from_iter([tree.summary().clone()]); + res.extend(child_summaries.drain(..TREE_BASE)); + res + }; + let tree = SumTree(Arc::new(Node::Internal { + height: *height, + summary: sum(new_child_summaries.iter(), cx), + child_summaries: new_child_summaries, + child_trees: { + let mut res = ArrayVec::from_iter([tree]); + res.extend(child_trees.drain(..TREE_BASE)); + res + }, + })); + + *summary = sum(child_summaries.iter(), cx); + Some(tree) + } + } else { + None + } + } + } + + // Merge two nodes into `large`. + // + // `large` will contain the contents of `small` followed by its own data. + // If the combined data exceed the node capacity, returns a new node that + // holds the first half of the merged items and `large` is left with the + // second half + // + // The nodes must be on the same height + // It only makes sense to call this when `small` is underflowing + fn merge_into_right( + small: Self, + large: &mut Self, + cx: <::Summary as Summary>::Context<'_>, + ) -> Option> { + debug_assert_eq!(small.0.height(), large.0.height()); + match (small.0.as_ref(), Arc::make_mut(&mut large.0)) { + ( + Node::Internal { + summary: small_summary, + child_summaries: small_child_summaries, + child_trees: small_child_trees, + .. + }, + Node::Internal { + summary, + child_summaries, + child_trees, + height, + }, + ) => { + let total_child_count = child_trees.len() + small_child_trees.len(); + if total_child_count <= 2 * TREE_BASE { + let mut all_trees = small_child_trees.clone(); + all_trees.extend(child_trees.drain(..)); + *child_trees = all_trees; + + let mut all_summaries = small_child_summaries.clone(); + all_summaries.extend(child_summaries.drain(..)); + *child_summaries = all_summaries; + + let mut full_summary = small_summary.clone(); + Summary::add_summary(&mut full_summary, summary, cx); + *summary = full_summary; + None + } else { + let midpoint = total_child_count.div_ceil(2); + let mut all_trees = small_child_trees.iter().chain(child_trees.iter()).cloned(); + let left_trees = all_trees.by_ref().take(midpoint).collect(); + *child_trees = all_trees.collect(); + + let mut all_summaries = small_child_summaries + .iter() + .chain(child_summaries.iter()) + .cloned(); + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8> = + all_summaries.by_ref().take(midpoint).collect(); + *child_summaries = all_summaries.collect(); + + *summary = sum(child_summaries.iter(), cx); + Some(SumTree(Arc::new(Node::Internal { + height: *height, + summary: sum(left_summaries.iter(), cx), + child_summaries: left_summaries, + child_trees: left_trees, + }))) + } + } + ( + Node::Leaf { + summary: small_summary, + items: small_items, + item_summaries: small_item_summaries, + }, + Node::Leaf { + summary, + items, + item_summaries, + }, + ) => { + let total_child_count = small_items.len() + items.len(); + if total_child_count <= 2 * TREE_BASE { + let mut all_items = small_items.clone(); + all_items.extend(items.drain(..)); + *items = all_items; + + let mut all_summaries = small_item_summaries.clone(); + all_summaries.extend(item_summaries.drain(..)); + *item_summaries = all_summaries; + + let mut full_summary = small_summary.clone(); + Summary::add_summary(&mut full_summary, summary, cx); + *summary = full_summary; + None + } else { + let midpoint = total_child_count.div_ceil(2); + let mut all_items = small_items.iter().chain(items.iter()).cloned(); + let left_items = all_items.by_ref().take(midpoint).collect(); + *items = all_items.collect(); + + let mut all_summaries = small_item_summaries + .iter() + .chain(item_summaries.iter()) + .cloned(); + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }, u8> = + all_summaries.by_ref().take(midpoint).collect(); + *item_summaries = all_summaries.collect(); + + *summary = sum(item_summaries.iter(), cx); + Some(SumTree(Arc::new(Node::Leaf { + items: left_items, + summary: sum(left_summaries.iter(), cx), + item_summaries: left_summaries, + }))) + } + } + _ => unreachable!(), + } + } + + fn from_child_trees( + left: SumTree, + right: SumTree, + cx: ::Context<'_>, + ) -> Self { + let height = left.0.height() + 1; + let mut child_summaries = ArrayVec::new(); + child_summaries.push(left.0.summary().clone()).unwrap_oob(); + child_summaries.push(right.0.summary().clone()).unwrap_oob(); + let mut child_trees = ArrayVec::new(); + child_trees.push(left).unwrap_oob(); + child_trees.push(right).unwrap_oob(); + SumTree(Arc::new(Node::Internal { + height, + summary: sum(child_summaries.iter(), cx), + child_summaries, + child_trees, + })) + } + + fn leftmost_leaf(&self) -> &Self { + match *self.0 { + Node::Leaf { .. } => self, + Node::Internal { + ref child_trees, .. + } => child_trees.first().unwrap().leftmost_leaf(), + } + } + + fn rightmost_leaf(&self) -> &Self { + match *self.0 { + Node::Leaf { .. } => self, + Node::Internal { + ref child_trees, .. + } => child_trees.last().unwrap().rightmost_leaf(), + } + } } impl PartialEq for SumTree { - fn eq(&self, other: &Self) -> bool { - self.iter().eq(other.iter()) - } + fn eq(&self, other: &Self) -> bool { + self.iter().eq(other.iter()) + } } impl Eq for SumTree {} impl SumTree { - pub fn insert_or_replace<'a, 'b>( - &'a mut self, - item: T, - cx: ::Context<'b>, - ) -> Option { - let mut replaced = None; - { - let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(&item.key(), Bias::Left); - if let Some(cursor_item) = cursor.item() - && cursor_item.key() == item.key() - { - replaced = Some(cursor_item.clone()); - cursor.next(); - } - new_tree.push(item, cx); - new_tree.append(cursor.suffix(), cx); - drop(cursor); - *self = new_tree - }; - replaced - } + pub fn insert_or_replace<'a, 'b>( + &'a mut self, + item: T, + cx: ::Context<'b>, + ) -> Option { + let mut replaced = None; + { + let mut cursor = self.cursor::(cx); + let mut new_tree = cursor.slice(&item.key(), Bias::Left); + if let Some(cursor_item) = cursor.item() + && cursor_item.key() == item.key() + { + replaced = Some(cursor_item.clone()); + cursor.next(); + } + new_tree.push(item, cx); + new_tree.append(cursor.suffix(), cx); + drop(cursor); + *self = new_tree + }; + replaced + } - pub fn remove(&mut self, key: &T::Key, cx: ::Context<'_>) -> Option { - let mut removed = None; - *self = { - let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(key, Bias::Left); - if let Some(item) = cursor.item() - && item.key() == *key - { - removed = Some(item.clone()); - cursor.next(); - } - new_tree.append(cursor.suffix(), cx); - new_tree - }; - removed - } + pub fn remove(&mut self, key: &T::Key, cx: ::Context<'_>) -> Option { + let mut removed = None; + *self = { + let mut cursor = self.cursor::(cx); + let mut new_tree = cursor.slice(key, Bias::Left); + if let Some(item) = cursor.item() + && item.key() == *key + { + removed = Some(item.clone()); + cursor.next(); + } + new_tree.append(cursor.suffix(), cx); + new_tree + }; + removed + } - pub fn edit( - &mut self, - mut edits: Vec>, - cx: ::Context<'_>, - ) -> Vec { - if edits.is_empty() { - return Vec::new(); - } + pub fn edit( + &mut self, + mut edits: Vec>, + cx: ::Context<'_>, + ) -> Vec { + if edits.is_empty() { + return Vec::new(); + } - let mut removed = Vec::new(); - edits.sort_unstable_by_key(|item| item.key()); + let mut removed = Vec::new(); + edits.sort_unstable_by_key(|item| item.key()); - *self = { - let mut cursor = self.cursor::(cx); - let mut new_tree = SumTree::new(cx); - let mut buffered_items = Vec::new(); + *self = { + let mut cursor = self.cursor::(cx); + let mut new_tree = SumTree::new(cx); + let mut buffered_items = Vec::new(); - cursor.seek(&T::Key::zero(cx), Bias::Left); - for edit in edits { - let new_key = edit.key(); - let mut old_item = cursor.item(); + cursor.seek(&T::Key::zero(cx), Bias::Left); + for edit in edits { + let new_key = edit.key(); + let mut old_item = cursor.item(); - if old_item - .as_ref() - .is_some_and(|old_item| old_item.key() < new_key) - { - new_tree.extend(buffered_items.drain(..), cx); - let slice = cursor.slice(&new_key, Bias::Left); - new_tree.append(slice, cx); - old_item = cursor.item(); - } + if old_item + .as_ref() + .is_some_and(|old_item| old_item.key() < new_key) + { + new_tree.extend(buffered_items.drain(..), cx); + let slice = cursor.slice(&new_key, Bias::Left); + new_tree.append(slice, cx); + old_item = cursor.item(); + } - if let Some(old_item) = old_item - && old_item.key() == new_key - { - removed.push(old_item.clone()); - cursor.next(); - } + if let Some(old_item) = old_item + && old_item.key() == new_key + { + removed.push(old_item.clone()); + cursor.next(); + } - match edit { - Edit::Insert(item) => { - buffered_items.push(item); - } - Edit::Remove(_) => {} - } - } + match edit { + Edit::Insert(item) => { + buffered_items.push(item); + } + Edit::Remove(_) => {} + } + } - new_tree.extend(buffered_items, cx); - new_tree.append(cursor.suffix(), cx); - new_tree - }; + new_tree.extend(buffered_items, cx); + new_tree.append(cursor.suffix(), cx); + new_tree + }; - removed - } + removed + } - pub fn get<'a>( - &'a self, - key: &T::Key, - cx: ::Context<'a>, - ) -> Option<&'a T> { - if let (_, _, Some(item)) = self.find_exact::(cx, key, Bias::Left) { - Some(item) - } else { - None - } - } + pub fn get<'a>( + &'a self, + key: &T::Key, + cx: ::Context<'a>, + ) -> Option<&'a T> { + if let (_, _, Some(item)) = self.find_exact::(cx, key, Bias::Left) { + Some(item) + } else { + None + } + } } impl Default for SumTree where - T: Item, - S: for<'a> Summary = ()>, + T: Item, + S: for<'a> Summary = ()>, { - fn default() -> Self { - Self::new(()) - } + fn default() -> Self { + Self::new(()) + } } #[derive(Clone)] pub enum Node { - Internal { - height: u8, - summary: T::Summary, - child_summaries: ArrayVec, - child_trees: ArrayVec, { 2 * TREE_BASE }, u8>, - }, - Leaf { - summary: T::Summary, - items: ArrayVec, - item_summaries: ArrayVec, - }, + Internal { + height: u8, + summary: T::Summary, + child_summaries: ArrayVec, + child_trees: ArrayVec, { 2 * TREE_BASE }, u8>, + }, + Leaf { + summary: T::Summary, + items: ArrayVec, + item_summaries: ArrayVec, + }, } impl fmt::Debug for Node where - T: Item + fmt::Debug, - T::Summary: fmt::Debug, + T: Item + fmt::Debug, + T::Summary: fmt::Debug, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Node::Internal { - height, - summary, - child_summaries, - child_trees, - } => f - .debug_struct("Internal") - .field("height", height) - .field("summary", summary) - .field("child_summaries", child_summaries) - .field("child_trees", child_trees) - .finish(), - Node::Leaf { - summary, - items, - item_summaries, - } => f - .debug_struct("Leaf") - .field("summary", summary) - .field("items", items) - .field("item_summaries", item_summaries) - .finish(), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Node::Internal { + height, + summary, + child_summaries, + child_trees, + } => f + .debug_struct("Internal") + .field("height", height) + .field("summary", summary) + .field("child_summaries", child_summaries) + .field("child_trees", child_trees) + .finish(), + Node::Leaf { + summary, + items, + item_summaries, + } => f + .debug_struct("Leaf") + .field("summary", summary) + .field("items", items) + .field("item_summaries", item_summaries) + .finish(), + } + } } impl Node { - fn is_leaf(&self) -> bool { - matches!(self, Node::Leaf { .. }) - } + fn is_leaf(&self) -> bool { + matches!(self, Node::Leaf { .. }) + } - fn height(&self) -> u8 { - match self { - Node::Internal { height, .. } => *height, - Node::Leaf { .. } => 0, - } - } + fn height(&self) -> u8 { + match self { + Node::Internal { height, .. } => *height, + Node::Leaf { .. } => 0, + } + } - fn summary(&self) -> &T::Summary { - match self { - Node::Internal { summary, .. } => summary, - Node::Leaf { summary, .. } => summary, - } - } + fn summary(&self) -> &T::Summary { + match self { + Node::Internal { summary, .. } => summary, + Node::Leaf { summary, .. } => summary, + } + } - fn child_summaries(&self) -> &[T::Summary] { - match self { - Node::Internal { - child_summaries, .. - } => child_summaries.as_slice(), - Node::Leaf { item_summaries, .. } => item_summaries.as_slice(), - } - } + fn child_summaries(&self) -> &[T::Summary] { + match self { + Node::Internal { + child_summaries, .. + } => child_summaries.as_slice(), + Node::Leaf { item_summaries, .. } => item_summaries.as_slice(), + } + } - fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }, u8> { - match self { - Node::Internal { child_trees, .. } => child_trees, - Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), - } - } + fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }, u8> { + match self { + Node::Internal { child_trees, .. } => child_trees, + Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), + } + } - fn items(&self) -> &ArrayVec { - match self { - Node::Leaf { items, .. } => items, - Node::Internal { .. } => panic!("Internal nodes have no items"), - } - } + fn items(&self) -> &ArrayVec { + match self { + Node::Leaf { items, .. } => items, + Node::Internal { .. } => panic!("Internal nodes have no items"), + } + } - fn is_underflowing(&self) -> bool { - match self { - Node::Internal { child_trees, .. } => child_trees.len() < TREE_BASE, - Node::Leaf { items, .. } => items.len() < TREE_BASE, - } - } + fn is_underflowing(&self) -> bool { + match self { + Node::Internal { child_trees, .. } => child_trees.len() < TREE_BASE, + Node::Leaf { items, .. } => items.len() < TREE_BASE, + } + } } #[derive(Debug)] pub enum Edit { - Insert(T), - Remove(T::Key), + Insert(T), + Remove(T::Key), } impl Edit { - fn key(&self) -> T::Key { - match self { - Edit::Insert(item) => item.key(), - Edit::Remove(key) => key.clone(), - } - } + fn key(&self) -> T::Key { + match self { + Edit::Insert(item) => item.key(), + Edit::Remove(key) => key.clone(), + } + } } fn sum<'a, T, I>(iter: I, cx: T::Context<'_>) -> T where - T: 'a + Summary, - I: Iterator, + T: 'a + Summary, + I: Iterator, { - let mut sum = T::zero(cx); - for value in iter { - sum.add_summary(value, cx); - } - sum + let mut sum = T::zero(cx); + for value in iter { + sum.add_summary(value, cx); + } + sum } #[cfg(test)] mod tests { - use super::*; - use rand::{distr::StandardUniform, prelude::*}; - use std::cmp; - - #[test] - fn test_extend_and_push_tree() { - let mut tree1 = SumTree::default(); - tree1.extend(0..20, ()); - - let mut tree2 = SumTree::default(); - tree2.extend(50..100, ()); - - tree1.append(tree2, ()); - assert_eq!(tree1.items(()), (0..20).chain(50..100).collect::>()); - } - - #[test] - fn test_random() { - let mut starting_seed = 0; - if let Ok(value) = std::env::var("SEED") { - starting_seed = value.parse().expect("invalid SEED variable"); - } - let mut num_iterations = 100; - if let Ok(value) = std::env::var("ITERATIONS") { - num_iterations = value.parse().expect("invalid ITERATIONS variable"); - } - let num_operations = std::env::var("OPERATIONS") - .map_or(5, |o| o.parse().expect("invalid OPERATIONS variable")); - - for seed in starting_seed..(starting_seed + num_iterations) { - eprintln!("seed = {}", seed); - let mut rng = StdRng::seed_from_u64(seed); - - let rng = &mut rng; - let mut tree = SumTree::::default(); - let count = rng.random_range(0..10); - if rng.random() { - tree.extend(rng.sample_iter(StandardUniform).take(count), ()); - } else { - let items = rng - .sample_iter(StandardUniform) - .take(count) - .collect::>(); - tree.par_extend(items, ()); - } - - for _ in 0..num_operations { - let splice_end = rng.random_range(0..tree.extent::(()).0 + 1); - let splice_start = rng.random_range(0..splice_end + 1); - let count = rng.random_range(0..10); - let tree_end = tree.extent::(()); - let new_items = rng - .sample_iter(StandardUniform) - .take(count) - .collect::>(); - - let mut reference_items = tree.items(()); - reference_items.splice(splice_start..splice_end, new_items.clone()); - - tree = { - let mut cursor = tree.cursor::(()); - let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right); - if rng.random() { - new_tree.extend(new_items, ()); - } else { - new_tree.par_extend(new_items, ()); - } - cursor.seek(&Count(splice_end), Bias::Right); - new_tree.append(cursor.slice(&tree_end, Bias::Right), ()); - new_tree - }; - - assert_eq!(tree.items(()), reference_items); - assert_eq!( - tree.iter().collect::>(), - tree.cursor::<()>(()).collect::>() - ); - - log::info!("tree items: {:?}", tree.items(())); - - let mut filter_cursor = - tree.filter::<_, Count>((), |summary| summary.contains_even); - let expected_filtered_items = tree - .items(()) - .into_iter() - .enumerate() - .filter(|(_, item)| (item & 1) == 0) - .collect::>(); - - let mut item_ix = if rng.random() { - filter_cursor.next(); - 0 - } else { - filter_cursor.prev(); - expected_filtered_items.len().saturating_sub(1) - }; - while item_ix < expected_filtered_items.len() { - log::info!("filter_cursor, item_ix: {}", item_ix); - let actual_item = filter_cursor.item().unwrap(); - let (reference_index, reference_item) = expected_filtered_items[item_ix]; - assert_eq!(actual_item, &reference_item); - assert_eq!(filter_cursor.start().0, reference_index); - log::info!("next"); - filter_cursor.next(); - item_ix += 1; - - while item_ix > 0 && rng.random_bool(0.2) { - log::info!("prev"); - filter_cursor.prev(); - item_ix -= 1; - - if item_ix == 0 && rng.random_bool(0.2) { - filter_cursor.prev(); - assert_eq!(filter_cursor.item(), None); - assert_eq!(filter_cursor.start().0, 0); - filter_cursor.next(); - } - } - } - assert_eq!(filter_cursor.item(), None); - - let mut before_start = false; - let mut cursor = tree.cursor::(()); - let start_pos = rng.random_range(0..=reference_items.len()); - cursor.seek(&Count(start_pos), Bias::Right); - let mut pos = rng.random_range(start_pos..=reference_items.len()); - cursor.seek_forward(&Count(pos), Bias::Right); - - for i in 0..10 { - assert_eq!(cursor.start().0, pos); - - if pos > 0 { - assert_eq!(cursor.prev_item().unwrap(), &reference_items[pos - 1]); - } else { - assert_eq!(cursor.prev_item(), None); - } - - if pos < reference_items.len() && !before_start { - assert_eq!(cursor.item().unwrap(), &reference_items[pos]); - } else { - assert_eq!(cursor.item(), None); - } - - if before_start { - assert_eq!(cursor.next_item(), reference_items.first()); - } else if pos + 1 < reference_items.len() { - assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]); - } else { - assert_eq!(cursor.next_item(), None); - } - - if i < 5 { - cursor.next(); - if pos < reference_items.len() { - pos += 1; - before_start = false; - } - } else { - cursor.prev(); - if pos == 0 { - before_start = true; - } - pos = pos.saturating_sub(1); - } - } - } - - for _ in 0..10 { - let end = rng.random_range(0..tree.extent::(()).0 + 1); - let start = rng.random_range(0..end + 1); - let start_bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - let end_bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - - let mut cursor = tree.cursor::(()); - cursor.seek(&Count(start), start_bias); - let slice = cursor.slice(&Count(end), end_bias); - - cursor.seek(&Count(start), start_bias); - let summary = cursor.summary::<_, Sum>(&Count(end), end_bias); - - assert_eq!(summary.0, slice.summary().sum); - } - } - } - - #[test] - fn test_cursor() { - // Empty tree - let tree = SumTree::::default(); - let mut cursor = tree.cursor::(()); - assert_eq!( - cursor.slice(&Count(0), Bias::Right).items(()), - Vec::::new() - ); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 0); - cursor.prev(); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 0); - cursor.next(); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 0); - - // Single-element tree - let mut tree = SumTree::::default(); - tree.extend(vec![1], ()); - let mut cursor = tree.cursor::(()); - assert_eq!( - cursor.slice(&Count(0), Bias::Right).items(()), - Vec::::new() - ); - assert_eq!(cursor.item(), Some(&1)); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 0); - - cursor.next(); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&1)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 1); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&1)); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 0); - - let mut cursor = tree.cursor::(()); - assert_eq!(cursor.slice(&Count(1), Bias::Right).items(()), [1]); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&1)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 1); - - cursor.seek(&Count(0), Bias::Right); - assert_eq!( - cursor - .slice(&tree.extent::(()), Bias::Right) - .items(()), - [1] - ); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&1)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 1); - - // Multiple-element tree - let mut tree = SumTree::default(); - tree.extend(vec![1, 2, 3, 4, 5, 6], ()); - let mut cursor = tree.cursor::(()); - - assert_eq!(cursor.slice(&Count(2), Bias::Right).items(()), [1, 2]); - assert_eq!(cursor.item(), Some(&3)); - assert_eq!(cursor.prev_item(), Some(&2)); - assert_eq!(cursor.next_item(), Some(&4)); - assert_eq!(cursor.start().sum, 3); - - cursor.next(); - assert_eq!(cursor.item(), Some(&4)); - assert_eq!(cursor.prev_item(), Some(&3)); - assert_eq!(cursor.next_item(), Some(&5)); - assert_eq!(cursor.start().sum, 6); - - cursor.next(); - assert_eq!(cursor.item(), Some(&5)); - assert_eq!(cursor.prev_item(), Some(&4)); - assert_eq!(cursor.next_item(), Some(&6)); - assert_eq!(cursor.start().sum, 10); - - cursor.next(); - assert_eq!(cursor.item(), Some(&6)); - assert_eq!(cursor.prev_item(), Some(&5)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 15); - - cursor.next(); - cursor.next(); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&6)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 21); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&6)); - assert_eq!(cursor.prev_item(), Some(&5)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 15); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&5)); - assert_eq!(cursor.prev_item(), Some(&4)); - assert_eq!(cursor.next_item(), Some(&6)); - assert_eq!(cursor.start().sum, 10); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&4)); - assert_eq!(cursor.prev_item(), Some(&3)); - assert_eq!(cursor.next_item(), Some(&5)); - assert_eq!(cursor.start().sum, 6); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&3)); - assert_eq!(cursor.prev_item(), Some(&2)); - assert_eq!(cursor.next_item(), Some(&4)); - assert_eq!(cursor.start().sum, 3); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&2)); - assert_eq!(cursor.prev_item(), Some(&1)); - assert_eq!(cursor.next_item(), Some(&3)); - assert_eq!(cursor.start().sum, 1); - - cursor.prev(); - assert_eq!(cursor.item(), Some(&1)); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), Some(&2)); - assert_eq!(cursor.start().sum, 0); - - cursor.prev(); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), Some(&1)); - assert_eq!(cursor.start().sum, 0); - - cursor.next(); - assert_eq!(cursor.item(), Some(&1)); - assert_eq!(cursor.prev_item(), None); - assert_eq!(cursor.next_item(), Some(&2)); - assert_eq!(cursor.start().sum, 0); - - let mut cursor = tree.cursor::(()); - assert_eq!( - cursor - .slice(&tree.extent::(()), Bias::Right) - .items(()), - tree.items(()) - ); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&6)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 21); - - cursor.seek(&Count(3), Bias::Right); - assert_eq!( - cursor - .slice(&tree.extent::(()), Bias::Right) - .items(()), - [4, 5, 6] - ); - assert_eq!(cursor.item(), None); - assert_eq!(cursor.prev_item(), Some(&6)); - assert_eq!(cursor.next_item(), None); - assert_eq!(cursor.start().sum, 21); - - // Seeking can bias left or right - cursor.seek(&Count(1), Bias::Left); - assert_eq!(cursor.item(), Some(&1)); - cursor.seek(&Count(1), Bias::Right); - assert_eq!(cursor.item(), Some(&2)); - - // Slicing without resetting starts from where the cursor is parked at. - cursor.seek(&Count(1), Bias::Right); - assert_eq!(cursor.slice(&Count(3), Bias::Right).items(()), vec![2, 3]); - assert_eq!(cursor.slice(&Count(6), Bias::Left).items(()), vec![4, 5]); - assert_eq!(cursor.slice(&Count(6), Bias::Right).items(()), vec![6]); - } - - #[test] - fn test_edit() { - let mut tree = SumTree::::default(); - - let removed = tree.edit(vec![Edit::Insert(1), Edit::Insert(2), Edit::Insert(0)], ()); - assert_eq!(tree.items(()), vec![0, 1, 2]); - assert_eq!(removed, Vec::::new()); - assert_eq!(tree.get(&0, ()), Some(&0)); - assert_eq!(tree.get(&1, ()), Some(&1)); - assert_eq!(tree.get(&2, ()), Some(&2)); - assert_eq!(tree.get(&4, ()), None); - - let removed = tree.edit(vec![Edit::Insert(2), Edit::Insert(4), Edit::Remove(0)], ()); - assert_eq!(tree.items(()), vec![1, 2, 4]); - assert_eq!(removed, vec![0, 2]); - assert_eq!(tree.get(&0, ()), None); - assert_eq!(tree.get(&1, ()), Some(&1)); - assert_eq!(tree.get(&2, ()), Some(&2)); - assert_eq!(tree.get(&4, ()), Some(&4)); - } - - #[test] - fn test_from_iter() { - assert_eq!( - SumTree::from_iter(0..100, ()).items(()), - (0..100).collect::>() - ); - - // Ensure `from_iter` works correctly when the given iterator restarts - // after calling `next` if `None` was already returned. - let mut ix = 0; - let iterator = std::iter::from_fn(|| { - ix = (ix + 1) % 2; - if ix == 1 { Some(1) } else { None } - }); - assert_eq!(SumTree::from_iter(iterator, ()).items(()), vec![1]); - } - - #[derive(Clone, Default, Debug)] - pub struct IntegersSummary { - count: usize, - sum: usize, - contains_even: bool, - max: u8, - } - - #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] - struct Count(usize); - - #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] - struct Sum(usize); - - impl Item for u8 { - type Summary = IntegersSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - IntegersSummary { - count: 1, - sum: *self as usize, - contains_even: (*self & 1) == 0, - max: *self, - } - } - } - - impl KeyedItem for u8 { - type Key = u8; - - fn key(&self) -> Self::Key { - *self - } - } - - impl ContextLessSummary for IntegersSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, other: &Self) { - self.count += other.count; - self.sum += other.sum; - self.contains_even |= other.contains_even; - self.max = cmp::max(self.max, other.max); - } - } - - impl Dimension<'_, IntegersSummary> for u8 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - *self = summary.max; - } - } - - impl Dimension<'_, IntegersSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - self.0 += summary.count; - } - } - - impl SeekTarget<'_, IntegersSummary, IntegersSummary> for Count { - fn cmp(&self, cursor_location: &IntegersSummary, _: ()) -> Ordering { - self.0.cmp(&cursor_location.count) - } - } - - impl Dimension<'_, IntegersSummary> for Sum { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - self.0 += summary.sum; - } - } + use super::*; + use rand::{distr::StandardUniform, prelude::*}; + use std::cmp; + + #[test] + fn test_extend_and_push_tree() { + let mut tree1 = SumTree::default(); + tree1.extend(0..20, ()); + + let mut tree2 = SumTree::default(); + tree2.extend(50..100, ()); + + tree1.append(tree2, ()); + assert_eq!(tree1.items(()), (0..20).chain(50..100).collect::>()); + } + + #[test] + fn test_random() { + let mut starting_seed = 0; + if let Ok(value) = std::env::var("SEED") { + starting_seed = value.parse().expect("invalid SEED variable"); + } + let mut num_iterations = 100; + if let Ok(value) = std::env::var("ITERATIONS") { + num_iterations = value.parse().expect("invalid ITERATIONS variable"); + } + let num_operations = std::env::var("OPERATIONS") + .map_or(5, |o| o.parse().expect("invalid OPERATIONS variable")); + + for seed in starting_seed..(starting_seed + num_iterations) { + eprintln!("seed = {}", seed); + let mut rng = StdRng::seed_from_u64(seed); + + let rng = &mut rng; + let mut tree = SumTree::::default(); + let count = rng.random_range(0..10); + if rng.random() { + tree.extend(rng.sample_iter(StandardUniform).take(count), ()); + } else { + let items = rng + .sample_iter(StandardUniform) + .take(count) + .collect::>(); + tree.par_extend(items, ()); + } + + for _ in 0..num_operations { + let splice_end = rng.random_range(0..tree.extent::(()).0 + 1); + let splice_start = rng.random_range(0..splice_end + 1); + let count = rng.random_range(0..10); + let tree_end = tree.extent::(()); + let new_items = rng + .sample_iter(StandardUniform) + .take(count) + .collect::>(); + + let mut reference_items = tree.items(()); + reference_items.splice(splice_start..splice_end, new_items.clone()); + + tree = { + let mut cursor = tree.cursor::(()); + let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right); + if rng.random() { + new_tree.extend(new_items, ()); + } else { + new_tree.par_extend(new_items, ()); + } + cursor.seek(&Count(splice_end), Bias::Right); + new_tree.append(cursor.slice(&tree_end, Bias::Right), ()); + new_tree + }; + + assert_eq!(tree.items(()), reference_items); + assert_eq!( + tree.iter().collect::>(), + tree.cursor::<()>(()).collect::>() + ); + + log::info!("tree items: {:?}", tree.items(())); + + let mut filter_cursor = + tree.filter::<_, Count>((), |summary| summary.contains_even); + let expected_filtered_items = tree + .items(()) + .into_iter() + .enumerate() + .filter(|(_, item)| (item & 1) == 0) + .collect::>(); + + let mut item_ix = if rng.random() { + filter_cursor.next(); + 0 + } else { + filter_cursor.prev(); + expected_filtered_items.len().saturating_sub(1) + }; + while item_ix < expected_filtered_items.len() { + log::info!("filter_cursor, item_ix: {}", item_ix); + let actual_item = filter_cursor.item().unwrap(); + let (reference_index, reference_item) = expected_filtered_items[item_ix]; + assert_eq!(actual_item, &reference_item); + assert_eq!(filter_cursor.start().0, reference_index); + log::info!("next"); + filter_cursor.next(); + item_ix += 1; + + while item_ix > 0 && rng.random_bool(0.2) { + log::info!("prev"); + filter_cursor.prev(); + item_ix -= 1; + + if item_ix == 0 && rng.random_bool(0.2) { + filter_cursor.prev(); + assert_eq!(filter_cursor.item(), None); + assert_eq!(filter_cursor.start().0, 0); + filter_cursor.next(); + } + } + } + assert_eq!(filter_cursor.item(), None); + + let mut before_start = false; + let mut cursor = tree.cursor::(()); + let start_pos = rng.random_range(0..=reference_items.len()); + cursor.seek(&Count(start_pos), Bias::Right); + let mut pos = rng.random_range(start_pos..=reference_items.len()); + cursor.seek_forward(&Count(pos), Bias::Right); + + for i in 0..10 { + assert_eq!(cursor.start().0, pos); + + if pos > 0 { + assert_eq!(cursor.prev_item().unwrap(), &reference_items[pos - 1]); + } else { + assert_eq!(cursor.prev_item(), None); + } + + if pos < reference_items.len() && !before_start { + assert_eq!(cursor.item().unwrap(), &reference_items[pos]); + } else { + assert_eq!(cursor.item(), None); + } + + if before_start { + assert_eq!(cursor.next_item(), reference_items.first()); + } else if pos + 1 < reference_items.len() { + assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]); + } else { + assert_eq!(cursor.next_item(), None); + } + + if i < 5 { + cursor.next(); + if pos < reference_items.len() { + pos += 1; + before_start = false; + } + } else { + cursor.prev(); + if pos == 0 { + before_start = true; + } + pos = pos.saturating_sub(1); + } + } + } + + for _ in 0..10 { + let end = rng.random_range(0..tree.extent::(()).0 + 1); + let start = rng.random_range(0..end + 1); + let start_bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; + let end_bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; + + let mut cursor = tree.cursor::(()); + cursor.seek(&Count(start), start_bias); + let slice = cursor.slice(&Count(end), end_bias); + + cursor.seek(&Count(start), start_bias); + let summary = cursor.summary::<_, Sum>(&Count(end), end_bias); + + assert_eq!(summary.0, slice.summary().sum); + } + } + } + + #[test] + fn test_cursor() { + // Empty tree + let tree = SumTree::::default(); + let mut cursor = tree.cursor::(()); + assert_eq!( + cursor.slice(&Count(0), Bias::Right).items(()), + Vec::::new() + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.prev(); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.next(); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + // Single-element tree + let mut tree = SumTree::::default(); + tree.extend(vec![1], ()); + let mut cursor = tree.cursor::(()); + assert_eq!( + cursor.slice(&Count(0), Bias::Right).items(()), + Vec::::new() + ); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + cursor.next(); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + let mut cursor = tree.cursor::(()); + assert_eq!(cursor.slice(&Count(1), Bias::Right).items(()), [1]); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + cursor.seek(&Count(0), Bias::Right); + assert_eq!( + cursor + .slice(&tree.extent::(()), Bias::Right) + .items(()), + [1] + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + // Multiple-element tree + let mut tree = SumTree::default(); + tree.extend(vec![1, 2, 3, 4, 5, 6], ()); + let mut cursor = tree.cursor::(()); + + assert_eq!(cursor.slice(&Count(2), Bias::Right).items(()), [1, 2]); + assert_eq!(cursor.item(), Some(&3)); + assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); + assert_eq!(cursor.start().sum, 3); + + cursor.next(); + assert_eq!(cursor.item(), Some(&4)); + assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); + assert_eq!(cursor.start().sum, 6); + + cursor.next(); + assert_eq!(cursor.item(), Some(&5)); + assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); + assert_eq!(cursor.start().sum, 10); + + cursor.next(); + assert_eq!(cursor.item(), Some(&6)); + assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 15); + + cursor.next(); + cursor.next(); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&6)); + assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 15); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&5)); + assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); + assert_eq!(cursor.start().sum, 10); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&4)); + assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); + assert_eq!(cursor.start().sum, 6); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&3)); + assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); + assert_eq!(cursor.start().sum, 3); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&2)); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), Some(&3)); + assert_eq!(cursor.start().sum, 1); + + cursor.prev(); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); + assert_eq!(cursor.start().sum, 0); + + cursor.prev(); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&1)); + assert_eq!(cursor.start().sum, 0); + + cursor.next(); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); + assert_eq!(cursor.start().sum, 0); + + let mut cursor = tree.cursor::(()); + assert_eq!( + cursor + .slice(&tree.extent::(()), Bias::Right) + .items(()), + tree.items(()) + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + cursor.seek(&Count(3), Bias::Right); + assert_eq!( + cursor + .slice(&tree.extent::(()), Bias::Right) + .items(()), + [4, 5, 6] + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + // Seeking can bias left or right + cursor.seek(&Count(1), Bias::Left); + assert_eq!(cursor.item(), Some(&1)); + cursor.seek(&Count(1), Bias::Right); + assert_eq!(cursor.item(), Some(&2)); + + // Slicing without resetting starts from where the cursor is parked at. + cursor.seek(&Count(1), Bias::Right); + assert_eq!(cursor.slice(&Count(3), Bias::Right).items(()), vec![2, 3]); + assert_eq!(cursor.slice(&Count(6), Bias::Left).items(()), vec![4, 5]); + assert_eq!(cursor.slice(&Count(6), Bias::Right).items(()), vec![6]); + } + + #[test] + fn test_edit() { + let mut tree = SumTree::::default(); + + let removed = tree.edit(vec![Edit::Insert(1), Edit::Insert(2), Edit::Insert(0)], ()); + assert_eq!(tree.items(()), vec![0, 1, 2]); + assert_eq!(removed, Vec::::new()); + assert_eq!(tree.get(&0, ()), Some(&0)); + assert_eq!(tree.get(&1, ()), Some(&1)); + assert_eq!(tree.get(&2, ()), Some(&2)); + assert_eq!(tree.get(&4, ()), None); + + let removed = tree.edit(vec![Edit::Insert(2), Edit::Insert(4), Edit::Remove(0)], ()); + assert_eq!(tree.items(()), vec![1, 2, 4]); + assert_eq!(removed, vec![0, 2]); + assert_eq!(tree.get(&0, ()), None); + assert_eq!(tree.get(&1, ()), Some(&1)); + assert_eq!(tree.get(&2, ()), Some(&2)); + assert_eq!(tree.get(&4, ()), Some(&4)); + } + + #[test] + fn test_from_iter() { + assert_eq!( + SumTree::from_iter(0..100, ()).items(()), + (0..100).collect::>() + ); + + // Ensure `from_iter` works correctly when the given iterator restarts + // after calling `next` if `None` was already returned. + let mut ix = 0; + let iterator = std::iter::from_fn(|| { + ix = (ix + 1) % 2; + if ix == 1 { Some(1) } else { None } + }); + assert_eq!(SumTree::from_iter(iterator, ()).items(()), vec![1]); + } + + #[derive(Clone, Default, Debug)] + pub struct IntegersSummary { + count: usize, + sum: usize, + contains_even: bool, + max: u8, + } + + #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] + struct Count(usize); + + #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] + struct Sum(usize); + + impl Item for u8 { + type Summary = IntegersSummary; + + fn summary(&self, _cx: ()) -> Self::Summary { + IntegersSummary { + count: 1, + sum: *self as usize, + contains_even: (*self & 1) == 0, + max: *self, + } + } + } + + impl KeyedItem for u8 { + type Key = u8; + + fn key(&self) -> Self::Key { + *self + } + } + + impl ContextLessSummary for IntegersSummary { + fn zero() -> Self { + Default::default() + } + + fn add_summary(&mut self, other: &Self) { + self.count += other.count; + self.sum += other.sum; + self.contains_even |= other.contains_even; + self.max = cmp::max(self.max, other.max); + } + } + + impl Dimension<'_, IntegersSummary> for u8 { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { + *self = summary.max; + } + } + + impl Dimension<'_, IntegersSummary> for Count { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { + self.0 += summary.count; + } + } + + impl SeekTarget<'_, IntegersSummary, IntegersSummary> for Count { + fn cmp(&self, cursor_location: &IntegersSummary, _: ()) -> Ordering { + self.0.cmp(&cursor_location.count) + } + } + + impl Dimension<'_, IntegersSummary> for Sum { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { + self.0 += summary.sum; + } + } } diff --git a/crates/gpui_sum_tree/src/tree_map.rs b/crates/gpui_sum_tree/src/tree_map.rs index 004ec91851..049c902d10 100644 --- a/crates/gpui_sum_tree/src/tree_map.rs +++ b/crates/gpui_sum_tree/src/tree_map.rs @@ -6,526 +6,526 @@ use crate::{Bias, ContextLessSummary, Dimension, Edit, Item, KeyedItem, SeekTarg #[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where - K: Clone + Ord, - V: Clone; + K: Clone + Ord, + V: Clone; #[derive(Clone, Debug, PartialEq, Eq)] pub struct MapEntry { - key: K, - value: V, + key: K, + value: V, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct MapKey(Option); impl Default for MapKey { - fn default() -> Self { - Self(None) - } + fn default() -> Self { + Self(None) + } } #[derive(Clone, Debug)] pub struct MapKeyRef<'a, K>(Option<&'a K>); impl Default for MapKeyRef<'_, K> { - fn default() -> Self { - Self(None) - } + fn default() -> Self { + Self(None) + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeSet(TreeMap) where - K: Clone + Ord; + K: Clone + Ord; impl TreeMap { - pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { - let tree = SumTree::from_iter( - entries - .into_iter() - .map(|(key, value)| MapEntry { key, value }), - (), - ); - Self(tree) - } + pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { + let tree = SumTree::from_iter( + entries + .into_iter() + .map(|(key, value)| MapEntry { key, value }), + (), + ); + Self(tree) + } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } - pub fn contains_key(&self, key: &K) -> bool { - self.get(key).is_some() - } + pub fn contains_key(&self, key: &K) -> bool { + self.get(key).is_some() + } - pub fn get(&self, key: &K) -> Option<&V> { - let (.., item) = self - .0 - .find::, _>((), &MapKeyRef(Some(key)), Bias::Left); - if let Some(item) = item { - if Some(key) == item.key().0.as_ref() { - Some(&item.value) - } else { - None - } - } else { - None - } - } + pub fn get(&self, key: &K) -> Option<&V> { + let (.., item) = self + .0 + .find::, _>((), &MapKeyRef(Some(key)), Bias::Left); + if let Some(item) = item { + if Some(key) == item.key().0.as_ref() { + Some(&item.value) + } else { + None + } + } else { + None + } + } - pub fn insert(&mut self, key: K, value: V) { - self.0.insert_or_replace(MapEntry { key, value }, ()); - } + pub fn insert(&mut self, key: K, value: V) { + self.0.insert_or_replace(MapEntry { key, value }, ()); + } - pub fn insert_or_replace(&mut self, key: K, value: V) -> Option { - self.0 - .insert_or_replace(MapEntry { key, value }, ()) - .map(|it| it.value) - } + pub fn insert_or_replace(&mut self, key: K, value: V) -> Option { + self.0 + .insert_or_replace(MapEntry { key, value }, ()) + .map(|it| it.value) + } - pub fn extend(&mut self, iter: impl IntoIterator) { - let edits: Vec<_> = iter - .into_iter() - .map(|(key, value)| Edit::Insert(MapEntry { key, value })) - .collect(); - self.0.edit(edits, ()); - } + pub fn extend(&mut self, iter: impl IntoIterator) { + let edits: Vec<_> = iter + .into_iter() + .map(|(key, value)| Edit::Insert(MapEntry { key, value })) + .collect(); + self.0.edit(edits, ()); + } - pub fn clear(&mut self) { - self.0 = SumTree::default(); - } + pub fn clear(&mut self) { + self.0 = SumTree::default(); + } - pub fn remove(&mut self, key: &K) -> Option { - let mut removed = None; - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left); - if key.cmp(&cursor.end(), ()) == Ordering::Equal { - removed = Some(cursor.item().unwrap().value.clone()); - cursor.next(); - } - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - removed - } + pub fn remove(&mut self, key: &K) -> Option { + let mut removed = None; + let mut cursor = self.0.cursor::>(()); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left); + if key.cmp(&cursor.end(), ()) == Ordering::Equal { + removed = Some(cursor.item().unwrap().value.clone()); + cursor.next(); + } + new_tree.append(cursor.suffix(), ()); + drop(cursor); + self.0 = new_tree; + removed + } - pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { - let start = MapSeekTargetAdaptor(start); - let end = MapSeekTargetAdaptor(end); - let mut cursor = self.0.cursor::>(()); - let mut new_tree = cursor.slice(&start, Bias::Left); - cursor.seek(&end, Bias::Left); - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - } + pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { + let start = MapSeekTargetAdaptor(start); + let end = MapSeekTargetAdaptor(end); + let mut cursor = self.0.cursor::>(()); + let mut new_tree = cursor.slice(&start, Bias::Left); + cursor.seek(&end, Bias::Left); + new_tree.append(cursor.suffix(), ()); + drop(cursor); + self.0 = new_tree; + } - /// Returns the key-value pair with the greatest key less than or equal to the given key. - pub fn closest(&self, key: &K) -> Option<(&K, &V)> { - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - cursor.seek(&key, Bias::Right); - cursor.prev(); - cursor.item().map(|item| (&item.key, &item.value)) - } + /// Returns the key-value pair with the greatest key less than or equal to the given key. + pub fn closest(&self, key: &K) -> Option<(&K, &V)> { + let mut cursor = self.0.cursor::>(()); + let key = MapKeyRef(Some(key)); + cursor.seek(&key, Bias::Right); + cursor.prev(); + cursor.item().map(|item| (&item.key, &item.value)) + } - pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator + 'a { - let mut cursor = self.0.cursor::>(()); - let from_key = MapKeyRef(Some(from)); - cursor.seek(&from_key, Bias::Left); + pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator + 'a { + let mut cursor = self.0.cursor::>(()); + let from_key = MapKeyRef(Some(from)); + cursor.seek(&from_key, Bias::Left); - cursor.map(|map_entry| (&map_entry.key, &map_entry.value)) - } + cursor.map(|map_entry| (&map_entry.key, &map_entry.value)) + } - pub fn update(&mut self, key: &K, f: F) -> Option - where - F: FnOnce(&mut V) -> T, - { - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left); - let mut result = None; - if key.cmp(&cursor.end(), ()) == Ordering::Equal { - let mut updated = cursor.item().unwrap().clone(); - result = Some(f(&mut updated.value)); - new_tree.push(updated, ()); - cursor.next(); - } - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - result - } + pub fn update(&mut self, key: &K, f: F) -> Option + where + F: FnOnce(&mut V) -> T, + { + let mut cursor = self.0.cursor::>(()); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left); + let mut result = None; + if key.cmp(&cursor.end(), ()) == Ordering::Equal { + let mut updated = cursor.item().unwrap().clone(); + result = Some(f(&mut updated.value)); + new_tree.push(updated, ()); + cursor.next(); + } + new_tree.append(cursor.suffix(), ()); + drop(cursor); + self.0 = new_tree; + result + } - pub fn retain bool>(&mut self, mut predicate: F) { - let mut new_map = SumTree::>::default(); + pub fn retain bool>(&mut self, mut predicate: F) { + let mut new_map = SumTree::>::default(); - let mut cursor = self.0.cursor::>(()); - cursor.next(); - while let Some(item) = cursor.item() { - if predicate(&item.key, &item.value) { - new_map.push(item.clone(), ()); - } - cursor.next(); - } - drop(cursor); + let mut cursor = self.0.cursor::>(()); + cursor.next(); + while let Some(item) = cursor.item() { + if predicate(&item.key, &item.value) { + new_map.push(item.clone(), ()); + } + cursor.next(); + } + drop(cursor); - self.0 = new_map; - } + self.0 = new_map; + } - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().map(|entry| (&entry.key, &entry.value)) - } + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|entry| (&entry.key, &entry.value)) + } - pub fn values(&self) -> impl Iterator + '_ { - self.0.iter().map(|entry| &entry.value) - } + pub fn values(&self) -> impl Iterator + '_ { + self.0.iter().map(|entry| &entry.value) + } - pub fn first(&self) -> Option<(&K, &V)> { - self.0.first().map(|entry| (&entry.key, &entry.value)) - } + pub fn first(&self) -> Option<(&K, &V)> { + self.0.first().map(|entry| (&entry.key, &entry.value)) + } - pub fn last(&self) -> Option<(&K, &V)> { - self.0.last().map(|entry| (&entry.key, &entry.value)) - } + pub fn last(&self) -> Option<(&K, &V)> { + self.0.last().map(|entry| (&entry.key, &entry.value)) + } - pub fn insert_tree(&mut self, other: TreeMap) { - let edits = other - .iter() - .map(|(key, value)| { - Edit::Insert(MapEntry { - key: key.to_owned(), - value: value.to_owned(), - }) - }) - .collect(); + pub fn insert_tree(&mut self, other: TreeMap) { + let edits = other + .iter() + .map(|(key, value)| { + Edit::Insert(MapEntry { + key: key.to_owned(), + value: value.to_owned(), + }) + }) + .collect(); - self.0.edit(edits, ()); - } + self.0.edit(edits, ()); + } } impl Debug for TreeMap where - K: Clone + Debug + Ord, - V: Clone + Debug, + K: Clone + Debug + Ord, + V: Clone + Debug, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_map().entries(self.iter()).finish() - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } } #[derive(Debug)] struct MapSeekTargetAdaptor<'a, T>(&'a T); impl<'a, K: Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> - for MapSeekTargetAdaptor<'_, T> + for MapSeekTargetAdaptor<'_, T> { - fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { - if let Some(key) = &cursor_location.0 { - MapSeekTarget::cmp_cursor(self.0, key) - } else { - Ordering::Greater - } - } + fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { + if let Some(key) = &cursor_location.0 { + MapSeekTarget::cmp_cursor(self.0, key) + } else { + Ordering::Greater + } + } } pub trait MapSeekTarget { - fn cmp_cursor(&self, cursor_location: &K) -> Ordering; + fn cmp_cursor(&self, cursor_location: &K) -> Ordering; } impl MapSeekTarget for K { - fn cmp_cursor(&self, cursor_location: &K) -> Ordering { - self.cmp(cursor_location) - } + fn cmp_cursor(&self, cursor_location: &K) -> Ordering { + self.cmp(cursor_location) + } } impl Default for TreeMap where - K: Clone + Ord, - V: Clone, + K: Clone + Ord, + V: Clone, { - fn default() -> Self { - Self(Default::default()) - } + fn default() -> Self { + Self(Default::default()) + } } impl Item for MapEntry where - K: Clone + Ord, - V: Clone, + K: Clone + Ord, + V: Clone, { - type Summary = MapKey; + type Summary = MapKey; - fn summary(&self, _cx: ()) -> Self::Summary { - self.key() - } + fn summary(&self, _cx: ()) -> Self::Summary { + self.key() + } } impl KeyedItem for MapEntry where - K: Clone + Ord, - V: Clone, + K: Clone + Ord, + V: Clone, { - type Key = MapKey; + type Key = MapKey; - fn key(&self) -> Self::Key { - MapKey(Some(self.key.clone())) - } + fn key(&self) -> Self::Key { + MapKey(Some(self.key.clone())) + } } impl ContextLessSummary for MapKey where - K: Clone, + K: Clone, { - fn zero() -> Self { - Default::default() - } + fn zero() -> Self { + Default::default() + } - fn add_summary(&mut self, summary: &Self) { - *self = summary.clone() - } + fn add_summary(&mut self, summary: &Self) { + *self = summary.clone() + } } impl<'a, K> Dimension<'a, MapKey> for MapKeyRef<'a, K> where - K: Clone + Ord, + K: Clone + Ord, { - fn zero(_cx: ()) -> Self { - Default::default() - } + fn zero(_cx: ()) -> Self { + Default::default() + } - fn add_summary(&mut self, summary: &'a MapKey, _: ()) { - self.0 = summary.0.as_ref(); - } + fn add_summary(&mut self, summary: &'a MapKey, _: ()) { + self.0 = summary.0.as_ref(); + } } impl<'a, K> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapKeyRef<'_, K> where - K: Clone + Ord, + K: Clone + Ord, { - fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { - Ord::cmp(&self.0, &cursor_location.0) - } + fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { + Ord::cmp(&self.0, &cursor_location.0) + } } impl Default for TreeSet where - K: Clone + Ord, + K: Clone + Ord, { - fn default() -> Self { - Self(Default::default()) - } + fn default() -> Self { + Self(Default::default()) + } } impl TreeSet where - K: Clone + Ord, + K: Clone + Ord, { - pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { - Self(TreeMap::from_ordered_entries( - entries.into_iter().map(|key| (key, ())), - )) - } + pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { + Self(TreeMap::from_ordered_entries( + entries.into_iter().map(|key| (key, ())), + )) + } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } - pub fn insert(&mut self, key: K) { - self.0.insert(key, ()); - } + pub fn insert(&mut self, key: K) { + self.0.insert(key, ()); + } - pub fn remove(&mut self, key: &K) -> bool { - self.0.remove(key).is_some() - } + pub fn remove(&mut self, key: &K) -> bool { + self.0.remove(key).is_some() + } - pub fn extend(&mut self, iter: impl IntoIterator) { - self.0.extend(iter.into_iter().map(|key| (key, ()))); - } + pub fn extend(&mut self, iter: impl IntoIterator) { + self.0.extend(iter.into_iter().map(|key| (key, ()))); + } - pub fn contains(&self, key: &K) -> bool { - self.0.get(key).is_some() - } + pub fn contains(&self, key: &K) -> bool { + self.0.get(key).is_some() + } - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().map(|(k, _)| k) - } + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|(k, _)| k) + } - pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator + 'a { - self.0.iter_from(key).map(move |(k, _)| k) - } + pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator + 'a { + self.0.iter_from(key).map(move |(k, _)| k) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_basic() { - let mut map = TreeMap::default(); - assert_eq!(map.iter().collect::>(), vec![]); + #[test] + fn test_basic() { + let mut map = TreeMap::default(); + assert_eq!(map.iter().collect::>(), vec![]); - map.insert(3, "c"); - assert_eq!(map.get(&3), Some(&"c")); - assert_eq!(map.iter().collect::>(), vec![(&3, &"c")]); + map.insert(3, "c"); + assert_eq!(map.get(&3), Some(&"c")); + assert_eq!(map.iter().collect::>(), vec![(&3, &"c")]); - map.insert(1, "a"); - assert_eq!(map.get(&1), Some(&"a")); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); + map.insert(1, "a"); + assert_eq!(map.get(&1), Some(&"a")); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); - map.insert(2, "b"); - assert_eq!(map.get(&2), Some(&"b")); - assert_eq!(map.get(&1), Some(&"a")); - assert_eq!(map.get(&3), Some(&"c")); - assert_eq!( - map.iter().collect::>(), - vec![(&1, &"a"), (&2, &"b"), (&3, &"c")] - ); + map.insert(2, "b"); + assert_eq!(map.get(&2), Some(&"b")); + assert_eq!(map.get(&1), Some(&"a")); + assert_eq!(map.get(&3), Some(&"c")); + assert_eq!( + map.iter().collect::>(), + vec![(&1, &"a"), (&2, &"b"), (&3, &"c")] + ); - assert_eq!(map.closest(&0), None); - assert_eq!(map.closest(&1), Some((&1, &"a"))); - assert_eq!(map.closest(&10), Some((&3, &"c"))); + assert_eq!(map.closest(&0), None); + assert_eq!(map.closest(&1), Some((&1, &"a"))); + assert_eq!(map.closest(&10), Some((&3, &"c"))); - map.remove(&2); - assert_eq!(map.get(&2), None); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); + map.remove(&2); + assert_eq!(map.get(&2), None); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); - assert_eq!(map.closest(&2), Some((&1, &"a"))); + assert_eq!(map.closest(&2), Some((&1, &"a"))); - map.remove(&3); - assert_eq!(map.get(&3), None); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a")]); + map.remove(&3); + assert_eq!(map.get(&3), None); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a")]); - map.remove(&1); - assert_eq!(map.get(&1), None); - assert_eq!(map.iter().collect::>(), vec![]); + map.remove(&1); + assert_eq!(map.get(&1), None); + assert_eq!(map.iter().collect::>(), vec![]); - map.insert(4, "d"); - map.insert(5, "e"); - map.insert(6, "f"); - map.retain(|key, _| *key % 2 == 0); - assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); - } + map.insert(4, "d"); + map.insert(5, "e"); + map.insert(6, "f"); + map.retain(|key, _| *key % 2 == 0); + assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); + } - #[test] - fn test_iter_from() { - let mut map = TreeMap::default(); + #[test] + fn test_iter_from() { + let mut map = TreeMap::default(); - map.insert("a", 1); - map.insert("b", 2); - map.insert("baa", 3); - map.insert("baaab", 4); - map.insert("c", 5); + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); - let result = map - .iter_from(&"ba") - .take_while(|(key, _)| key.starts_with("ba")) - .collect::>(); + let result = map + .iter_from(&"ba") + .take_while(|(key, _)| key.starts_with("ba")) + .collect::>(); - assert_eq!(result.len(), 2); - assert!(result.iter().any(|(k, _)| k == &&"baa")); - assert!(result.iter().any(|(k, _)| k == &&"baaab")); + assert_eq!(result.len(), 2); + assert!(result.iter().any(|(k, _)| k == &&"baa")); + assert!(result.iter().any(|(k, _)| k == &&"baaab")); - let result = map - .iter_from(&"c") - .take_while(|(key, _)| key.starts_with("c")) - .collect::>(); + let result = map + .iter_from(&"c") + .take_while(|(key, _)| key.starts_with("c")) + .collect::>(); - assert_eq!(result.len(), 1); - assert!(result.iter().any(|(k, _)| k == &&"c")); - } + assert_eq!(result.len(), 1); + assert!(result.iter().any(|(k, _)| k == &&"c")); + } - #[test] - fn test_insert_tree() { - let mut map = TreeMap::default(); - map.insert("a", 1); - map.insert("b", 2); - map.insert("c", 3); + #[test] + fn test_insert_tree() { + let mut map = TreeMap::default(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); - let mut other = TreeMap::default(); - other.insert("a", 2); - other.insert("b", 2); - other.insert("d", 4); + let mut other = TreeMap::default(); + other.insert("a", 2); + other.insert("b", 2); + other.insert("d", 4); - map.insert_tree(other); + map.insert_tree(other); - assert_eq!(map.iter().count(), 4); - assert_eq!(map.get(&"a"), Some(&2)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"c"), Some(&3)); - assert_eq!(map.get(&"d"), Some(&4)); - } + assert_eq!(map.iter().count(), 4); + assert_eq!(map.get(&"a"), Some(&2)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), Some(&3)); + assert_eq!(map.get(&"d"), Some(&4)); + } - #[test] - fn test_extend() { - let mut map = TreeMap::default(); - map.insert("a", 1); - map.insert("b", 2); - map.insert("c", 3); - map.extend([("a", 2), ("b", 2), ("d", 4)]); - assert_eq!(map.iter().count(), 4); - assert_eq!(map.get(&"a"), Some(&2)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"c"), Some(&3)); - assert_eq!(map.get(&"d"), Some(&4)); - } + #[test] + fn test_extend() { + let mut map = TreeMap::default(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + map.extend([("a", 2), ("b", 2), ("d", 4)]); + assert_eq!(map.iter().count(), 4); + assert_eq!(map.get(&"a"), Some(&2)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), Some(&3)); + assert_eq!(map.get(&"d"), Some(&4)); + } - #[test] - fn test_remove_between_and_path_successor() { - use std::path::{Path, PathBuf}; + #[test] + fn test_remove_between_and_path_successor() { + use std::path::{Path, PathBuf}; - #[derive(Debug)] - pub struct PathDescendants<'a>(&'a Path); + #[derive(Debug)] + pub struct PathDescendants<'a>(&'a Path); - impl MapSeekTarget for PathDescendants<'_> { - fn cmp_cursor(&self, key: &PathBuf) -> Ordering { - if key.starts_with(self.0) { - Ordering::Greater - } else { - self.0.cmp(key) - } - } - } + impl MapSeekTarget for PathDescendants<'_> { + fn cmp_cursor(&self, key: &PathBuf) -> Ordering { + if key.starts_with(self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } + } - let mut map = TreeMap::default(); + let mut map = TreeMap::default(); - map.insert(PathBuf::from("a"), 1); - map.insert(PathBuf::from("a/a"), 1); - map.insert(PathBuf::from("b"), 2); - map.insert(PathBuf::from("b/a/a"), 3); - map.insert(PathBuf::from("b/a/a/a/b"), 4); - map.insert(PathBuf::from("c"), 5); - map.insert(PathBuf::from("c/a"), 6); + map.insert(PathBuf::from("a"), 1); + map.insert(PathBuf::from("a/a"), 1); + map.insert(PathBuf::from("b"), 2); + map.insert(PathBuf::from("b/a/a"), 3); + map.insert(PathBuf::from("b/a/a/a/b"), 4); + map.insert(PathBuf::from("c"), 5); + map.insert(PathBuf::from("c/a"), 6); - map.remove_range( - &PathBuf::from("b/a"), - &PathDescendants(&PathBuf::from("b/a")), - ); + map.remove_range( + &PathBuf::from("b/a"), + &PathDescendants(&PathBuf::from("b/a")), + ); - assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - assert_eq!(map.get(&PathBuf::from("b/a/a")), None); - assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); - assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); - assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("b/a/a")), None); + assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); + assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); + assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); - map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); + map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); - assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - assert_eq!(map.get(&PathBuf::from("c")), None); - assert_eq!(map.get(&PathBuf::from("c/a")), None); + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("c")), None); + assert_eq!(map.get(&PathBuf::from("c/a")), None); - map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); + map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); - assert_eq!(map.get(&PathBuf::from("a")), None); - assert_eq!(map.get(&PathBuf::from("a/a")), None); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("a")), None); + assert_eq!(map.get(&PathBuf::from("a/a")), None); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); + map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); - assert_eq!(map.get(&PathBuf::from("b")), None); - } + assert_eq!(map.get(&PathBuf::from("b")), None); + } } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index d32843ae86..1cb81e360e 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -10,91 +10,91 @@ pub use tokio::task::JoinError; /// If you need more threads (or access to the runtime outside of GPUI), you can create the runtime /// yourself and pass a Handle to `init_from_handle`. pub fn init(cx: &mut App) { - let runtime = tokio::runtime::Builder::new_multi_thread() - // Since we now have two executors, let's try to keep our footprint small - .worker_threads(2) - .enable_all() - .build() - .expect("Failed to initialize Tokio"); + let runtime = tokio::runtime::Builder::new_multi_thread() + // Since we now have two executors, let's try to keep our footprint small + .worker_threads(2) + .enable_all() + .build() + .expect("Failed to initialize Tokio"); - let handle = runtime.handle().clone(); - cx.set_global(GlobalTokio { - owned_runtime: Some(runtime), - handle, - }); + let handle = runtime.handle().clone(); + cx.set_global(GlobalTokio { + owned_runtime: Some(runtime), + handle, + }); } /// Initializes the Tokio wrapper using a Tokio runtime handle. pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) { - cx.set_global(GlobalTokio { - owned_runtime: None, - handle, - }); + cx.set_global(GlobalTokio { + owned_runtime: None, + handle, + }); } struct GlobalTokio { - owned_runtime: Option, - handle: tokio::runtime::Handle, + owned_runtime: Option, + handle: tokio::runtime::Handle, } impl Global for GlobalTokio {} impl Drop for GlobalTokio { - fn drop(&mut self) { - if let Some(runtime) = self.owned_runtime.take() { - runtime.shutdown_background(); - } - } + fn drop(&mut self) { + if let Some(runtime) = self.owned_runtime.take() { + runtime.shutdown_background(); + } + } } pub struct Tokio {} impl Tokio { - /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task - /// Note that the Tokio task will be cancelled if the GPUI task is dropped - pub fn spawn(cx: &C, f: Fut) -> Task> - where - C: AppContext, - Fut: Future + Send + 'static, - R: Send + 'static, - { - cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.handle.spawn(f); - let abort_handle = join_handle.abort_handle(); - let cancel = defer(move || { - abort_handle.abort(); - }); - cx.background_spawn(async move { - let result = join_handle.await; - drop(cancel); - result - }) - }) - } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn(cx: &C, f: Fut) -> Task> + where + C: AppContext, + Fut: Future + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.handle.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await; + drop(cancel); + result + }) + }) + } - /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task - /// Note that the Tokio task will be cancelled if the GPUI task is dropped - pub fn spawn_result(cx: &C, f: Fut) -> Task> - where - C: AppContext, - Fut: Future> + Send + 'static, - R: Send + 'static, - { - cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.handle.spawn(f); - let abort_handle = join_handle.abort_handle(); - let cancel = defer(move || { - abort_handle.abort(); - }); - cx.background_spawn(async move { - let result = join_handle.await?; - drop(cancel); - result - }) - }) - } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> Task> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.handle.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } - pub fn handle(cx: &App) -> tokio::runtime::Handle { - GlobalTokio::global(cx).handle.clone() - } + pub fn handle(cx: &App) -> tokio::runtime::Handle { + GlobalTokio::global(cx).handle.clone() + } } diff --git a/crates/gpui_web/src/dispatcher.rs b/crates/gpui_web/src/dispatcher.rs index 21283dc64c..7f477b8800 100644 --- a/crates/gpui_web/src/dispatcher.rs +++ b/crates/gpui_web/src/dispatcher.rs @@ -1,6 +1,6 @@ use gpui::{ - PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, - ThreadTaskTimings, + PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, + ThreadTaskTimings, }; use std::sync::Arc; use std::sync::atomic::AtomicI32; @@ -12,131 +12,131 @@ use web_time::Instant; const MIN_BACKGROUND_THREADS: usize = 2; fn shared_memory_supported() -> bool { - #[cfg(feature = "multithreaded")] - { - 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 - } - #[cfg(not(feature = "multithreaded"))] - { - false - } + #[cfg(feature = "multithreaded")] + { + 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 + } + #[cfg(not(feature = "multithreaded"))] + { + false + } } enum MainThreadItem { - Runnable(RunnableVariant), - Delayed { - runnable: RunnableVariant, - millis: i32, - }, - // TODO-Wasm: Shouldn't these run on their own dedicated thread? - RealtimeFunction(Box), + Runnable(RunnableVariant), + Delayed { + runnable: RunnableVariant, + millis: i32, + }, + // TODO-Wasm: Shouldn't these run on their own dedicated thread? + RealtimeFunction(Box), } struct MainThreadMailbox { - sender: PriorityQueueSender, - receiver: parking_lot::Mutex>, - signal: AtomicI32, + 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 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"); - } + 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(); - } + // 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 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 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; - } + 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"); + 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"); - let result = match js_sys::Atomics::wait_async(&view, 0, 0) { - Ok(result) => result, - Err(error) => { - log::error!("Atomics.waitAsync failed: {error:?}"); - break; - } - }; + 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); + let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async")) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false); - if !is_async { - log::error!("Atomics.waitAsync returned synchronously; waker loop exiting"); - break; - } + if !is_async { + log::error!("Atomics.waitAsync returned synchronously; waker loop exiting"); + break; + } - let promise: js_sys::Promise = - js_sys::Reflect::get(&result, &JsValue::from_str("value")) - .expect("waitAsync result missing 'value'") - .unchecked_into(); + 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; + let _ = wasm_bindgen_futures::JsFuture::from(promise).await; - mailbox.drain(&window); - } - }); - } + mailbox.drain(&window); + } + }); + } } pub struct WebDispatcher { - main_thread_id: std::thread::ThreadId, - browser_window: web_sys::Window, - background_sender: PriorityQueueSender, - main_thread_mailbox: Arc, - supports_threads: bool, - #[cfg(feature = "multithreaded")] - _background_threads: Vec>, + main_thread_id: std::thread::ThreadId, + browser_window: web_sys::Window, + background_sender: PriorityQueueSender, + main_thread_mailbox: Arc, + supports_threads: bool, + #[cfg(feature = "multithreaded")] + _background_threads: Vec>, } // Safety: `web_sys::Window` is only accessed from the main thread @@ -145,196 +145,196 @@ unsafe impl Send for WebDispatcher {} unsafe impl Sync for WebDispatcher {} 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(); + 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 main_thread_mailbox = Arc::new(MainThreadMailbox::new()); - #[cfg(feature = "multithreaded")] - let supports_threads = _allow_threads && shared_memory_supported(); - #[cfg(not(feature = "multithreaded"))] - let supports_threads = false; + #[cfg(feature = "multithreaded")] + let supports_threads = _allow_threads && shared_memory_supported(); + #[cfg(not(feature = "multithreaded"))] + let supports_threads = false; - if supports_threads { - main_thread_mailbox.run_waker_loop(browser_window.clone()); - } else { - log::warn!( - "SharedArrayBuffer not available; falling back to single-threaded dispatcher" - ); - } + if supports_threads { + main_thread_mailbox.run_waker_loop(browser_window.clone()); + } else { + log::warn!( + "SharedArrayBuffer not available; 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; + #[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; - } - }; + // 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() - }; + runnable.run(); + } + }) + .expect("failed to spawn background worker thread") + }) + .collect::>() + } else { + Vec::new() + }; - Self { - main_thread_id: std::thread::current().id(), - browser_window, - background_sender, - main_thread_mailbox, - supports_threads, - #[cfg(feature = "multithreaded")] - _background_threads: background_threads, - } - } + Self { + main_thread_id: std::thread::current().id(), + browser_window, + 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 - } + fn on_main_thread(&self) -> bool { + std::thread::current().id() == self.main_thread_id + } } impl PlatformDispatcher for WebDispatcher { - fn get_all_timings(&self) -> Vec { - // TODO-Wasm: should we panic here? - Vec::new() - } + fn get_all_timings(&self) -> Vec { + // TODO-Wasm: should we panic here? + Vec::new() + } - fn get_current_thread_timings(&self) -> ThreadTaskTimings { - ThreadTaskTimings { - thread_name: None, - thread_id: std::thread::current().id(), - timings: Vec::new(), - total_pushed: 0, - } - } + fn get_current_thread_timings(&self) -> ThreadTaskTimings { + ThreadTaskTimings { + thread_name: None, + thread_id: std::thread::current().id(), + timings: Vec::new(), + total_pushed: 0, + } + } - fn is_main_thread(&self) -> bool { - self.on_main_thread() - } + 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; - } + 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) - }; + 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:?}"); - } - } + 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(&self.browser_window, runnable, priority); - } else { - self.main_thread_mailbox - .post(priority, MainThreadItem::Runnable(runnable)); - } - } + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { + if self.on_main_thread() { + schedule_runnable(&self.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(); - }); - self.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 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(); + }); + self.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(); - }); - self.browser_window - .queue_microtask(callback.unchecked_ref()); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::RealtimeFunction(function)); - } - } + fn spawn_realtime(&self, function: Box) { + if self.on_main_thread() { + let callback = Closure::once_into_js(move || { + function(); + }); + self.browser_window + .queue_microtask(callback.unchecked_ref()); + } else { + self.main_thread_mailbox + .post(Priority::High, MainThreadItem::RealtimeFunction(function)); + } + } - fn now(&self) -> Instant { - Instant::now() - } + fn now(&self) -> Instant { + Instant::now() + } } fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) { - match item { - MainThreadItem::Runnable(runnable) => { - runnable.run(); - } - 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::RealtimeFunction(function) => { - function(); - } - } + match item { + MainThreadItem::Runnable(runnable) => { + runnable.run(); + } + 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::RealtimeFunction(function) => { + function(); + } + } } 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(); + 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(); - } - } + 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 index 5023e7de33..15724a7f48 100644 --- a/crates/gpui_web/src/display.rs +++ b/crates/gpui_web/src/display.rs @@ -3,9 +3,9 @@ use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px}; #[derive(Debug)] pub struct WebDisplay { - id: DisplayId, - uuid: uuid::Uuid, - browser_window: web_sys::Window, + id: DisplayId, + uuid: uuid::Uuid, + browser_window: web_sys::Window, } // Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`. @@ -13,86 +13,86 @@ 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, - } - } + 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.), - }; - }; + 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; + 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), - } - } + 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; + 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), - } - } + Size { + width: px(width), + height: px(height), + } + } } impl PlatformDisplay for WebDisplay { - fn id(&self) -> DisplayId { - self.id - } + fn id(&self) -> DisplayId { + self.id + } - fn uuid(&self) -> Result { - Ok(self.uuid) - } + fn uuid(&self) -> Result { + Ok(self.uuid) + } - fn bounds(&self) -> Bounds { - let size = self.screen_size(); - Bounds { - origin: Point::default(), - size, - } - } + 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 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 }, - } - } + 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 index 46be646cb5..f03773d1eb 100644 --- a/crates/gpui_web/src/events.rs +++ b/crates/gpui_web/src/events.rs @@ -1,10 +1,10 @@ use std::rc::Rc; use gpui::{ - Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, - Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, - MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, - ScrollWheelEvent, TouchPhase, point, px, + Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, + Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, + MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, + ScrollWheelEvent, TouchPhase, point, px, }; use smallvec::smallvec; use wasm_bindgen::prelude::*; @@ -12,671 +12,671 @@ use wasm_bindgen::prelude::*; use crate::window::WebWindowInner; pub struct WebEventListeners { - #[allow(dead_code)] - closures: Vec>, + #[allow(dead_code)] + closures: Vec>, } pub(crate) struct ClickState { - last_position: Point, - last_time: f64, - current_count: usize, + last_position: Point, + last_time: f64, + current_count: usize, } impl Default for ClickState { - fn default() -> Self { - Self { - last_position: Point::default(), - last_time: 0.0, - current_count: 0, - } - } + 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(); + 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; - } + 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 - } + self.last_position = position; + self.last_time = time; + self.current_count + } } impl WebWindowInner { - pub fn register_event_listeners(self: &Rc) -> WebEventListeners { - let mut closures = vec![ - self.register_pointer_down(), - self.register_pointer_up(), - self.register_pointer_move(), - self.register_pointer_leave(), - self.register_wheel(), - self.register_context_menu(), - self.register_dragover(), - self.register_drop(), - self.register_dragleave(), - self.register_key_down(), - self.register_key_up(), - self.register_composition_start(), - self.register_composition_update(), - self.register_composition_end(), - self.register_focus(), - self.register_blur(), - self.register_pointer_enter(), - self.register_pointer_leave_hover(), - ]; - closures.extend(self.register_visibility_change()); - closures.extend(self.register_appearance_change()); - - WebEventListeners { closures } - } - - fn listen( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - self.canvas - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - closure - } - - fn listen_input( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - self.input_element - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - closure - } - - /// Registers a listener with `{passive: false}` so that `preventDefault()` works. - /// Needed for events like `wheel` which are passive by default in modern browsers. - fn listen_non_passive( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - let canvas_js: &JsValue = self.canvas.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(canvas_js, &"addEventListener".into()) { - if let Ok(add_fn) = add_fn_val.dyn_into::() { - add_fn - .call3(canvas_js, &event_name.into(), callback_js, &options) - .ok(); - } - } - closure - } - - fn dispatch_input(&self, input: PlatformInput) -> Option { - let mut borrowed = self.callbacks.borrow_mut(); - borrowed.input.as_mut().map(|callback| callback(input)) - } - - fn register_pointer_down(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerdown", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - this.input_element.focus().ok(); - - let button = dom_mouse_button_to_gpui(event.button()); - let position = pointer_position_in_element(&event); - 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, - })); - }) - } - - fn register_pointer_up(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerup", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let button = dom_mouse_button_to_gpui(event.button()); - let position = pointer_position_in_element(&event); - 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, - })); - }) - } - - fn register_pointer_move(self: &Rc) -> Closure { - 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); - 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) -> Closure { - 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; - } - - this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { - position, - pressed_button: current_pressed, - modifiers, - })); - }) - } - - fn register_wheel(self: &Rc) -> Closure { - 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); - - let delta_mode = event.delta_mode(); - let delta = if delta_mode == 1 { - ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32)) - } else { - ScrollDelta::Pixels(point( - px(-event.delta_x() as f32), - px(-event.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) -> Closure { - self.listen("contextmenu", move |event: JsValue| { - let event: web_sys::Event = event.unchecked_into(); - event.prevent_default(); - }) - } - - fn register_dragover(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("dragover", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - } - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); - }) - } - - fn register_drop(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("drop", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - } - - let paths = extract_file_paths_from_drag(&event); - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered { - position, - paths: ExternalPaths(paths), - })); - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); - }) - } - - fn register_dragleave(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("dragleave", move |_event: JsValue| { - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited)); - }) - } - - fn register_key_down(self: &Rc) -> Closure { - 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); - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - current_state.capslock = capslock; - } - - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - event.prevent_default(); - - 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 { - return; - } - } - - if this.is_composing.get() || event.is_composing() { - return; - } - - if modifiers.is_subset_of(&Modifiers::shift()) { - if let Some(text) = key_char { - this.with_input_handler(|handler| { - handler.replace_text_in_range(None, &text); - }); - } - } - }) - } - - fn register_key_up(self: &Rc) -> Closure { - 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); - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - current_state.capslock = capslock; - } - - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - event.prevent_default(); - - let key_char = compute_key_char(&event, &key, &modifiers); - - let keystroke = Keystroke { - modifiers, - key, - key_char, - }; - - this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); - }) - } - - fn register_composition_start(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("compositionstart", move |_event: JsValue| { - this.is_composing.set(true); - }) - } - - fn register_composition_update(self: &Rc) -> Closure { - 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) -> Closure { - 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| { - handler.replace_text_in_range(None, &data); - handler.unmark_text(); - }); - this.input_element.set_value(""); - }) - } - - fn register_focus(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("focus", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_active = true; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(true); - } - }) - } - - fn register_blur(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("blur", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_active = false; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(false); - } - }) - } - - fn register_pointer_enter(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerenter", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_hovered = true; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.hover_status_change { - callback(true); - } - }) - } - - fn register_pointer_leave_hover(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerleave", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_hovered = false; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.hover_status_change { - callback(false); - } - }) - } + pub fn register_event_listeners(self: &Rc) -> WebEventListeners { + let mut closures = vec![ + self.register_pointer_down(), + self.register_pointer_up(), + self.register_pointer_move(), + self.register_pointer_leave(), + self.register_wheel(), + self.register_context_menu(), + self.register_dragover(), + self.register_drop(), + self.register_dragleave(), + self.register_key_down(), + self.register_key_up(), + self.register_composition_start(), + self.register_composition_update(), + self.register_composition_end(), + self.register_focus(), + self.register_blur(), + self.register_pointer_enter(), + self.register_pointer_leave_hover(), + ]; + closures.extend(self.register_visibility_change()); + closures.extend(self.register_appearance_change()); + + WebEventListeners { closures } + } + + fn listen( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + self.canvas + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + closure + } + + fn listen_input( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + self.input_element + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + closure + } + + /// Registers a listener with `{passive: false}` so that `preventDefault()` works. + /// Needed for events like `wheel` which are passive by default in modern browsers. + fn listen_non_passive( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + let canvas_js: &JsValue = self.canvas.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(canvas_js, &"addEventListener".into()) { + if let Ok(add_fn) = add_fn_val.dyn_into::() { + add_fn + .call3(canvas_js, &event_name.into(), callback_js, &options) + .ok(); + } + } + closure + } + + fn dispatch_input(&self, input: PlatformInput) -> Option { + let mut borrowed = self.callbacks.borrow_mut(); + borrowed.input.as_mut().map(|callback| callback(input)) + } + + fn register_pointer_down(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerdown", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + event.prevent_default(); + this.input_element.focus().ok(); + + let button = dom_mouse_button_to_gpui(event.button()); + let position = pointer_position_in_element(&event); + 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, + })); + }) + } + + fn register_pointer_up(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerup", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + event.prevent_default(); + + let button = dom_mouse_button_to_gpui(event.button()); + let position = pointer_position_in_element(&event); + 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, + })); + }) + } + + fn register_pointer_move(self: &Rc) -> Closure { + 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); + 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) -> Closure { + 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; + } + + this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { + position, + pressed_button: current_pressed, + modifiers, + })); + }) + } + + fn register_wheel(self: &Rc) -> Closure { + 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); + + let delta_mode = event.delta_mode(); + let delta = if delta_mode == 1 { + ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32)) + } else { + ScrollDelta::Pixels(point( + px(-event.delta_x() as f32), + px(-event.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) -> Closure { + self.listen("contextmenu", move |event: JsValue| { + let event: web_sys::Event = event.unchecked_into(); + event.prevent_default(); + }) + } + + fn register_dragover(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("dragover", move |event: JsValue| { + let event: web_sys::DragEvent = event.unchecked_into(); + event.prevent_default(); + + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + let position = mouse_position_in_element(mouse_event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + } + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + }) + } + + fn register_drop(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("drop", move |event: JsValue| { + let event: web_sys::DragEvent = event.unchecked_into(); + event.prevent_default(); + + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + let position = mouse_position_in_element(mouse_event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + } + + let paths = extract_file_paths_from_drag(&event); + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: ExternalPaths(paths), + })); + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); + }) + } + + fn register_dragleave(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("dragleave", move |_event: JsValue| { + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited)); + }) + } + + fn register_key_down(self: &Rc) -> Closure { + 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); + + { + let mut current_state = this.state.borrow_mut(); + current_state.modifiers = modifiers; + current_state.capslock = capslock; + } + + this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + })); + + let key = dom_key_to_gpui_key(&event); + + if is_modifier_only_key(&key) { + return; + } + + event.prevent_default(); + + 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 { + return; + } + } + + if this.is_composing.get() || event.is_composing() { + return; + } + + if modifiers.is_subset_of(&Modifiers::shift()) { + if let Some(text) = key_char { + this.with_input_handler(|handler| { + handler.replace_text_in_range(None, &text); + }); + } + } + }) + } + + fn register_key_up(self: &Rc) -> Closure { + 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); + + { + let mut current_state = this.state.borrow_mut(); + current_state.modifiers = modifiers; + current_state.capslock = capslock; + } + + this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + })); + + let key = dom_key_to_gpui_key(&event); + + if is_modifier_only_key(&key) { + return; + } + + event.prevent_default(); + + let key_char = compute_key_char(&event, &key, &modifiers); + + let keystroke = Keystroke { + modifiers, + key, + key_char, + }; + + this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); + }) + } + + fn register_composition_start(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionstart", move |_event: JsValue| { + this.is_composing.set(true); + }) + } + + fn register_composition_update(self: &Rc) -> Closure { + 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) -> Closure { + 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| { + handler.replace_text_in_range(None, &data); + handler.unmark_text(); + }); + this.input_element.set_value(""); + }) + } + + fn register_focus(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("focus", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_active = true; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(true); + } + }) + } + + fn register_blur(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("blur", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_active = false; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(false); + } + }) + } + + fn register_pointer_enter(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerenter", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_hovered = true; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.hover_status_change { + callback(true); + } + }) + } + + fn register_pointer_leave_hover(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerleave", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_hovered = false; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.hover_status_change { + callback(false); + } + }) + } } 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() - } - } + 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, - } + 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, - } + 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, - } + 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, - } + 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"), - } + Capslock { + on: event.get_modifier_state("CapsLock"), + } } pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { - let navigator = browser_window.navigator(); + 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; - } - } + #[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"); - } + if let Ok(user_agent) = navigator.user_agent() { + return user_agent.contains("Mac"); + } - false + false } fn is_modifier_only_key(key: &str) -> bool { - matches!( - key, - "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" - ) + matches!( + key, + "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" + ) } fn compute_key_char( - event: &web_sys::KeyboardEvent, - gpui_key: &str, - modifiers: &Modifiers, + event: &web_sys::KeyboardEvent, + gpui_key: &str, + modifiers: &Modifiers, ) -> Option { - if modifiers.platform || modifiers.control { - return None; - } + if modifiers.platform || modifiers.control { + return None; + } - if is_modifier_only_key(gpui_key) { - return None; - } + if is_modifier_only_key(gpui_key) { + return None; + } - if gpui_key == "space" { - return Some(" ".to_string()); - } + if gpui_key == "space" { + return Some(" ".to_string()); + } - let raw_key = event.key(); + let raw_key = event.key(); - if raw_key.len() == 1 { - return Some(raw_key); - } + if raw_key.len() == 1 { + return Some(raw_key); + } - None + 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) + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + mouse_position_in_element(mouse_event) } 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)) + // 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)) } fn extract_file_paths_from_drag( - event: &web_sys::DragEvent, + event: &web_sys::DragEvent, ) -> smallvec::SmallVec<[std::path::PathBuf; 2]> { - let mut paths = smallvec![]; - let Some(data_transfer) = event.data_transfer() else { - return paths; - }; - let file_list = data_transfer.files(); - let Some(files) = file_list else { - return paths; - }; - for index in 0..files.length() { - if let Some(file) = files.get(index) { - paths.push(std::path::PathBuf::from(file.name())); - } - } - paths + let mut paths = smallvec![]; + let Some(data_transfer) = event.data_transfer() else { + return paths; + }; + let file_list = data_transfer.files(); + let Some(files) = file_list else { + return paths; + }; + for index in 0..files.length() { + if let Some(file) = files.get(index) { + paths.push(std::path::PathBuf::from(file.name())); + } + } + paths } diff --git a/crates/gpui_web/src/http_client.rs b/crates/gpui_web/src/http_client.rs index 331ff9561d..267af3403e 100644 --- a/crates/gpui_web/src/http_client.rs +++ b/crates/gpui_web/src/http_client.rs @@ -8,30 +8,30 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { - #[wasm_bindgen(catch, js_name = "fetch")] - fn global_fetch(input: &web_sys::Request) -> Result; + #[wasm_bindgen(catch, js_name = "fetch")] + fn global_fetch(input: &web_sys::Request) -> Result; } pub struct FetchHttpClient; impl Default for FetchHttpClient { - fn default() -> Self { - Self - } + fn default() -> Self { + Self + } } #[cfg(feature = "multithreaded")] impl FetchHttpClient { - pub unsafe fn new() -> Self { - Self - } + pub unsafe fn new() -> Self { + Self + } } #[cfg(not(feature = "multithreaded"))] impl FetchHttpClient { - pub fn new() -> Self { - Self - } + pub fn new() -> Self { + Self + } } /// Wraps a `!Send` future to satisfy the `Send` bound on `BoxFuture`. @@ -40,60 +40,60 @@ struct AssertSend(F); unsafe impl Send for AssertSend {} impl Future for AssertSend { - type Output = F::Output; + type Output = F::Output; - fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let inner = unsafe { self.map_unchecked_mut(|this| &mut this.0) }; - inner.poll(cx) - } + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let inner = unsafe { self.map_unchecked_mut(|this| &mut this.0) }; + inner.poll(cx) + } } impl HttpClient for FetchHttpClient { - fn get( - &self, - url: &str, - follow_redirects: bool, - ) -> futures::future::BoxFuture<'static, anyhow::Result> { - let url = url.to_string(); - Box::pin(AssertSend(async move { - let init = web_sys::RequestInit::new(); - init.set_method("GET"); + fn get( + &self, + url: &str, + follow_redirects: bool, + ) -> futures::future::BoxFuture<'static, anyhow::Result> { + let url = url.to_string(); + Box::pin(AssertSend(async move { + let init = web_sys::RequestInit::new(); + init.set_method("GET"); - if !follow_redirects { - init.set_redirect(web_sys::RequestRedirect::Manual); - } + if !follow_redirects { + init.set_redirect(web_sys::RequestRedirect::Manual); + } - let request = web_sys::Request::new_with_str_and_init(&url, &init) - .map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?; + let request = web_sys::Request::new_with_str_and_init(&url, &init) + .map_err(|error| anyhow!("failed to create fetch Request: {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 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 web_response: web_sys::Response = response_value + .dyn_into() + .map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?; - let status_code = http::StatusCode::from_u16(web_response.status()) - .map_err(|_| anyhow!("invalid status code"))?; + let status_code = http::StatusCode::from_u16(web_response.status()) + .map_err(|_| anyhow!("invalid status code"))?; - let body_promise = web_response - .array_buffer() - .map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?; - let body_value = wasm_bindgen_futures::JsFuture::from(body_promise) - .await - .map_err(|error| anyhow!("failed to read response body: {error:?}"))?; - let array_buffer: js_sys::ArrayBuffer = body_value - .dyn_into() - .map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?; - let body = js_sys::Uint8Array::new(&array_buffer).to_vec(); + let body_promise = web_response + .array_buffer() + .map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?; + let body_value = wasm_bindgen_futures::JsFuture::from(body_promise) + .await + .map_err(|error| anyhow!("failed to read response body: {error:?}"))?; + let array_buffer: js_sys::ArrayBuffer = body_value + .dyn_into() + .map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?; + let body = js_sys::Uint8Array::new(&array_buffer).to_vec(); - Ok(HttpResponse { - status: status_code, - body, - }) - })) - } + Ok(HttpResponse { + status: status_code, + body, + }) + })) + } } diff --git a/crates/gpui_web/src/keyboard.rs b/crates/gpui_web/src/keyboard.rs index 0ab4f7aa4a..367baf77e0 100644 --- a/crates/gpui_web/src/keyboard.rs +++ b/crates/gpui_web/src/keyboard.rs @@ -3,17 +3,17 @@ use gpui::PlatformKeyboardLayout; pub struct WebKeyboardLayout; impl WebKeyboardLayout { - pub fn new() -> Self { - WebKeyboardLayout - } + pub fn new() -> Self { + WebKeyboardLayout + } } impl PlatformKeyboardLayout for WebKeyboardLayout { - fn id(&self) -> &str { - "us" - } + fn id(&self) -> &str { + "us" + } - fn name(&self) -> &str { - "US" - } + fn name(&self) -> &str { + "US" + } } diff --git a/crates/gpui_web/src/logging.rs b/crates/gpui_web/src/logging.rs index 773118eeb2..ebc394434c 100644 --- a/crates/gpui_web/src/logging.rs +++ b/crates/gpui_web/src/logging.rs @@ -3,35 +3,35 @@ use log::{Level, Log, Metadata, Record}; struct ConsoleLogger; impl Log for ConsoleLogger { - fn enabled(&self, _metadata: &Metadata) -> bool { - true - } + fn enabled(&self, _metadata: &Metadata) -> bool { + true + } - fn log(&self, record: &Record) { - if !self.enabled(record.metadata()) { - return; - } + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } - let message = format!( - "[{}] {}: {}", - record.level(), - record.target(), - record.args() - ); - let js_string = wasm_bindgen::JsValue::from_str(&message); + let message = format!( + "[{}] {}: {}", + record.level(), + record.target(), + record.args() + ); + let js_string = wasm_bindgen::JsValue::from_str(&message); - match record.level() { - Level::Error => web_sys::console::error_1(&js_string), - Level::Warn => web_sys::console::warn_1(&js_string), - Level::Info => web_sys::console::info_1(&js_string), - Level::Debug | Level::Trace => web_sys::console::log_1(&js_string), - } - } + match record.level() { + Level::Error => web_sys::console::error_1(&js_string), + Level::Warn => web_sys::console::warn_1(&js_string), + Level::Info => web_sys::console::info_1(&js_string), + Level::Debug | Level::Trace => web_sys::console::log_1(&js_string), + } + } - fn flush(&self) {} + fn flush(&self) {} } pub fn init_logging() { - log::set_logger(&ConsoleLogger).ok(); - log::set_max_level(log::LevelFilter::Info); + log::set_logger(&ConsoleLogger).ok(); + log::set_max_level(log::LevelFilter::Info); } diff --git a/crates/gpui_web/src/platform.rs b/crates/gpui_web/src/platform.rs index ace27bef48..2288dfb20a 100644 --- a/crates/gpui_web/src/platform.rs +++ b/crates/gpui_web/src/platform.rs @@ -5,431 +5,431 @@ use crate::window::WebWindow; use anyhow::Result; use futures::channel::oneshot; use gpui::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper, - ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, - ThermalState, WindowAppearance, WindowParams, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper, + ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, + ThermalState, WindowAppearance, WindowParams, }; use gpui_wgpu::WgpuContext; use std::{ - borrow::Cow, - cell::{Cell, RefCell}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, + borrow::Cow, + cell::{Cell, RefCell}, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, }; use wasm_bindgen::prelude::*; static BUNDLED_FONTS: &[&[u8]] = &[ - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"), ]; pub struct WebPlatform { - browser_window: web_sys::Window, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - active_window: RefCell>, - active_display: Rc, - callbacks: RefCell, - wgpu_context: Rc>>, - cursor_visible: Rc>, - last_cursor_css: Rc>, - _cursor_restore_listeners: Vec, + browser_window: web_sys::Window, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + active_window: RefCell>, + active_display: Rc, + callbacks: RefCell, + wgpu_context: Rc>>, + cursor_visible: Rc>, + last_cursor_css: Rc>, + _cursor_restore_listeners: Vec, } #[derive(Default)] struct WebPlatformCallbacks { - open_urls: Option)>>, - quit: Option>, - reopen: Option>, - app_menu_action: Option>, - will_open_app_menu: Option>, - validate_app_menu_command: Option bool>>, - keyboard_layout_change: Option>, - thermal_state_change: Option>, + open_urls: Option)>>, + quit: Option>, + reopen: Option>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, + keyboard_layout_change: Option>, + thermal_state_change: Option>, } impl WebPlatform { - pub fn new(allow_multi_threading: bool) -> Self { - let browser_window = - web_sys::window().expect("must be running in a browser window context"); - let dispatcher = Arc::new(WebDispatcher::new( - browser_window.clone(), - allow_multi_threading, - )); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts( - "IBM Plex Sans", - )); - let fonts = BUNDLED_FONTS - .iter() - .map(|bytes| Cow::Borrowed(*bytes)) - .collect(); - if let Err(error) = text_system.add_fonts(fonts) { - log::error!("failed to load bundled fonts: {error:#}"); - } - let text_system: Arc = text_system; - let active_display: Rc = - Rc::new(WebDisplay::new(browser_window.clone())); + pub fn new(allow_multi_threading: bool) -> Self { + let browser_window = + web_sys::window().expect("must be running in a browser window context"); + let dispatcher = Arc::new(WebDispatcher::new( + browser_window.clone(), + allow_multi_threading, + )); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts( + "IBM Plex Sans", + )); + let fonts = BUNDLED_FONTS + .iter() + .map(|bytes| Cow::Borrowed(*bytes)) + .collect(); + if let Err(error) = text_system.add_fonts(fonts) { + log::error!("failed to load bundled fonts: {error:#}"); + } + let text_system: Arc = text_system; + let active_display: Rc = + Rc::new(WebDisplay::new(browser_window.clone())); - let cursor_visible = Rc::new(Cell::new(true)); - let last_cursor_css = Rc::new(Cell::new("default")); - let cursor_restore_listeners = cursor_restore_listeners( - &browser_window, - cursor_visible.clone(), - last_cursor_css.clone(), - ); + let cursor_visible = Rc::new(Cell::new(true)); + let last_cursor_css = Rc::new(Cell::new("default")); + let cursor_restore_listeners = cursor_restore_listeners( + &browser_window, + cursor_visible.clone(), + last_cursor_css.clone(), + ); - Self { - browser_window, - background_executor, - foreground_executor, - text_system, - active_window: RefCell::new(None), - active_display, - callbacks: RefCell::new(WebPlatformCallbacks::default()), - wgpu_context: Rc::new(RefCell::new(None)), - cursor_visible, - last_cursor_css, - _cursor_restore_listeners: cursor_restore_listeners, - } - } + Self { + browser_window, + background_executor, + foreground_executor, + text_system, + active_window: RefCell::new(None), + active_display, + callbacks: RefCell::new(WebPlatformCallbacks::default()), + wgpu_context: Rc::new(RefCell::new(None)), + cursor_visible, + last_cursor_css, + _cursor_restore_listeners: cursor_restore_listeners, + } + } } impl Platform for WebPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } - fn text_system(&self) -> Arc { - self.text_system.clone() - } + fn text_system(&self) -> Arc { + self.text_system.clone() + } - fn run(&self, on_finish_launching: Box) { - let wgpu_context = self.wgpu_context.clone(); - wasm_bindgen_futures::spawn_local(async move { - match WgpuContext::new_web().await { - Ok(context) => { - log::info!("WebGPU context initialized successfully"); - *wgpu_context.borrow_mut() = Some(context); - on_finish_launching(); - } - Err(err) => { - log::error!("Failed to initialize WebGPU context: {err:#}"); - on_finish_launching(); - } - } - }); - } + fn run(&self, on_finish_launching: Box) { + let wgpu_context = self.wgpu_context.clone(); + wasm_bindgen_futures::spawn_local(async move { + match WgpuContext::new_web().await { + Ok(context) => { + log::info!("WebGPU context initialized successfully"); + *wgpu_context.borrow_mut() = Some(context); + on_finish_launching(); + } + Err(err) => { + log::error!("Failed to initialize WebGPU context: {err:#}"); + on_finish_launching(); + } + } + }); + } - fn quit(&self) { - log::warn!("WebPlatform::quit called, but quitting is not supported in the browser ."); - } + fn quit(&self) { + log::warn!("WebPlatform::quit called, but quitting is not supported in the browser ."); + } - fn restart(&self, _binary_path: Option) {} + fn restart(&self, _binary_path: Option) {} - fn activate(&self, _ignoring_other_apps: bool) {} + fn activate(&self, _ignoring_other_apps: bool) {} - fn hide(&self) {} + fn hide(&self) {} - fn hide_other_apps(&self) {} + fn hide_other_apps(&self) {} - fn unhide_other_apps(&self) {} + fn unhide_other_apps(&self) {} - fn displays(&self) -> Vec> { - vec![self.active_display.clone()] - } + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } - fn primary_display(&self) -> Option> { - Some(self.active_display.clone()) - } + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } - fn active_window(&self) -> Option { - *self.active_window.borrow() - } + fn active_window(&self) -> Option { + *self.active_window.borrow() + } - fn open_window( - &self, - handle: AnyWindowHandle, - params: WindowParams, - ) -> anyhow::Result> { - let context_ref = self.wgpu_context.borrow(); - let context = context_ref.as_ref().ok_or_else(|| { - anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") - })?; + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let context_ref = self.wgpu_context.borrow(); + let context = context_ref.as_ref().ok_or_else(|| { + anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") + })?; - let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; - *self.active_window.borrow_mut() = Some(handle); - Ok(Box::new(window)) - } + let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; + *self.active_window.borrow_mut() = Some(handle); + Ok(Box::new(window)) + } - fn window_appearance(&self) -> WindowAppearance { - let Ok(Some(media_query)) = self - .browser_window - .match_media("(prefers-color-scheme: dark)") - else { - return WindowAppearance::Light; - }; - if media_query.matches() { - WindowAppearance::Dark - } else { - WindowAppearance::Light - } - } + fn window_appearance(&self) -> WindowAppearance { + let Ok(Some(media_query)) = self + .browser_window + .match_media("(prefers-color-scheme: dark)") + else { + return WindowAppearance::Light; + }; + if media_query.matches() { + WindowAppearance::Dark + } else { + WindowAppearance::Light + } + } - fn open_url(&self, url: &str) { - if let Err(error) = self.browser_window.open_with_url(url) { - log::warn!("Failed to open URL '{url}': {error:?}"); - } - } + fn open_url(&self, url: &str) { + if let Err(error) = self.browser_window.open_with_url(url) { + log::warn!("Failed to open URL '{url}': {error:?}"); + } + } - fn on_open_urls(&self, callback: Box)>) { - self.callbacks.borrow_mut().open_urls = Some(callback); - } + fn on_open_urls(&self, callback: Box)>) { + self.callbacks.borrow_mut().open_urls = Some(callback); + } - fn register_url_scheme(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } + 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(Err(anyhow::anyhow!( - "prompt_for_paths is not supported on the web" - ))) - .ok(); - rx - } + fn prompt_for_paths( + &self, + _options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (tx, rx) = oneshot::channel(); + tx.send(Err(anyhow::anyhow!( + "prompt_for_paths is not supported on the web" + ))) + .ok(); + rx + } - fn prompt_for_new_path( - &self, - _directory: &Path, - _suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let (sender, receiver) = oneshot::channel(); - sender - .send(Err(anyhow::anyhow!( - "prompt_for_new_path is not supported on the web" - ))) - .ok(); - receiver - } + fn prompt_for_new_path( + &self, + _directory: &Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (sender, receiver) = oneshot::channel(); + sender + .send(Err(anyhow::anyhow!( + "prompt_for_new_path is not supported on the web" + ))) + .ok(); + receiver + } - fn can_select_mixed_files_and_dirs(&self) -> bool { - false - } + fn can_select_mixed_files_and_dirs(&self) -> bool { + false + } - fn reveal_path(&self, _path: &Path) {} + fn reveal_path(&self, _path: &Path) {} - fn open_with_system(&self, _path: &Path) {} + fn open_with_system(&self, _path: &Path) {} - fn on_quit(&self, callback: Box) { - self.callbacks.borrow_mut().quit = Some(callback); - } + fn on_quit(&self, callback: Box) { + self.callbacks.borrow_mut().quit = Some(callback); + } - fn on_reopen(&self, callback: Box) { - self.callbacks.borrow_mut().reopen = Some(callback); - } + fn on_reopen(&self, callback: Box) { + self.callbacks.borrow_mut().reopen = Some(callback); + } - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} - fn on_app_menu_action(&self, callback: Box) { - self.callbacks.borrow_mut().app_menu_action = Some(callback); - } + fn on_app_menu_action(&self, callback: Box) { + self.callbacks.borrow_mut().app_menu_action = Some(callback); + } - fn on_will_open_app_menu(&self, callback: Box) { - self.callbacks.borrow_mut().will_open_app_menu = Some(callback); - } + fn on_will_open_app_menu(&self, callback: Box) { + self.callbacks.borrow_mut().will_open_app_menu = Some(callback); + } - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.callbacks.borrow_mut().validate_app_menu_command = Some(callback); - } + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.callbacks.borrow_mut().validate_app_menu_command = Some(callback); + } - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } - fn on_thermal_state_change(&self, callback: Box) { - self.callbacks.borrow_mut().thermal_state_change = Some(callback); - } + fn on_thermal_state_change(&self, callback: Box) { + self.callbacks.borrow_mut().thermal_state_change = Some(callback); + } - fn compositor_name(&self) -> &'static str { - "Web" - } + fn compositor_name(&self) -> &'static str { + "Web" + } - fn app_path(&self) -> Result { - Err(anyhow::anyhow!("app_path is not available on the web")) - } + fn app_path(&self) -> Result { + Err(anyhow::anyhow!("app_path is not available on the web")) + } - fn path_for_auxiliary_executable(&self, _name: &str) -> Result { - Err(anyhow::anyhow!( - "path_for_auxiliary_executable is not available on the web" - )) - } + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + Err(anyhow::anyhow!( + "path_for_auxiliary_executable is not available on the web" + )) + } - fn set_cursor_style(&self, style: CursorStyle) { - let css_cursor = match style { - CursorStyle::Arrow => "default", - CursorStyle::IBeam => "text", - CursorStyle::Crosshair => "crosshair", - CursorStyle::ClosedHand => "grabbing", - CursorStyle::OpenHand => "grab", - CursorStyle::PointingHand => "pointer", - CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => { - "ew-resize" - } - CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => { - "ns-resize" - } - CursorStyle::ResizeUpLeftDownRight => "nesw-resize", - CursorStyle::ResizeUpRightDownLeft => "nwse-resize", - CursorStyle::ResizeColumn => "col-resize", - CursorStyle::ResizeRow => "row-resize", - CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", - CursorStyle::OperationNotAllowed => "not-allowed", - CursorStyle::DragLink => "alias", - CursorStyle::DragCopy => "copy", - CursorStyle::ContextualMenu => "context-menu", - }; + fn set_cursor_style(&self, style: CursorStyle) { + let css_cursor = match style { + CursorStyle::Arrow => "default", + CursorStyle::IBeam => "text", + CursorStyle::Crosshair => "crosshair", + CursorStyle::ClosedHand => "grabbing", + CursorStyle::OpenHand => "grab", + CursorStyle::PointingHand => "pointer", + CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => { + "ew-resize" + } + CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => { + "ns-resize" + } + CursorStyle::ResizeUpLeftDownRight => "nesw-resize", + CursorStyle::ResizeUpRightDownLeft => "nwse-resize", + CursorStyle::ResizeColumn => "col-resize", + CursorStyle::ResizeRow => "row-resize", + CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", + CursorStyle::OperationNotAllowed => "not-allowed", + CursorStyle::DragLink => "alias", + CursorStyle::DragCopy => "copy", + CursorStyle::ContextualMenu => "context-menu", + }; - self.last_cursor_css.set(css_cursor); - if self.cursor_visible.get() { - set_body_cursor(&self.browser_window, css_cursor); - } - } + self.last_cursor_css.set(css_cursor); + if self.cursor_visible.get() { + set_body_cursor(&self.browser_window, css_cursor); + } + } - fn hide_cursor_until_mouse_moves(&self) { - if !self.cursor_visible.replace(false) { - return; - } - set_body_cursor(&self.browser_window, "none"); - } + fn hide_cursor_until_mouse_moves(&self) { + if !self.cursor_visible.replace(false) { + return; + } + set_body_cursor(&self.browser_window, "none"); + } - fn is_cursor_visible(&self) -> bool { - self.cursor_visible.get() - } + fn is_cursor_visible(&self) -> bool { + self.cursor_visible.get() + } - fn should_auto_hide_scrollbars(&self) -> bool { - true - } + fn should_auto_hide_scrollbars(&self) -> bool { + true + } - fn read_from_clipboard(&self) -> Option { - None - } + fn read_from_clipboard(&self) -> Option { + None + } - fn write_to_clipboard(&self, _item: ClipboardItem) {} + fn write_to_clipboard(&self, _item: ClipboardItem) {} - fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "credential storage is not available on the web" - ))) - } + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "credential storage is not available on the web" + ))) + } - fn read_credentials(&self, _url: &str) -> Task)>>> { - Task::ready(Ok(None)) - } + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "credential storage is not available on the web" - ))) - } + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "credential storage is not available on the web" + ))) + } - fn keyboard_layout(&self) -> Box { - Box::new(WebKeyboardLayout) - } + fn keyboard_layout(&self) -> Box { + Box::new(WebKeyboardLayout) + } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } - fn on_keyboard_layout_change(&self, callback: Box) { - self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); - } + fn on_keyboard_layout_change(&self, callback: Box) { + self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); + } } struct EventListenerHandle { - target: web_sys::EventTarget, - event_name: &'static str, - closure: Closure, + target: web_sys::EventTarget, + event_name: &'static str, + closure: 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(); - } + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_name, + self.closure.as_ref().unchecked_ref(), + ) + .ok(); + } } fn cursor_restore_listeners( - browser_window: &web_sys::Window, - cursor_visible: Rc>, - last_cursor_css: Rc>, + browser_window: &web_sys::Window, + cursor_visible: Rc>, + last_cursor_css: Rc>, ) -> Vec { - let mut handles = Vec::new(); - let Some(document) = browser_window.document() else { - return handles; - }; + let mut handles = Vec::new(); + let Some(document) = browser_window.document() else { + return handles; + }; - let make_restore_handler = |browser_window: web_sys::Window| { - let cursor_visible = cursor_visible.clone(); - let last_cursor_css = last_cursor_css.clone(); - Closure::::new(move |_event: JsValue| { - if !cursor_visible.replace(true) { - set_body_cursor(&browser_window, last_cursor_css.get()); - } - }) - }; + let make_restore_handler = |browser_window: web_sys::Window| { + let cursor_visible = cursor_visible.clone(); + let last_cursor_css = last_cursor_css.clone(); + Closure::::new(move |_event: JsValue| { + if !cursor_visible.replace(true) { + set_body_cursor(&browser_window, last_cursor_css.get()); + } + }) + }; - let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { - let closure = make_restore_handler(browser_window.clone()); - target - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - handles.push(EventListenerHandle { - target: target.clone(), - event_name, - closure, - }); - }; + let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { + let closure = make_restore_handler(browser_window.clone()); + target + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + handles.push(EventListenerHandle { + target: target.clone(), + event_name, + closure, + }); + }; - let document_target: &web_sys::EventTarget = document.as_ref(); - let window_target: &web_sys::EventTarget = browser_window.as_ref(); + let document_target: &web_sys::EventTarget = document.as_ref(); + let window_target: &web_sys::EventTarget = browser_window.as_ref(); - add_listener(document_target, "mousemove"); - add_listener(document_target, "mouseenter"); - add_listener(window_target, "blur"); - add_listener(document_target, "visibilitychange"); + add_listener(document_target, "mousemove"); + add_listener(document_target, "mouseenter"); + add_listener(window_target, "blur"); + add_listener(document_target, "visibilitychange"); - handles + handles } fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) { - if let Some(document) = browser_window.document() - && let Some(body) = document.body() - && let Err(error) = body.style().set_property("cursor", css_cursor) - { - log::warn!("Failed to set cursor style: {error:?}"); - } + if let Some(document) = browser_window.document() + && let Some(body) = document.body() + && let Err(error) = body.style().set_property("cursor", css_cursor) + { + log::warn!("Failed to set cursor style: {error:?}"); + } } diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index b9399d32e6..a4e315ed81 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -4,728 +4,728 @@ use std::sync::Arc; use std::{cell::Cell, cell::RefCell, rc::Rc}; use gpui::{ - AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs, - Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, + AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs, + Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, }; use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig}; use wasm_bindgen::prelude::*; #[derive(Default)] pub(crate) struct WebWindowCallbacks { - pub(crate) request_frame: Option>, - pub(crate) input: Option DispatchEventResult>>, - pub(crate) active_status_change: Option>, - pub(crate) hover_status_change: Option>, - pub(crate) resize: Option, f32)>>, - pub(crate) moved: Option>, - pub(crate) should_close: Option bool>>, - pub(crate) close: Option>, - pub(crate) appearance_changed: Option>, - pub(crate) hit_test_window_control: Option Option>>, + pub(crate) request_frame: Option>, + pub(crate) input: Option DispatchEventResult>>, + pub(crate) active_status_change: Option>, + pub(crate) hover_status_change: Option>, + pub(crate) resize: Option, f32)>>, + pub(crate) moved: Option>, + pub(crate) should_close: Option bool>>, + pub(crate) close: Option>, + pub(crate) appearance_changed: Option>, + pub(crate) hit_test_window_control: Option Option>>, } pub(crate) struct WebWindowMutableState { - pub(crate) renderer: WgpuRenderer, - pub(crate) bounds: Bounds, - pub(crate) scale_factor: f32, - pub(crate) max_texture_dimension: u32, - pub(crate) title: String, - pub(crate) input_handler: Option, - pub(crate) is_fullscreen: bool, - pub(crate) is_active: bool, - pub(crate) is_hovered: bool, - pub(crate) mouse_position: Point, - pub(crate) modifiers: Modifiers, - pub(crate) capslock: Capslock, + pub(crate) renderer: WgpuRenderer, + pub(crate) bounds: Bounds, + pub(crate) scale_factor: f32, + pub(crate) max_texture_dimension: u32, + pub(crate) title: String, + pub(crate) input_handler: Option, + pub(crate) is_fullscreen: bool, + pub(crate) is_active: bool, + pub(crate) is_hovered: bool, + pub(crate) mouse_position: Point, + pub(crate) modifiers: Modifiers, + pub(crate) capslock: Capslock, } pub(crate) struct WebWindowInner { - pub(crate) browser_window: web_sys::Window, - pub(crate) canvas: web_sys::HtmlCanvasElement, - pub(crate) input_element: web_sys::HtmlInputElement, - pub(crate) has_device_pixel_support: bool, - pub(crate) is_mac: bool, - pub(crate) state: RefCell, - pub(crate) callbacks: RefCell, - pub(crate) click_state: RefCell, - pub(crate) pressed_button: Cell>, - pub(crate) last_physical_size: Cell<(u32, u32)>, - pub(crate) notify_scale: Cell, - pub(crate) is_composing: Cell, - mql_handle: RefCell>, - pending_physical_size: Cell>, + pub(crate) browser_window: web_sys::Window, + pub(crate) canvas: web_sys::HtmlCanvasElement, + pub(crate) input_element: web_sys::HtmlInputElement, + pub(crate) has_device_pixel_support: bool, + pub(crate) is_mac: bool, + pub(crate) state: RefCell, + pub(crate) callbacks: RefCell, + pub(crate) click_state: RefCell, + pub(crate) pressed_button: Cell>, + pub(crate) last_physical_size: Cell<(u32, u32)>, + pub(crate) notify_scale: Cell, + pub(crate) is_composing: Cell, + mql_handle: RefCell>, + pending_physical_size: Cell>, } pub struct WebWindow { - inner: Rc, - display: Rc, - #[allow(dead_code)] - handle: AnyWindowHandle, - _raf_closure: Closure, - _resize_observer: Option, - _resize_observer_closure: Closure, - _event_listeners: WebEventListeners, + inner: Rc, + display: Rc, + #[allow(dead_code)] + handle: AnyWindowHandle, + _raf_closure: Closure, + _resize_observer: Option, + _resize_observer_closure: Closure, + _event_listeners: WebEventListeners, } impl WebWindow { - pub fn new( - handle: AnyWindowHandle, - _params: WindowParams, - context: &WgpuContext, - browser_window: web_sys::Window, - ) -> anyhow::Result { - let document = browser_window - .document() - .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; + pub fn new( + handle: AnyWindowHandle, + _params: WindowParams, + context: &WgpuContext, + browser_window: web_sys::Window, + ) -> anyhow::Result { + let document = browser_window + .document() + .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; - let canvas: web_sys::HtmlCanvasElement = document - .create_element("canvas") - .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))? - .dyn_into() - .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?; + let canvas: web_sys::HtmlCanvasElement = document + .create_element("canvas") + .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))? + .dyn_into() + .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?; - let dpr = browser_window.device_pixel_ratio() as f32; - let max_texture_dimension = context.device.limits().max_texture_dimension_2d; - let has_device_pixel_support = check_device_pixel_support(); + let dpr = browser_window.device_pixel_ratio() as f32; + let max_texture_dimension = context.device.limits().max_texture_dimension_2d; + let has_device_pixel_support = check_device_pixel_support(); - canvas.set_tab_index(-1); + canvas.set_tab_index(-1); - let style = canvas.style(); - style - .set_property("width", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?; - style - .set_property("height", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?; - style - .set_property("display", "block") - .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?; - style - .set_property("outline", "none") - .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?; - style - .set_property("touch-action", "none") - .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?; + let style = canvas.style(); + style + .set_property("width", "100%") + .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?; + style + .set_property("height", "100%") + .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?; + style + .set_property("display", "block") + .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?; + style + .set_property("outline", "none") + .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?; + style + .set_property("touch-action", "none") + .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?; - let body = document - .body() - .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; - body.append_child(&canvas) - .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; + let body = document + .body() + .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; + body.append_child(&canvas) + .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; - let input_element: web_sys::HtmlInputElement = document - .create_element("input") - .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))? - .dyn_into() - .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?; - let input_style = input_element.style(); - input_style.set_property("position", "fixed").ok(); - input_style.set_property("top", "0").ok(); - input_style.set_property("left", "0").ok(); - input_style.set_property("width", "1px").ok(); - input_style.set_property("height", "1px").ok(); - input_style.set_property("opacity", "0").ok(); - body.append_child(&input_element) - .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; - input_element.focus().ok(); + let input_element: web_sys::HtmlInputElement = document + .create_element("input") + .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))? + .dyn_into() + .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?; + let input_style = input_element.style(); + input_style.set_property("position", "fixed").ok(); + input_style.set_property("top", "0").ok(); + input_style.set_property("left", "0").ok(); + input_style.set_property("width", "1px").ok(); + input_style.set_property("height", "1px").ok(); + input_style.set_property("opacity", "0").ok(); + body.append_child(&input_element) + .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; + input_element.focus().ok(); - let device_size = Size { - width: DevicePixels(0), - height: DevicePixels(0), - }; + let device_size = Size { + width: DevicePixels(0), + height: DevicePixels(0), + }; - let renderer_config = WgpuSurfaceConfig { - size: device_size, - transparent: false, - preferred_present_mode: None, - }; + let renderer_config = WgpuSurfaceConfig { + size: device_size, + transparent: false, + preferred_present_mode: None, + }; - let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?; + let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?; - let display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); + let display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); - let initial_bounds = Bounds { - origin: Point::default(), - size: Size::default(), - }; + let initial_bounds = Bounds { + origin: Point::default(), + size: Size::default(), + }; - let mutable_state = WebWindowMutableState { - renderer, - bounds: initial_bounds, - scale_factor: dpr, - max_texture_dimension, - title: String::new(), - input_handler: None, - is_fullscreen: false, - is_active: true, - is_hovered: false, - mouse_position: Point::default(), - modifiers: Modifiers::default(), - capslock: Capslock::default(), - }; + let mutable_state = WebWindowMutableState { + renderer, + bounds: initial_bounds, + scale_factor: dpr, + max_texture_dimension, + title: String::new(), + input_handler: None, + is_fullscreen: false, + is_active: true, + is_hovered: false, + mouse_position: Point::default(), + modifiers: Modifiers::default(), + capslock: Capslock::default(), + }; - let is_mac = is_mac_platform(&browser_window); + let is_mac = is_mac_platform(&browser_window); - let inner = Rc::new(WebWindowInner { - browser_window, - canvas, - input_element, - has_device_pixel_support, - is_mac, - state: RefCell::new(mutable_state), - callbacks: RefCell::new(WebWindowCallbacks::default()), - click_state: RefCell::new(ClickState::default()), - pressed_button: Cell::new(None), - last_physical_size: Cell::new((0, 0)), - notify_scale: Cell::new(false), - is_composing: Cell::new(false), - mql_handle: RefCell::new(None), - pending_physical_size: Cell::new(None), - }); + let inner = Rc::new(WebWindowInner { + browser_window, + canvas, + input_element, + has_device_pixel_support, + is_mac, + state: RefCell::new(mutable_state), + callbacks: RefCell::new(WebWindowCallbacks::default()), + click_state: RefCell::new(ClickState::default()), + pressed_button: Cell::new(None), + last_physical_size: Cell::new((0, 0)), + notify_scale: Cell::new(false), + is_composing: Cell::new(false), + mql_handle: RefCell::new(None), + pending_physical_size: Cell::new(None), + }); - let raf_closure = inner.create_raf_closure(); - inner.schedule_raf(&raf_closure); + let raf_closure = inner.create_raf_closure(); + inner.schedule_raf(&raf_closure); - let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner)); - let resize_observer = - web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok(); + let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner)); + let resize_observer = + web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok(); - if let Some(ref observer) = resize_observer { - inner.observe_canvas(observer); - inner.watch_dpr_changes(observer); - } + if let Some(ref observer) = resize_observer { + inner.observe_canvas(observer); + inner.watch_dpr_changes(observer); + } - let event_listeners = inner.register_event_listeners(); + let event_listeners = inner.register_event_listeners(); - Ok(Self { - inner, - display, - handle, - _raf_closure: raf_closure, - _resize_observer: resize_observer, - _resize_observer_closure: resize_observer_closure, - _event_listeners: event_listeners, - }) - } + Ok(Self { + inner, + display, + handle, + _raf_closure: raf_closure, + _resize_observer: resize_observer, + _resize_observer_closure: resize_observer_closure, + _event_listeners: event_listeners, + }) + } - fn create_resize_observer_closure( - inner: Rc, - ) -> Closure { - Closure::new(move |entries: js_sys::Array| { - let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() { - Some(entry) => entry, - None => return, - }; + fn create_resize_observer_closure( + inner: Rc, + ) -> Closure { + Closure::new(move |entries: js_sys::Array| { + let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() { + Some(entry) => entry, + None => return, + }; - let dpr = inner.browser_window.device_pixel_ratio(); - let dpr_f32 = dpr as f32; + let dpr = inner.browser_window.device_pixel_ratio(); + let dpr_f32 = dpr as f32; - let (physical_width, physical_height, logical_width, logical_height) = - if inner.has_device_pixel_support { - let size: web_sys::ResizeObserverSize = entry - .device_pixel_content_box_size() - .get(0) - .unchecked_into(); - let pw = size.inline_size() as u32; - let ph = size.block_size() as u32; - let lw = pw as f64 / dpr; - let lh = ph as f64 / dpr; - (pw, ph, lw as f32, lh as f32) - } else { - // Safari fallback: use contentRect (always CSS px). - let rect = entry.content_rect(); - let lw = rect.width() as f32; - let lh = rect.height() as f32; - let pw = (lw as f64 * dpr).round() as u32; - let ph = (lh as f64 * dpr).round() as u32; - (pw, ph, lw, lh) - }; + let (physical_width, physical_height, logical_width, logical_height) = + if inner.has_device_pixel_support { + let size: web_sys::ResizeObserverSize = entry + .device_pixel_content_box_size() + .get(0) + .unchecked_into(); + let pw = size.inline_size() as u32; + let ph = size.block_size() as u32; + let lw = pw as f64 / dpr; + let lh = ph as f64 / dpr; + (pw, ph, lw as f32, lh as f32) + } else { + // Safari fallback: use contentRect (always CSS px). + let rect = entry.content_rect(); + let lw = rect.width() as f32; + let lh = rect.height() as f32; + let pw = (lw as f64 * dpr).round() as u32; + let ph = (lh as f64 * dpr).round() as u32; + (pw, ph, lw, lh) + }; - let scale_changed = inner.notify_scale.replace(false); - let prev = inner.last_physical_size.get(); - let size_changed = prev != (physical_width, physical_height); + let scale_changed = inner.notify_scale.replace(false); + let prev = inner.last_physical_size.get(); + let size_changed = prev != (physical_width, physical_height); - if !scale_changed && !size_changed { - return; - } - inner - .last_physical_size - .set((physical_width, physical_height)); + if !scale_changed && !size_changed { + return; + } + inner + .last_physical_size + .set((physical_width, physical_height)); - // Skip rendering to a zero-size canvas (e.g. display:none). - if physical_width == 0 || physical_height == 0 { - let mut s = inner.state.borrow_mut(); - s.bounds.size = Size::default(); - s.scale_factor = dpr_f32; - // Still fire the callback so GPUI knows the window is gone. - drop(s); - let mut cbs = inner.callbacks.borrow_mut(); - if let Some(ref mut callback) = cbs.resize { - callback(Size::default(), dpr_f32); - } - return; - } + // Skip rendering to a zero-size canvas (e.g. display:none). + if physical_width == 0 || physical_height == 0 { + let mut s = inner.state.borrow_mut(); + s.bounds.size = Size::default(); + s.scale_factor = dpr_f32; + // Still fire the callback so GPUI knows the window is gone. + drop(s); + let mut cbs = inner.callbacks.borrow_mut(); + if let Some(ref mut callback) = cbs.resize { + callback(Size::default(), dpr_f32); + } + return; + } - let max_texture_dimension = inner.state.borrow().max_texture_dimension; - let clamped_width = physical_width.min(max_texture_dimension); - let clamped_height = physical_height.min(max_texture_dimension); + let max_texture_dimension = inner.state.borrow().max_texture_dimension; + let clamped_width = physical_width.min(max_texture_dimension); + let clamped_height = physical_height.min(max_texture_dimension); - inner - .pending_physical_size - .set(Some((clamped_width, clamped_height))); + inner + .pending_physical_size + .set(Some((clamped_width, clamped_height))); - { - let mut s = inner.state.borrow_mut(); - s.bounds.size = Size { - width: px(logical_width), - height: px(logical_height), - }; - s.scale_factor = dpr_f32; - } + { + let mut s = inner.state.borrow_mut(); + s.bounds.size = Size { + width: px(logical_width), + height: px(logical_height), + }; + s.scale_factor = dpr_f32; + } - let new_size = Size { - width: px(logical_width), - height: px(logical_height), - }; + let new_size = Size { + width: px(logical_width), + height: px(logical_height), + }; - let mut cbs = inner.callbacks.borrow_mut(); - if let Some(ref mut callback) = cbs.resize { - callback(new_size, dpr_f32); - } - }) - } + let mut cbs = inner.callbacks.borrow_mut(); + if let Some(ref mut callback) = cbs.resize { + callback(new_size, dpr_f32); + } + }) + } } impl WebWindowInner { - fn create_raf_closure(self: &Rc) -> Closure { - let raf_handle: Rc>> = Rc::new(RefCell::new(None)); - let raf_handle_inner = Rc::clone(&raf_handle); + fn create_raf_closure(self: &Rc) -> Closure { + let raf_handle: Rc>> = Rc::new(RefCell::new(None)); + let raf_handle_inner = Rc::clone(&raf_handle); - let this = Rc::clone(self); - let closure = Closure::new(move || { - { - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.request_frame { - callback(RequestFrameOptions { - require_presentation: true, - force_render: false, - }); - } - } + let this = Rc::clone(self); + let closure = Closure::new(move || { + { + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.request_frame { + callback(RequestFrameOptions { + require_presentation: true, + force_render: false, + }); + } + } - // Re-schedule for the next frame - if let Some(ref func) = *raf_handle_inner.borrow() { - this.browser_window.request_animation_frame(func).ok(); - } - }); + // Re-schedule for the next frame + if let Some(ref func) = *raf_handle_inner.borrow() { + this.browser_window.request_animation_frame(func).ok(); + } + }); - let js_func: js_sys::Function = - closure.as_ref().unchecked_ref::().clone(); - *raf_handle.borrow_mut() = Some(js_func); + let js_func: js_sys::Function = + closure.as_ref().unchecked_ref::().clone(); + *raf_handle.borrow_mut() = Some(js_func); - closure - } + closure + } - fn schedule_raf(&self, closure: &Closure) { - self.browser_window - .request_animation_frame(closure.as_ref().unchecked_ref()) - .ok(); - } + fn schedule_raf(&self, closure: &Closure) { + self.browser_window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .ok(); + } - fn observe_canvas(&self, observer: &web_sys::ResizeObserver) { - observer.unobserve(&self.canvas); - if self.has_device_pixel_support { - let options = web_sys::ResizeObserverOptions::new(); - options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox); - observer.observe_with_options(&self.canvas, &options); - } else { - observer.observe(&self.canvas); - } - } + fn observe_canvas(&self, observer: &web_sys::ResizeObserver) { + observer.unobserve(&self.canvas); + if self.has_device_pixel_support { + let options = web_sys::ResizeObserverOptions::new(); + options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox); + observer.observe_with_options(&self.canvas, &options); + } else { + observer.observe(&self.canvas); + } + } - fn watch_dpr_changes(self: &Rc, observer: &web_sys::ResizeObserver) { - let current_dpr = self.browser_window.device_pixel_ratio(); - let media_query = - format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})"); - let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else { - return; - }; + fn watch_dpr_changes(self: &Rc, observer: &web_sys::ResizeObserver) { + let current_dpr = self.browser_window.device_pixel_ratio(); + let media_query = + format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})"); + let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else { + return; + }; - let this = Rc::clone(self); - let observer = observer.clone(); + let this = Rc::clone(self); + let observer = observer.clone(); - let closure = Closure::::new(move |_event: JsValue| { - this.notify_scale.set(true); - this.observe_canvas(&observer); - this.watch_dpr_changes(&observer); - }); + let closure = Closure::::new(move |_event: JsValue| { + this.notify_scale.set(true); + this.observe_canvas(&observer); + this.watch_dpr_changes(&observer); + }); - mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) - .ok(); + mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) + .ok(); - *self.mql_handle.borrow_mut() = Some(MqlHandle { - mql, - _closure: closure, - }); - } + *self.mql_handle.borrow_mut() = Some(MqlHandle { + mql, + _closure: closure, + }); + } - pub(crate) fn register_visibility_change( - self: &Rc, - ) -> Option> { - let document = self.browser_window.document()?; - let this = Rc::clone(self); + pub(crate) fn register_visibility_change( + self: &Rc, + ) -> Option> { + let document = self.browser_window.document()?; + let this = Rc::clone(self); - let closure = Closure::::new(move |_event: JsValue| { - let is_visible = this - .browser_window - .document() - .map(|doc| { - let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into()) - .ok() - .and_then(|v| v.as_string()) - .unwrap_or_default(); - state_str == "visible" - }) - .unwrap_or(true); + let closure = Closure::::new(move |_event: JsValue| { + let is_visible = this + .browser_window + .document() + .map(|doc| { + let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into()) + .ok() + .and_then(|v| v.as_string()) + .unwrap_or_default(); + state_str == "visible" + }) + .unwrap_or(true); - { - let mut state = this.state.borrow_mut(); - state.is_active = is_visible; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(is_visible); - } - }); + { + let mut state = this.state.borrow_mut(); + state.is_active = is_visible; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(is_visible); + } + }); - document - .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) - .ok(); + document + .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) + .ok(); - Some(closure) - } + Some(closure) + } - pub(crate) fn with_input_handler( - &self, - f: impl FnOnce(&mut PlatformInputHandler) -> R, - ) -> Option { - let mut handler = self.state.borrow_mut().input_handler.take()?; - let result = f(&mut handler); - self.state.borrow_mut().input_handler = Some(handler); - Some(result) - } + pub(crate) fn with_input_handler( + &self, + f: impl FnOnce(&mut PlatformInputHandler) -> R, + ) -> Option { + let mut handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut handler); + self.state.borrow_mut().input_handler = Some(handler); + Some(result) + } - pub(crate) fn register_appearance_change( - self: &Rc, - ) -> Option> { - let mql = self - .browser_window - .match_media("(prefers-color-scheme: dark)") - .ok()??; + pub(crate) fn register_appearance_change( + self: &Rc, + ) -> Option> { + let mql = self + .browser_window + .match_media("(prefers-color-scheme: dark)") + .ok()??; - let this = Rc::clone(self); - let closure = Closure::::new(move |_event: JsValue| { - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.appearance_changed { - callback(); - } - }); + let this = Rc::clone(self); + let closure = Closure::::new(move |_event: JsValue| { + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.appearance_changed { + callback(); + } + }); - mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) - .ok(); + mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) + .ok(); - Some(closure) - } + Some(closure) + } } fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance { - let is_dark = browser_window - .match_media("(prefers-color-scheme: dark)") - .ok() - .flatten() - .map(|mql| mql.matches()) - .unwrap_or(false); + let is_dark = browser_window + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|mql| mql.matches()) + .unwrap_or(false); - if is_dark { - WindowAppearance::Dark - } else { - WindowAppearance::Light - } + if is_dark { + WindowAppearance::Dark + } else { + WindowAppearance::Light + } } struct MqlHandle { - mql: web_sys::MediaQueryList, - _closure: Closure, + mql: web_sys::MediaQueryList, + _closure: Closure, } impl Drop for MqlHandle { - fn drop(&mut self) { - self.mql - .remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref()) - .ok(); - } + fn drop(&mut self) { + self.mql + .remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref()) + .ok(); + } } // Safari does not support `devicePixelContentBoxSize`, so detect whether it's available. fn check_device_pixel_support() -> bool { - let global: JsValue = js_sys::global().into(); - let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else { - return false; - }; - let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else { - return false; - }; - let descriptor = js_sys::Object::get_own_property_descriptor( - &prototype.unchecked_into::(), - &"devicePixelContentBoxSize".into(), - ); - !descriptor.is_undefined() + let global: JsValue = js_sys::global().into(); + let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else { + return false; + }; + let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else { + return false; + }; + let descriptor = js_sys::Object::get_own_property_descriptor( + &prototype.unchecked_into::(), + &"devicePixelContentBoxSize".into(), + ); + !descriptor.is_undefined() } impl raw_window_handle::HasWindowHandle for WebWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - let canvas_ref: &JsValue = self.inner.canvas.as_ref(); - let obj = std::ptr::NonNull::from(canvas_ref).cast::(); - let handle = raw_window_handle::WebCanvasWindowHandle::new(obj); - Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) }) - } + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + let canvas_ref: &JsValue = self.inner.canvas.as_ref(); + let obj = std::ptr::NonNull::from(canvas_ref).cast::(); + let handle = raw_window_handle::WebCanvasWindowHandle::new(obj); + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) }) + } } impl raw_window_handle::HasDisplayHandle for WebWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - Ok(raw_window_handle::DisplayHandle::web()) - } + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Ok(raw_window_handle::DisplayHandle::web()) + } } impl PlatformWindow for WebWindow { - fn bounds(&self) -> Bounds { - self.inner.state.borrow().bounds - } + fn bounds(&self) -> Bounds { + self.inner.state.borrow().bounds + } - fn is_maximized(&self) -> bool { - false - } + fn is_maximized(&self) -> bool { + false + } - fn window_bounds(&self) -> WindowBounds { - WindowBounds::Windowed(self.bounds()) - } + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } - fn content_size(&self) -> Size { - self.inner.state.borrow().bounds.size - } + fn content_size(&self) -> Size { + self.inner.state.borrow().bounds.size + } - fn resize(&mut self, size: Size) { - let style = self.inner.canvas.style(); - style - .set_property("width", &format!("{}px", f32::from(size.width))) - .ok(); - style - .set_property("height", &format!("{}px", f32::from(size.height))) - .ok(); - } + fn resize(&mut self, size: Size) { + let style = self.inner.canvas.style(); + style + .set_property("width", &format!("{}px", f32::from(size.width))) + .ok(); + style + .set_property("height", &format!("{}px", f32::from(size.height))) + .ok(); + } - fn scale_factor(&self) -> f32 { - self.inner.state.borrow().scale_factor - } + fn scale_factor(&self) -> f32 { + self.inner.state.borrow().scale_factor + } - fn appearance(&self) -> WindowAppearance { - current_appearance(&self.inner.browser_window) - } + fn appearance(&self) -> WindowAppearance { + current_appearance(&self.inner.browser_window) + } - fn display(&self) -> Option> { - Some(self.display.clone()) - } + fn display(&self) -> Option> { + Some(self.display.clone()) + } - fn mouse_position(&self) -> Point { - self.inner.state.borrow().mouse_position - } + fn mouse_position(&self) -> Point { + self.inner.state.borrow().mouse_position + } - fn modifiers(&self) -> Modifiers { - self.inner.state.borrow().modifiers - } + fn modifiers(&self) -> Modifiers { + self.inner.state.borrow().modifiers + } - fn capslock(&self) -> Capslock { - self.inner.state.borrow().capslock - } + fn capslock(&self) -> Capslock { + self.inner.state.borrow().capslock + } - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.inner.state.borrow_mut().input_handler = Some(input_handler); - } + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.inner.state.borrow_mut().input_handler = Some(input_handler); + } - fn take_input_handler(&mut self) -> Option { - self.inner.state.borrow_mut().input_handler.take() - } + fn take_input_handler(&mut self) -> Option { + self.inner.state.borrow_mut().input_handler.take() + } - fn prompt( - &self, - _level: PromptLevel, - _msg: &str, - _detail: Option<&str>, - _answers: &[PromptButton], - ) -> Option> { - None - } + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } - fn activate(&self) { - self.inner.state.borrow_mut().is_active = true; - } + fn activate(&self) { + self.inner.state.borrow_mut().is_active = true; + } - fn is_active(&self) -> bool { - self.inner.state.borrow().is_active - } + fn is_active(&self) -> bool { + self.inner.state.borrow().is_active + } - fn is_hovered(&self) -> bool { - self.inner.state.borrow().is_hovered - } + fn is_hovered(&self) -> bool { + self.inner.state.borrow().is_hovered + } - fn background_appearance(&self) -> WindowBackgroundAppearance { - WindowBackgroundAppearance::Opaque - } + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } - fn set_title(&mut self, title: &str) { - self.inner.state.borrow_mut().title = title.to_owned(); - if let Some(document) = self.inner.browser_window.document() { - document.set_title(title); - } - } + fn set_title(&mut self, title: &str) { + self.inner.state.borrow_mut().title = title.to_owned(); + if let Some(document) = self.inner.browser_window.document() { + document.set_title(title); + } + } - fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} - fn minimize(&self) { - log::warn!("WebWindow::minimize is not supported in the browser"); - } + fn minimize(&self) { + log::warn!("WebWindow::minimize is not supported in the browser"); + } - fn zoom(&self) { - log::warn!("WebWindow::zoom is not supported in the browser"); - } + fn zoom(&self) { + log::warn!("WebWindow::zoom is not supported in the browser"); + } - fn toggle_fullscreen(&self) { - let mut state = self.inner.state.borrow_mut(); - state.is_fullscreen = !state.is_fullscreen; + fn toggle_fullscreen(&self) { + let mut state = self.inner.state.borrow_mut(); + state.is_fullscreen = !state.is_fullscreen; - if state.is_fullscreen { - let canvas: &web_sys::Element = self.inner.canvas.as_ref(); - canvas.request_fullscreen().ok(); - } else { - if let Some(document) = self.inner.browser_window.document() { - document.exit_fullscreen(); - } - } - } + if state.is_fullscreen { + let canvas: &web_sys::Element = self.inner.canvas.as_ref(); + canvas.request_fullscreen().ok(); + } else { + if let Some(document) = self.inner.browser_window.document() { + document.exit_fullscreen(); + } + } + } - fn is_fullscreen(&self) -> bool { - self.inner.state.borrow().is_fullscreen - } + fn is_fullscreen(&self) -> bool { + self.inner.state.borrow().is_fullscreen + } - fn on_request_frame(&self, callback: Box) { - self.inner.callbacks.borrow_mut().request_frame = Some(callback); - } + fn on_request_frame(&self, callback: Box) { + self.inner.callbacks.borrow_mut().request_frame = Some(callback); + } - fn on_input(&self, callback: Box DispatchEventResult>) { - self.inner.callbacks.borrow_mut().input = Some(callback); - } + fn on_input(&self, callback: Box DispatchEventResult>) { + self.inner.callbacks.borrow_mut().input = Some(callback); + } - fn on_active_status_change(&self, callback: Box) { - self.inner.callbacks.borrow_mut().active_status_change = Some(callback); - } + fn on_active_status_change(&self, callback: Box) { + self.inner.callbacks.borrow_mut().active_status_change = Some(callback); + } - fn on_hover_status_change(&self, callback: Box) { - self.inner.callbacks.borrow_mut().hover_status_change = Some(callback); - } + fn on_hover_status_change(&self, callback: Box) { + self.inner.callbacks.borrow_mut().hover_status_change = Some(callback); + } - fn on_resize(&self, callback: Box, f32)>) { - self.inner.callbacks.borrow_mut().resize = Some(callback); - } + fn on_resize(&self, callback: Box, f32)>) { + self.inner.callbacks.borrow_mut().resize = Some(callback); + } - fn on_moved(&self, callback: Box) { - self.inner.callbacks.borrow_mut().moved = Some(callback); - } + fn on_moved(&self, callback: Box) { + self.inner.callbacks.borrow_mut().moved = Some(callback); + } - fn on_should_close(&self, callback: Box bool>) { - self.inner.callbacks.borrow_mut().should_close = Some(callback); - } + fn on_should_close(&self, callback: Box bool>) { + self.inner.callbacks.borrow_mut().should_close = Some(callback); + } - fn on_close(&self, callback: Box) { - self.inner.callbacks.borrow_mut().close = Some(callback); - } + fn on_close(&self, callback: Box) { + self.inner.callbacks.borrow_mut().close = Some(callback); + } - fn on_hit_test_window_control(&self, callback: Box Option>) { - self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback); - } + fn on_hit_test_window_control(&self, callback: Box Option>) { + self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback); + } - fn on_appearance_changed(&self, callback: Box) { - self.inner.callbacks.borrow_mut().appearance_changed = Some(callback); - } + fn on_appearance_changed(&self, callback: Box) { + self.inner.callbacks.borrow_mut().appearance_changed = Some(callback); + } - fn draw(&self, scene: &Scene) { - if let Some((width, height)) = self.inner.pending_physical_size.take() { - if self.inner.canvas.width() != width || self.inner.canvas.height() != height { - self.inner.canvas.set_width(width); - self.inner.canvas.set_height(height); - } + fn draw(&self, scene: &Scene) { + if let Some((width, height)) = self.inner.pending_physical_size.take() { + if self.inner.canvas.width() != width || self.inner.canvas.height() != height { + self.inner.canvas.set_width(width); + self.inner.canvas.set_height(height); + } - let mut state = self.inner.state.borrow_mut(); - state.renderer.update_drawable_size(Size { - width: DevicePixels(width as i32), - height: DevicePixels(height as i32), - }); - drop(state); - } + let mut state = self.inner.state.borrow_mut(); + state.renderer.update_drawable_size(Size { + width: DevicePixels(width as i32), + height: DevicePixels(height as i32), + }); + drop(state); + } - self.inner.state.borrow_mut().renderer.draw(scene); - } + self.inner.state.borrow_mut().renderer.draw(scene); + } - fn completed_frame(&self) { - // On web, presentation happens automatically via wgpu surface present - } + fn completed_frame(&self) { + // On web, presentation happens automatically via wgpu surface present + } - fn sprite_atlas(&self) -> Arc { - self.inner.state.borrow().renderer.sprite_atlas().clone() - } + fn sprite_atlas(&self) -> Arc { + self.inner.state.borrow().renderer.sprite_atlas().clone() + } - fn is_subpixel_rendering_supported(&self) -> bool { - self.inner - .state - .borrow() - .renderer - .supports_dual_source_blending() - } + fn is_subpixel_rendering_supported(&self) -> bool { + self.inner + .state + .borrow() + .renderer + .supports_dual_source_blending() + } - fn gpu_specs(&self) -> Option { - Some(self.inner.state.borrow().renderer.gpu_specs()) - } + fn gpu_specs(&self) -> Option { + Some(self.inner.state.borrow().renderer.gpu_specs()) + } - fn update_ime_position(&self, _bounds: Bounds) {} + fn update_ime_position(&self, _bounds: Bounds) {} - fn request_decorations(&self, _decorations: WindowDecorations) {} + fn request_decorations(&self, _decorations: WindowDecorations) {} - fn show_window_menu(&self, _position: Point) {} + fn show_window_menu(&self, _position: Point) {} - fn start_window_move(&self) {} + fn start_window_move(&self) {} - fn start_window_resize(&self, _edge: ResizeEdge) {} + fn start_window_resize(&self, _edge: ResizeEdge) {} - fn window_decorations(&self) -> Decorations { - Decorations::Server - } + fn window_decorations(&self) -> Decorations { + Decorations::Server + } - fn set_app_id(&mut self, _app_id: &str) {} + fn set_app_id(&mut self, _app_id: &str) {} - fn window_controls(&self) -> WindowControls { - WindowControls { - fullscreen: true, - maximize: false, - minimize: false, - window_menu: false, - } - } + fn window_controls(&self) -> WindowControls { + WindowControls { + fullscreen: true, + maximize: false, + minimize: false, + window_menu: false, + } + } - fn set_client_inset(&self, _inset: Pixels) {} + fn set_client_inset(&self, _inset: Pixels) {} } diff --git a/crates/gpui_wgpu/benches/layout_line.rs b/crates/gpui_wgpu/benches/layout_line.rs index 1b4a40163e..171e02a7a1 100644 --- a/crates/gpui_wgpu/benches/layout_line.rs +++ b/crates/gpui_wgpu/benches/layout_line.rs @@ -5,79 +5,79 @@ 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"); + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"); // ~4 000 chars of typical ASCII code text. 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 + 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 } 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 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_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 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(); + let text = code_text(); - let runs_no_fallback = vec![FontRun { - len: text.len(), - font_id: font_id_no_fallback, - letter_spacing: None, - }]; - let runs_with_fallback = vec![FontRun { - len: text.len(), - font_id: font_id_with_fallback, - letter_spacing: None, - }]; + let runs_no_fallback = vec![FontRun { + len: text.len(), + font_id: font_id_no_fallback, + letter_spacing: None, + }]; + let runs_with_fallback = vec![FontRun { + len: text.len(), + font_id: font_id_with_fallback, + letter_spacing: None, + }]; - let mut group = c.benchmark_group("layout_line"); + 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("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("with_fallback_ascii", |b| { + b.iter(|| system.layout_line(&text, px(14.0), &runs_with_fallback)) + }); - group.finish(); + group.finish(); } criterion_group!(benches, bench_layout_line); diff --git a/crates/gpui_wgpu/src/cosmic_text_system.rs b/crates/gpui_wgpu/src/cosmic_text_system.rs index ac2eceba79..26d8fc6847 100644 --- a/crates/gpui_wgpu/src/cosmic_text_system.rs +++ b/crates/gpui_wgpu/src/cosmic_text_system.rs @@ -1,14 +1,14 @@ 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, + Attrs, AttrsList, Ellipsize, Family, Font as CosmicTextFont, + FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine, }; use gpui::{ - Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, - LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, - SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, - size, + Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, + LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, + SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, + size, }; use itertools::Itertools; @@ -16,8 +16,8 @@ use parking_lot::RwLock; use smallvec::SmallVec; use std::{borrow::Cow, sync::Arc}; use swash::{ - scale::{Render, ScaleContext, Source, StrikeWith}, - zeno::{Format, Vector}, + scale::{Render, ScaleContext, Source, StrikeWith}, + zeno::{Format, Vector}, }; use unicode_segmentation::UnicodeSegmentation; @@ -25,1048 +25,1048 @@ pub struct CosmicTextSystem(RwLock); #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct FontKey { - family: SharedString, - features: FontFeatures, - fallbacks: Option, + family: SharedString, + features: FontFeatures, + fallbacks: Option, } impl FontKey { - fn new(family: SharedString, features: FontFeatures, fallbacks: Option) -> Self { - Self { - family, - features, - fallbacks, - } - } + fn new(family: SharedString, features: FontFeatures, fallbacks: Option) -> Self { + Self { + family, + features, + fallbacks, + } + } } struct CosmicTextSystemState { - font_system: FontSystem, - scratch: ShapeBuffer, - swash_scale_context: ScaleContext, - /// 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, + font_system: FontSystem, + scratch: ShapeBuffer, + swash_scale_context: ScaleContext, + /// 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)]>, + 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)]>, } impl CosmicTextSystem { - pub fn new(system_font_fallback: &str) -> Self { - let font_system = FontSystem::new(); + 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(), - loaded_fonts: Vec::new(), - font_ids_by_family_cache: HashMap::default(), - system_font_fallback: system_font_fallback.to_string(), - })) - } + Self(RwLock::new(CosmicTextSystemState { + font_system, + scratch: ShapeBuffer::default(), + swash_scale_context: ScaleContext::new(), + 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(), - ); + 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(), - loaded_fonts: Vec::new(), - font_ids_by_family_cache: HashMap::default(), - system_font_fallback: system_font_fallback.to_string(), - })) - } + Self(RwLock::new(CosmicTextSystemState { + font_system, + scratch: ShapeBuffer::default(), + swash_scale_context: ScaleContext::new(), + 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 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(); - result.dedup(); - result - } + 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(); + 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() - }; + 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)?; + let ix = find_best_match(font, candidates, &state)?; - Ok(candidates[ix]) - } + Ok(candidates[ix]) + } - fn font_metrics(&self, font_id: FontId) -> FontMetrics { - let metrics = self - .0 - .read() - .loaded_font(font_id) - .font - .as_swash() - .metrics(&[]); + 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), - }, - } - } + 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 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 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_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 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 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 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 - } + 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 loaded_font(&self, font_id: FontId) -> &LoadedFont { + &self.loaded_fonts[font_id.0] + } - #[profiling::function] - fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { - let db = self.font_system.db_mut(); - for bytes in fonts { - match bytes { - Cow::Borrowed(embedded_font) => { - db.load_font_data(embedded_font.to_vec()); - } - Cow::Owned(bytes) => { - db.load_font_data(bytes); - } - } - } - Ok(()) - } + #[profiling::function] + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let db = self.font_system.db_mut(); + for bytes in fonts { + match bytes { + Cow::Borrowed(embedded_font) => { + db.load_font_data(embedded_font.to_vec()); + } + Cow::Owned(bytes) => { + db.load_font_data(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()), - }; + #[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 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 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 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")?; + 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", - ]; + // 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; - }; + 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), - }); - } + 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) - } + 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 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 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)?; - Ok(Bounds { - origin: point(image.placement.left.into(), (-image.placement.top).into()), - size: size(image.placement.width.into(), image.placement.height.into()), - }) - } + fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { + let image = self.render_glyph_image(params)?; + Ok(Bounds { + origin: point(image.placement.left.into(), (-image.placement.top).into()), + size: size(image.placement.width.into(), image.placement.height.into()), + }) + } - #[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"); - } + #[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 = 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)) - } - } - } - } + let mut image = 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); + 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 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 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 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 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:?}")) - } + 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")?; + /// 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()), - }); + 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) - } - } + Ok(font_id) + } + } - #[profiling::function] - fn layout_line(&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; + #[profiling::function] + fn layout_line(&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 loaded_font = self.loaded_font(run.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 {:?}", - run.font_id - ); - offs = run_end; - continue; - }; - let Some(first_family) = face.families.first() else { - log::warn!( - "font face has no family names for font_id {:?}", - run.font_id - ); - offs = run_end; - continue; - }; + let loaded_font = self.loaded_font(run.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 {:?}", + run.font_id + ); + offs = run_end; + continue; + }; + let Some(first_family) = face.families.first() else { + log::warn!( + "font face has no family names for font_id {:?}", + run.font_id + ); + offs = run_end; + continue; + }; - let primary_family_name: SharedString = first_family.0.clone().into(); - let primary_stretch = face.stretch; - let primary_style = face.style; - let primary_weight = face.weight; - let primary_features = loaded_font.features.clone(); - let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain); - let letter_spacing = run - .letter_spacing - .map(|spacing| spacing.as_f32() / font_size.as_f32()); + let primary_family_name: SharedString = first_family.0.clone().into(); + let primary_stretch = face.stretch; + let primary_style = face.style; + let primary_weight = face.weight; + let primary_features = loaded_font.features.clone(); + let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain); + let letter_spacing = run + .letter_spacing + .map(|spacing| spacing.as_f32() / font_size.as_f32()); - // build one `Attrs` per slot up front. each clone of span attrs - // would otherwise re-allocate the `font_features` Vec. - let mut primary_attrs = Attrs::new() - .metadata(run.font_id.0) - .family(Family::Name(&primary_family_name)) - .stretch(primary_stretch) - .style(primary_style) - .weight(primary_weight) - .font_features(primary_features.clone()); - if let Some(letter_spacing) = letter_spacing { - primary_attrs = primary_attrs.letter_spacing(letter_spacing); - } - let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain - .iter() - .map(|(fb_id, fb_name)| { - let mut attrs = Attrs::new() - .metadata(fb_id.0) - .family(Family::Name(fb_name)) - .stretch(primary_stretch) - .style(primary_style) - .weight(primary_weight) - .font_features(primary_features.clone()); - if let Some(letter_spacing) = letter_spacing { - attrs = attrs.letter_spacing(letter_spacing); - } - attrs - }) - .collect(); + // build one `Attrs` per slot up front. each clone of span attrs + // would otherwise re-allocate the `font_features` Vec. + let mut primary_attrs = Attrs::new() + .metadata(run.font_id.0) + .family(Family::Name(&primary_family_name)) + .stretch(primary_stretch) + .style(primary_style) + .weight(primary_weight) + .font_features(primary_features.clone()); + if let Some(letter_spacing) = letter_spacing { + primary_attrs = primary_attrs.letter_spacing(letter_spacing); + } + let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain + .iter() + .map(|(fb_id, fb_name)| { + let mut attrs = Attrs::new() + .metadata(fb_id.0) + .family(Family::Name(fb_name)) + .stretch(primary_stretch) + .style(primary_style) + .weight(primary_weight) + .font_features(primary_features.clone()); + if let Some(letter_spacing) = letter_spacing { + attrs = attrs.letter_spacing(letter_spacing); + } + attrs + }) + .collect(); - let spans = if 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, &fallback_chain, &covers) - }; + let spans = if 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, &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; - } + 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 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 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; + 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; - } + // 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, - }; + 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], - }); - } - } + 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(), - } - } + LineLayout { + font_size, + width: layout.w.into(), + ascent: layout.max_ascent.into(), + descent: layout.max_descent.into(), + runs, + len: text.len(), + } + } } #[cfg(feature = "font-kit")] fn find_best_match( - font: &Font, - candidates: &[FontId], - state: &CosmicTextSystemState, + 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 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")?; + 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) + Ok(ix) } #[cfg(not(feature = "font-kit"))] fn find_best_match( - font: &Font, - candidates: &[FontId], - state: &CosmicTextSystemState, + 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); - } + 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 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; + 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")?; + 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; + 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; - } - } + if score < best_score { + best_score = score; + best_index = index; + } + } - Ok(best_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, + 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, + 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 + 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)], + slot: Option, + primary: FontId, + fallback_chain: &[(FontId, SharedString)], ) -> FontId { - match slot { - None => primary, - Some(ix) => fallback_chain[ix].0, - } + 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, + 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; - } - for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() { - if covers(*fb_id, ch) { - return Some(ix); - } - } - None + 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; + } + for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() { + if covers(*fb_id, ch) { + return Some(ix); + } + } + None } 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) + 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 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); + let tag = cosmic_text::FeatureTag::new(&name_bytes); - result.set(tag, feature.1); - } - Ok(result) + 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(), - } + 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, + 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, - }, - } + 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" + // TODO: Include other common emoji fonts + postscript_name == "NotoColorEmoji" } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn fid(i: usize) -> FontId { - FontId(i) - } + 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 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, - } - } + fn span(start: usize, end: usize, slot: Option, font_id: FontId) -> RunSpan { + RunSpan { + start, + end, + slot, + font_id, + } + } - #[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_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 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 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 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 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 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_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_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_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_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_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_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()); - } + #[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_wgpu/src/wgpu_atlas.rs b/crates/gpui_wgpu/src/wgpu_atlas.rs index 94b4b561c0..d8cc7f622e 100644 --- a/crates/gpui_wgpu/src/wgpu_atlas.rs +++ b/crates/gpui_wgpu/src/wgpu_atlas.rs @@ -2,8 +2,8 @@ use anyhow::{Context as _, Result}; use collections::FxHashMap; use etagere::{BucketedAtlasAllocator, size2}; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, + PlatformAtlas, Point, Size, }; use parking_lot::Mutex; use std::{borrow::Cow, ops, sync::Arc}; @@ -11,471 +11,471 @@ 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) + 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), - } + Point { + x: DevicePixels(point.x), + y: DevicePixels(point.y), + } } pub struct WgpuAtlas(Mutex); struct PendingUpload { - id: AtlasTextureId, - bounds: Bounds, - data: Vec, + 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, + 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, + 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 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 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 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(), - } - } + 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(); - } + /// 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(); - } + /// 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 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(); + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); - let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { - return; - }; + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; - let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { - return; - }; + 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.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); - } - } - } + if let Some(mut texture) = texture_slot.take() { + 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]; + 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); - } - } + 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) - } + 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), - }; + 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 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 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 view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let texture_list = &mut self.storage[kind]; - let index = texture_list.free_list.pop(); + 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, - }; + 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") - } - } + 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()); + 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 }); - } + 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(); + 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, - }, - ); - } - } + 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, + 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, - } - } + 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, - } - } + 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()) - } + 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") - } + 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, + 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 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 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 decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } - fn is_unreferenced(&self) -> bool { - self.live_atlas_keys == 0 - } + 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(), - } + 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; + 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))) - }) - } + 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()?; + #[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])))); + 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(()) - } + // 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 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_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] - ); - } + #[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_wgpu/src/wgpu_context.rs b/crates/gpui_wgpu/src/wgpu_context.rs index 8bc47eaece..2659979661 100644 --- a/crates/gpui_wgpu/src/wgpu_context.rs +++ b/crates/gpui_wgpu/src/wgpu_context.rs @@ -7,19 +7,19 @@ 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, - dual_source_blending: bool, - color_texture_format: wgpu::TextureFormat, - device_lost: Arc, + pub instance: wgpu::Instance, + pub adapter: wgpu::Adapter, + pub device: Arc, + pub queue: Arc, + dual_source_blending: bool, + color_texture_format: wgpu::TextureFormat, + device_lost: Arc, } #[derive(Clone, Copy)] pub struct CompositorGpuHint { - pub vendor_id: u32, - pub device_id: u32, + pub vendor_id: u32, + pub device_id: u32, } /// Extra wgpu features and limits that an application can request on top of @@ -27,495 +27,495 @@ pub struct CompositorGpuHint { /// [`gpui::App::set_gpu_requirements`] *before* opening any windows. #[derive(Clone, Debug, Default)] pub struct WgpuDeviceRequirements { - /// Additional [`wgpu::Features`] to enable. These are OR-ed with gpui's - /// own required features. - pub features: wgpu::Features, - /// Additional [`wgpu::Limits`] to request. Each field is merged by taking - /// `max(gpui_limit, app_limit)` for upper-bound limits and - /// `min(gpui_limit, app_limit)` for alignment/lower-bound limits. - pub limits: wgpu::Limits, + /// Additional [`wgpu::Features`] to enable. These are OR-ed with gpui's + /// own required features. + pub features: wgpu::Features, + /// Additional [`wgpu::Limits`] to request. Each field is merged by taking + /// `max(gpui_limit, app_limit)` for upper-bound limits and + /// `min(gpui_limit, app_limit)` for alignment/lower-bound limits. + pub limits: wgpu::Limits, } impl WgpuContext { - #[cfg(not(target_family = "wasm"))] - pub fn new( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> anyhow::Result { - Self::new_with_options(instance, surface, compositor_gpu, false, extra_requirements) - } + #[cfg(not(target_family = "wasm"))] + pub fn new( + instance: wgpu::Instance, + surface: &wgpu::Surface<'_>, + compositor_gpu: Option, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> anyhow::Result { + Self::new_with_options(instance, surface, compositor_gpu, false, extra_requirements) + } - #[cfg(not(target_family = "wasm"))] - pub fn new_rejecting_software( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> anyhow::Result { - Self::new_with_options(instance, surface, compositor_gpu, true, extra_requirements) - } + #[cfg(not(target_family = "wasm"))] + pub fn new_rejecting_software( + instance: wgpu::Instance, + surface: &wgpu::Surface<'_>, + compositor_gpu: Option, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> anyhow::Result { + Self::new_with_options(instance, surface, compositor_gpu, true, extra_requirements) + } - #[cfg(not(target_family = "wasm"))] - fn new_with_options( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - reject_software: bool, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> 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 - } - }; + #[cfg(not(target_family = "wasm"))] + fn new_with_options( + instance: wgpu::Instance, + surface: &wgpu::Surface<'_>, + compositor_gpu: Option, + reject_software: bool, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> 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, - extra_requirements, - ))?; + // 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, + extra_requirements, + ))?; - 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); - } - } - }); + 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 - ); + log::info!( + "Selected GPU adapter: {:?} ({:?})", + adapter.get_info().name, + adapter.get_info().backend + ); - Ok(Self { - instance, - adapter, - device: Arc::new(device), - queue: Arc::new(queue), - dual_source_blending, - color_texture_format, - device_lost, - }) - } + Ok(Self { + instance, + adapter, + device: Arc::new(device), + queue: Arc::new(queue), + dual_source_blending, + color_texture_format, + device_lost, + }) + } - #[cfg(target_family = "wasm")] - pub async fn new_web() -> anyhow::Result { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: None, - }); + #[cfg(target_family = "wasm")] + pub async fn new_web() -> anyhow::Result { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, + 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::HighPerformance, - compatible_surface: None, - force_fallback_adapter: false, - }) - .await - .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?; + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }) + .await + .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?; - log::info!( - "Selected GPU adapter: {:?} ({:?})", - adapter.get_info().name, - adapter.get_info().backend - ); + log::info!( + "Selected GPU adapter: {:?} ({:?})", + adapter.get_info().name, + adapter.get_info().backend + ); - let device_lost = Arc::new(AtomicBool::new(false)); - let (device, queue, dual_source_blending, color_texture_format) = - Self::create_device(&adapter, None).await?; + let device_lost = Arc::new(AtomicBool::new(false)); + let (device, queue, dual_source_blending, color_texture_format) = + Self::create_device(&adapter, None).await?; - Ok(Self { - instance, - adapter, - device: Arc::new(device), - queue: Arc::new(queue), - dual_source_blending, - color_texture_format, - device_lost, - }) - } + Ok(Self { + instance, + adapter, + device: Arc::new(device), + queue: Arc::new(queue), + dual_source_blending, + color_texture_format, + device_lost, + }) + } - async fn create_device( - adapter: &wgpu::Adapter, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> { - let dual_source_blending = adapter - .features() - .contains(wgpu::Features::DUAL_SOURCE_BLENDING); + async fn create_device( + adapter: &wgpu::Adapter, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> 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. \ + 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)?; + let color_atlas_texture_format = Self::select_color_texture_format(adapter)?; - let mut required_limits = wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()); + let mut required_limits = wgpu::Limits::downlevel_defaults() + .using_resolution(adapter.limits()) + .using_alignment(adapter.limits()); - // Merge application-requested requirements. - if let Some(reqs) = extra_requirements { - required_features |= reqs.features; - required_limits = required_limits.or_better_values_from(&reqs.limits); - } + // Merge application-requested requirements. + if let Some(reqs) = extra_requirements { + required_features |= reqs.features; + required_limits = required_limits.or_better_values_from(&reqs.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}"))?; + 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, - )) - } + Ok(( + device, + queue, + dual_source_blending, + color_atlas_texture_format, + )) + } - #[cfg(not(target_family = "wasm"))] - pub fn instance(display: Box) -> wgpu::Instance { - #[cfg(not(target_os = "windows"))] - let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL; - #[cfg(target_os = "windows")] - let backends = wgpu::Backends::DX12; + #[cfg(not(target_family = "wasm"))] + pub fn instance(display: Box) -> wgpu::Instance { + #[cfg(not(target_os = "windows"))] + let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL; + #[cfg(target_os = "windows")] + let backends = wgpu::Backends::DX12; - wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: Some(display), - }) - } + wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + 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 \ + 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(()) - } + 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, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> anyhow::Result<( - wgpu::Adapter, - wgpu::Device, - wgpu::Queue, - bool, - TextureFormat, - )> { - let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await; + /// 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, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> 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 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); - } + 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(); + // 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; + // 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 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 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 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, - }; + 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, - ) - }); + ( + 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, - ); - } + // 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(); + // 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; - } + 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); + log::info!("Testing adapter: {} ({:?})...", info.name, info.backend); - match Self::try_adapter_with_surface(&adapter, surface, extra_requirements).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 - ); - } - } - } + match Self::try_adapter_with_surface(&adapter, surface, extra_requirements).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") - } + 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<'_>, - extra_requirements: Option<&WgpuDeviceRequirements>, - ) -> 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"); - } + /// 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<'_>, + extra_requirements: Option<&WgpuDeviceRequirements>, + ) -> 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, extra_requirements).await?; - let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation); + let (device, queue, dual_source_blending, color_atlas_texture_format) = + Self::create_device(adapter, extra_requirements).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![], - }; + 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); + surface.configure(&device, &test_config); - let error = error_scope.pop().await; - if let Some(e) = error { - anyhow::bail!("surface configuration failed: {e}"); - } + 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, - )) - } + 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); - if bgra_features.allowed_usages.contains(required_usages) { - return Ok(wgpu::TextureFormat::Bgra8Unorm); - } + 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); + if bgra_features.allowed_usages.contains(required_usages) { + return Ok(wgpu::TextureFormat::Bgra8Unorm); + } - let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm); - if rgba_features.allowed_usages.contains(required_usages) { - let info = adapter.get_info(); - log::warn!( - "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \ + let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm); + 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); - } + 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 \ + 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 supports_dual_source_blending(&self) -> bool { - self.dual_source_blending - } + info.name, + info.backend, + info.device, + required_usages, + bgra_features.allowed_usages, + rgba_features.allowed_usages, + )) + } + pub fn supports_dual_source_blending(&self) -> bool { + self.dual_source_blending + } - pub fn color_texture_format(&self) -> wgpu::TextureFormat { - self.color_texture_format - } + 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 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) - } + /// 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(); + 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" - ); + 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") + u32::from_str_radix(id, 16).context("parsing PCI ID as hex") } #[cfg(test)] mod tests { - use super::parse_pci_id; + 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(), - ); + #[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(), - ); - } + assert_eq!( + parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(), + parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(), + ); + } } diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index ff84c8fe01..e97fa42d8a 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -1,10 +1,10 @@ use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext, WgpuDeviceRequirements}; use bytemuck::{Pod, Zeroable}; use gpui::{ - AtlasTextureId, BackdropFilter, Background, Bounds, DevicePixels, FilterBoundary, GpuSpecs, - MonochromeSprite, PaintSurface, Path, Point, PolychromeSprite, PrimitiveBatch, Quad, - ScaledFilter, ScaledPixels, Scene, Shadow, Size, SubpixelSprite, Underline, - get_gamma_correction_ratios, + AtlasTextureId, BackdropFilter, Background, Bounds, DevicePixels, FilterBoundary, GpuSpecs, + MonochromeSprite, PaintSurface, Path, Point, PolychromeSprite, PrimitiveBatch, Quad, + ScaledFilter, ScaledPixels, Scene, Shadow, Size, SubpixelSprite, Underline, + get_gamma_correction_ratios, }; use log::warn; @@ -15,9 +15,9 @@ use log::warn; /// forcing this backend to handle (or deliberately ignore) the new filter rather than silently /// dropping it. fn max_blur_radius(filters: &[ScaledFilter]) -> f32 { - filters.iter().fold(0.0, |acc, filter| match filter { - ScaledFilter::Blur(radius) => acc.max(radius.0), - }) + filters.iter().fold(0.0, |acc, filter| match filter { + ScaledFilter::Blur(radius) => acc.max(radius.0), + }) } #[cfg(not(target_family = "wasm"))] use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; @@ -29,32 +29,32 @@ use std::sync::{Arc, Mutex}; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct GlobalParams { - viewport_size: [f32; 2], - premultiplied_alpha: u32, - pad: u32, + viewport_size: [f32; 2], + premultiplied_alpha: u32, + pad: u32, } #[repr(C)] #[derive(Clone, Copy, Default, Pod, Zeroable)] struct PodBounds { - origin: [f32; 2], - size: [f32; 2], + 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], - } - } + 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 SurfaceParams { - bounds: PodBounds, - content_mask: PodBounds, + bounds: PodBounds, + content_mask: PodBounds, } /// Uniform passed to the blur pipelines. The same struct drives the downsample, separable @@ -62,97 +62,97 @@ struct SurfaceParams { #[repr(C)] #[derive(Clone, Copy, Default, Pod, Zeroable)] struct BlurParams { - /// Composite target rectangle, in device pixels (composite pass only). - bounds: PodBounds, - /// Clip rectangle, in device pixels (composite pass only). - content_mask: PodBounds, - /// Rounded-corner radii (tl, tr, br, bl), in device pixels (composite pass only). - corner_radii: [f32; 4], - /// Per-tap sampling step in UV space (gaussian passes only): (1/width, 0) or (0, 1/height). - direction: [f32; 2], - /// Gaussian sigma, in the (half-resolution) blur texture's pixels. - sigma: f32, - /// Element opacity, multiplied into the composited result. - opacity: f32, - /// Number of taps to each side of center (gaussian passes only). - tap_count: f32, - /// Spacing between taps in pixels; >1 lets `tap_count` taps span very large radii without - /// truncating the gaussian (see #6 in review). - tap_step: f32, - /// 1.0 to clip the composite to the rounded rect (backdrop — the panel has a defined shape), - /// 0.0 to let the blurred result fade out on its own (content `filter` — it bleeds past the - /// element bounds like CSS, so the fade isn't sharply truncated at the box edge). - clip_rounded: f32, - /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the - /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy - /// (the scene blit, which must not downsample). Downsample pass only. - downsample: f32, + /// Composite target rectangle, in device pixels (composite pass only). + bounds: PodBounds, + /// Clip rectangle, in device pixels (composite pass only). + content_mask: PodBounds, + /// Rounded-corner radii (tl, tr, br, bl), in device pixels (composite pass only). + corner_radii: [f32; 4], + /// Per-tap sampling step in UV space (gaussian passes only): (1/width, 0) or (0, 1/height). + direction: [f32; 2], + /// Gaussian sigma, in the (half-resolution) blur texture's pixels. + sigma: f32, + /// Element opacity, multiplied into the composited result. + opacity: f32, + /// Number of taps to each side of center (gaussian passes only). + tap_count: f32, + /// Spacing between taps in pixels; >1 lets `tap_count` taps span very large radii without + /// truncating the gaussian (see #6 in review). + tap_step: f32, + /// 1.0 to clip the composite to the rounded rect (backdrop — the panel has a defined shape), + /// 0.0 to let the blurred result fade out on its own (content `filter` — it bleeds past the + /// element bounds like CSS, so the fade isn't sharply truncated at the box edge). + clip_rounded: f32, + /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the + /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy + /// (the scene blit, which must not downsample). Downsample pass only. + downsample: f32, } #[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, + 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, + bounds: Bounds, } #[derive(Clone, Debug)] #[repr(C)] struct PathRasterizationVertex { - xy_position: Point, - st_position: Point, - color: Background, - bounds: Bounds, + xy_position: Point, + st_position: Point, + color: Background, + bounds: Bounds, } 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, + 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, - /// Copies a source texture into the (smaller) target with one bilinear tap. Used both to - /// downsample the scene into the half-resolution blur texture and to blit the offscreen - /// scene into the swapchain at the end of the frame. - blur_downsample: wgpu::RenderPipeline, - /// One axis of a separable gaussian blur; direction is supplied per draw via [`BlurParams`]. - blur: wgpu::RenderPipeline, - /// Composites a blurred texture into a rounded rectangle (with clip + opacity). - blur_composite: wgpu::RenderPipeline, + 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, + /// Copies a source texture into the (smaller) target with one bilinear tap. Used both to + /// downsample the scene into the half-resolution blur texture and to blit the offscreen + /// scene into the swapchain at the end of the frame. + blur_downsample: wgpu::RenderPipeline, + /// One axis of a separable gaussian blur; direction is supplied per draw via [`BlurParams`]. + blur: wgpu::RenderPipeline, + /// Composites a blurred texture into a rounded rectangle (with clip + opacity). + blur_composite: wgpu::RenderPipeline, } struct WgpuBindGroupLayouts { - globals: wgpu::BindGroupLayout, - instances: wgpu::BindGroupLayout, - instances_with_texture: wgpu::BindGroupLayout, - surfaces: wgpu::BindGroupLayout, - blur: wgpu::BindGroupLayout, + globals: wgpu::BindGroupLayout, + instances: wgpu::BindGroupLayout, + instances_with_texture: wgpu::BindGroupLayout, + surfaces: wgpu::BindGroupLayout, + blur: wgpu::BindGroupLayout, } /// Shared GPU context reference, used to coordinate device recovery across multiple windows. @@ -160,61 +160,61 @@ pub type GpuContext = Rc>>; /// 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, - surface_sampler: wgpu::Sampler, - #[allow(dead_code)] - surface_uniform_buffer: wgpu::Buffer, - /// One reused uniform buffer holding [`BlurParams`] for every blur pass in a frame, each at a - /// distinct (alignment-strided) offset. Avoids allocating a buffer per pass; distinct offsets - /// mean `write_buffer`'s last-write-at-submit semantics don't clobber earlier passes. - blur_params_buffer: wgpu::Buffer, - globals_buffer: wgpu::Buffer, - globals_bind_group: wgpu::BindGroup, - path_globals_bind_group: wgpu::BindGroup, - instance_buffer: wgpu::Buffer, - path_intermediate_texture: Option, - path_intermediate_view: Option, - path_msaa_texture: Option, - path_msaa_view: Option, - /// Blur offscreen targets. Allocated lazily (only when a frame actually uses a blur filter) - /// so apps that never blur pay no extra VRAM. `None`/empty until first use. - /// - /// Full-resolution offscreen color target the scene is rendered into so that blur passes - /// can sample already-painted content; blitted to the swapchain at the end of the frame. - scene_color_texture: Option, - scene_color_view: Option, - /// Half-resolution ping/pong targets for the downsample + separable gaussian passes. - blur_ping_texture: Option, - blur_ping_view: Option, - blur_pong_texture: Option, - blur_pong_view: Option, - /// Full-resolution offscreen targets a content-filter (`filter`) group renders into before - /// being blurred and composited back. One per nesting level (indexed by depth) so nested - /// content blurs isolate correctly, up to [`MAX_FILTER_DEPTH`]; deeper nests render inline. - group_textures: Vec, - group_views: Vec, + device: Arc, + queue: Arc, + surface: wgpu::Surface<'static>, + pipelines: WgpuPipelines, + bind_group_layouts: WgpuBindGroupLayouts, + atlas_sampler: wgpu::Sampler, + surface_sampler: wgpu::Sampler, + #[allow(dead_code)] + surface_uniform_buffer: wgpu::Buffer, + /// One reused uniform buffer holding [`BlurParams`] for every blur pass in a frame, each at a + /// distinct (alignment-strided) offset. Avoids allocating a buffer per pass; distinct offsets + /// mean `write_buffer`'s last-write-at-submit semantics don't clobber earlier passes. + blur_params_buffer: wgpu::Buffer, + globals_buffer: wgpu::Buffer, + globals_bind_group: wgpu::BindGroup, + path_globals_bind_group: wgpu::BindGroup, + instance_buffer: wgpu::Buffer, + path_intermediate_texture: Option, + path_intermediate_view: Option, + path_msaa_texture: Option, + path_msaa_view: Option, + /// Blur offscreen targets. Allocated lazily (only when a frame actually uses a blur filter) + /// so apps that never blur pay no extra VRAM. `None`/empty until first use. + /// + /// Full-resolution offscreen color target the scene is rendered into so that blur passes + /// can sample already-painted content; blitted to the swapchain at the end of the frame. + scene_color_texture: Option, + scene_color_view: Option, + /// Half-resolution ping/pong targets for the downsample + separable gaussian passes. + blur_ping_texture: Option, + blur_ping_view: Option, + blur_pong_texture: Option, + blur_pong_view: Option, + /// Full-resolution offscreen targets a content-filter (`filter`) group renders into before + /// being blurred and composited back. One per nesting level (indexed by depth) so nested + /// content blurs isolate correctly, up to [`MAX_FILTER_DEPTH`]; deeper nests render inline. + group_textures: Vec, + group_views: Vec, } 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; - self.scene_color_texture = None; - self.scene_color_view = None; - self.blur_ping_texture = None; - self.blur_ping_view = None; - self.blur_pong_texture = None; - self.blur_pong_view = None; - self.group_textures.clear(); - self.group_views.clear(); - } + 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; + self.scene_color_texture = None; + self.scene_color_view = None; + self.blur_ping_texture = None; + self.blur_ping_view = None; + self.blur_pong_texture = None; + self.blur_pong_view = None; + self.group_textures.clear(); + self.group_views.clear(); + } } /// Number of content-filter (`filter`) nesting levels that get their own isolated group texture. @@ -227,2450 +227,2447 @@ const MAX_FILTER_DEPTH: usize = 2; const BLUR_PARAMS_SLOTS: u64 = 256; 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, - /// Application-requested extra wgpu features/limits, stored for device recovery. - #[allow(dead_code)] - extra_requirements: Option, - resources: Option, - surface_config: wgpu::SurfaceConfiguration, - atlas: Arc, - path_globals_offset: u64, - gamma_offset: u64, - instance_buffer_capacity: u64, - max_buffer_size: u64, - storage_buffer_alignment: u64, - /// Stride between [`BlurParams`] slots in `blur_params_buffer`, and a per-frame bump cursor - /// (in slots) handed out to blur passes. Cell so the `&self` blur helpers can advance it. - blur_params_stride: u64, - blur_params_slot: std::cell::Cell, - 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, + /// 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, + /// Application-requested extra wgpu features/limits, stored for device recovery. + #[allow(dead_code)] + extra_requirements: Option, + resources: Option, + surface_config: wgpu::SurfaceConfiguration, + atlas: Arc, + path_globals_offset: u64, + gamma_offset: u64, + instance_buffer_capacity: u64, + max_buffer_size: u64, + storage_buffer_alignment: u64, + /// Stride between [`BlurParams`] slots in `blur_params_buffer`, and a per-frame bump cursor + /// (in slots) handed out to blur passes. Cell so the `&self` blur helpers can advance it. + blur_params_stride: u64, + blur_params_slot: std::cell::Cell, + 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(&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") - } + 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, - extra_requirements: 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}"))?; + /// 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, + extra_requirements: 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(), - }; + 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()))); + // 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}"))? - }; + // 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, - extra_requirements.as_ref(), - )?), - }; + 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, + extra_requirements.as_ref(), + )?), + }; - let atlas = Arc::new(WgpuAtlas::from_context(context)); + let atlas = Arc::new(WgpuAtlas::from_context(context)); - Self::new_internal( - Some(Rc::clone(&gpu_context)), - context, - surface, - config, - compositor_gpu, - extra_requirements, - atlas, - ) - } + Self::new_internal( + Some(Rc::clone(&gpu_context)), + context, + surface, + config, + compositor_gpu, + extra_requirements, + 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}"))?; + #[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}"))?; - let atlas = Arc::new(WgpuAtlas::from_context(context)); + let atlas = Arc::new(WgpuAtlas::from_context(context)); - Self::new_internal(None, context, surface, config, None, None, atlas) - } + Self::new_internal(None, context, surface, config, None, None, atlas) + } - fn new_internal( - gpu_context: Option, - context: &WgpuContext, - surface: wgpu::Surface<'static>, - config: WgpuSurfaceConfig, - compositor_gpu: Option, - extra_requirements: 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 - ) - })?; + fn new_internal( + gpu_context: Option, + context: &WgpuContext, + surface: wgpu::Surface<'static>, + config: WgpuSurfaceConfig, + compositor_gpu: Option, + extra_requirements: 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 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 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 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 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 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); + 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 {}. \ + 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 - ); - } + 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 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 dual_source_blending = context.supports_dual_source_blending(); + let queue = Arc::clone(&context.queue); + let dual_source_blending = context.supports_dual_source_blending(); - let rendering_params = RenderingParameters::new(&context.adapter, surface_format); - let bind_group_layouts = Self::create_bind_group_layouts(&device); - let pipelines = Self::create_pipelines( - &device, - &bind_group_layouts, - surface_format, - alpha_mode, - rendering_params.path_sample_count, - dual_source_blending, - ); + let rendering_params = RenderingParameters::new(&context.adapter, surface_format); + let bind_group_layouts = Self::create_bind_group_layouts(&device); + let pipelines = Self::create_pipelines( + &device, + &bind_group_layouts, + surface_format, + alpha_mode, + rendering_params.path_sample_count, + dual_source_blending, + ); - 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 atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("atlas_sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); - let surface_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("surface_sampler"), - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); + let surface_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("surface_sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); - let surface_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("surface_uniform_buffer"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + let surface_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("surface_uniform_buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64; - // Shared blur-params buffer: BLUR_PARAMS_SLOTS slots, each one alignment stride apart. - let blur_params_stride = - (std::mem::size_of::() as u64).next_multiple_of(uniform_alignment); - let blur_params_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("blur_params_buffer"), - size: blur_params_stride * BLUR_PARAMS_SLOTS, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64; + // Shared blur-params buffer: BLUR_PARAMS_SLOTS slots, each one alignment stride apart. + let blur_params_stride = + (std::mem::size_of::() as u64).next_multiple_of(uniform_alignment); + let blur_params_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("blur_params_buffer"), + size: blur_params_stride * BLUR_PARAMS_SLOTS, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - 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_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 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 max_buffer_size = device.limits().max_buffer_size; - let storage_buffer_alignment = device.limits().min_storage_buffer_offset_alignment as u64; - let initial_instance_buffer_capacity = 2 * 1024 * 1024; - let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: initial_instance_buffer_capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + let max_buffer_size = device.limits().max_buffer_size; + let storage_buffer_alignment = device.limits().min_storage_buffer_offset_alignment as u64; + let initial_instance_buffer_capacity = 2 * 1024 * 1024; + let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("instance_buffer"), + size: initial_instance_buffer_capacity, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - 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 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 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 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 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, - surface_sampler, - surface_uniform_buffer, - blur_params_buffer, - globals_buffer, - globals_bind_group, - path_globals_bind_group, - instance_buffer, - // 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, - scene_color_texture: None, - scene_color_view: None, - blur_ping_texture: None, - blur_ping_view: None, - blur_pong_texture: None, - blur_pong_view: None, - group_textures: Vec::new(), - group_views: Vec::new(), - }; + let resources = WgpuResources { + device, + queue, + surface, + pipelines, + bind_group_layouts, + atlas_sampler, + surface_sampler, + surface_uniform_buffer, + blur_params_buffer, + globals_buffer, + globals_bind_group, + path_globals_bind_group, + instance_buffer, + // 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, + scene_color_texture: None, + scene_color_view: None, + blur_ping_texture: None, + blur_ping_view: None, + blur_pong_texture: None, + blur_pong_view: None, + group_textures: Vec::new(), + group_views: Vec::new(), + }; - Ok(Self { - context: gpu_context, - compositor_gpu, - extra_requirements, - resources: Some(resources), - surface_config, - atlas, - path_globals_offset, - gamma_offset, - instance_buffer_capacity: initial_instance_buffer_capacity, - max_buffer_size, - storage_buffer_alignment, - blur_params_stride, - blur_params_slot: std::cell::Cell::new(0), - 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, - }) - } + Ok(Self { + context: gpu_context, + compositor_gpu, + extra_requirements, + resources: Some(resources), + surface_config, + atlas, + path_globals_offset, + gamma_offset, + instance_buffer_capacity: initial_instance_buffer_capacity, + max_buffer_size, + storage_buffer_alignment, + blur_params_stride, + blur_params_slot: std::cell::Cell::new(0), + 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) -> 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, - }, - ], - }); + fn create_bind_group_layouts(device: &wgpu::Device) -> 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 storage_buffer_entry = |binding: u32| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }; + let storage_buffer_entry = |binding: u32| wgpu::BindGroupLayoutEntry { + binding, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: 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: &[storage_buffer_entry(0)], - }); + let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("instances_layout"), + entries: &[storage_buffer_entry(0)], + }); - let instances_with_texture = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("instances_with_texture_layout"), - entries: &[ - storage_buffer_entry(0), - wgpu::BindGroupLayoutEntry { - binding: 1, - 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: 2, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); + let instances_with_texture = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("instances_with_texture_layout"), + entries: &[ + storage_buffer_entry(0), + wgpu::BindGroupLayoutEntry { + binding: 1, + 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: 2, + 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::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::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); - let blur = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("blur_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::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); + let blur = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("blur_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::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); - WgpuBindGroupLayouts { - globals, - instances, - instances_with_texture, - surfaces, - blur, - } - } + WgpuBindGroupLayouts { + globals, + instances, + instances_with_texture, + surfaces, + blur, + } + } - fn create_pipelines( - device: &wgpu::Device, - layouts: &WgpuBindGroupLayouts, - surface_format: wgpu::TextureFormat, - alpha_mode: wgpu::CompositeAlphaMode, - path_sample_count: u32, - dual_source_blending: 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 \ + fn create_pipelines( + device: &wgpu::Device, + layouts: &WgpuBindGroupLayouts, + surface_format: wgpu::TextureFormat, + alpha_mode: wgpu::CompositeAlphaMode, + path_sample_count: u32, + dual_source_blending: 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; + device.features(), + ); + } + let dual_source_blending = dual_source_blending && device_has_feature; - let base_shader_source = include_str!("shaders.wgsl"); - let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("gpui_shaders"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)), - }); + let base_shader_source = include_str!("shaders.wgsl"); + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpui_shaders"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)), + }); - let subpixel_shader_source = include_str!("shaders_subpixel.wgsl"); - let subpixel_shader_module = if dual_source_blending { - let combined = format!( - "enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}" - ); - Some(device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("gpui_subpixel_shaders"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)), - })) - } else { - None - }; + let subpixel_shader_source = include_str!("shaders_subpixel.wgsl"); + let subpixel_shader_module = if dual_source_blending { + let combined = format!( + "enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}" + ); + Some(device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpui_subpixel_shaders"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)), + })) + } else { + None + }; - let blend_mode = match alpha_mode { - wgpu::CompositeAlphaMode::PreMultiplied => { - wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING - } - _ => wgpu::BlendState::ALPHA_BLENDING, - }; + 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 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, - topology: wgpu::PrimitiveTopology, - color_targets: &[Option], - sample_count: u32, - module: &wgpu::ShaderModule| { - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(&format!("{name}_layout")), - bind_group_layouts: &[Some(globals_layout), Some(data_layout)], - immediate_size: 0, - }); + let create_pipeline = |name: &str, + vs_entry: &str, + fs_entry: &str, + globals_layout: &wgpu::BindGroupLayout, + data_layout: &wgpu::BindGroupLayout, + topology: wgpu::PrimitiveTopology, + color_targets: &[Option], + sample_count: u32, + module: &wgpu::ShaderModule| { + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some(&format!("{name}_layout")), + bind_group_layouts: &[Some(globals_layout), Some(data_layout)], + 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, - }) - }; + 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, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); + let quads = create_pipeline( + "quads", + "vs_quad", + "fs_quad", + &layouts.globals, + &layouts.instances, + wgpu::PrimitiveTopology::TriangleStrip, + &[Some(color_target.clone())], + 1, + &shader_module, + ); - let shadows = create_pipeline( - "shadows", - "vs_shadow", - "fs_shadow", - &layouts.globals, - &layouts.instances, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); + let shadows = create_pipeline( + "shadows", + "vs_shadow", + "fs_shadow", + &layouts.globals, + &layouts.instances, + 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, - 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 path_rasterization = create_pipeline( + "path_rasterization", + "vs_path_rasterization", + "fs_path_rasterization", + &layouts.globals, + &layouts.instances, + 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_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_with_texture, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(paths_blend), - write_mask: wgpu::ColorWrites::ALL, - })], - 1, - &shader_module, - ); + let paths = create_pipeline( + "paths", + "vs_path", + "fs_path", + &layouts.globals, + &layouts.instances_with_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, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); + let underlines = create_pipeline( + "underlines", + "vs_underline", + "fs_underline", + &layouts.globals, + &layouts.instances, + 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_with_texture, - 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_with_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, - }, - }; + 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_with_texture, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(subpixel_blend), - write_mask: wgpu::ColorWrites::COLOR, - })], - 1, - subpixel_module, - )) - } else { - None - }; + Some(create_pipeline( + "subpixel_sprites", + "vs_subpixel_sprite", + "fs_subpixel_sprite", + &layouts.globals, + &layouts.instances_with_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_with_texture, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); + let poly_sprites = create_pipeline( + "poly_sprites", + "vs_poly_sprite", + "fs_poly_sprite", + &layouts.globals, + &layouts.instances_with_texture, + wgpu::PrimitiveTopology::TriangleStrip, + &[Some(color_target.clone())], + 1, + &shader_module, + ); - let surfaces = create_pipeline( - "surfaces", - "vs_surface", - "fs_surface", - &layouts.globals, - &layouts.surfaces, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target)], - 1, - &shader_module, - ); + let surfaces = create_pipeline( + "surfaces", + "vs_surface", + "fs_surface", + &layouts.globals, + &layouts.surfaces, + wgpu::PrimitiveTopology::TriangleStrip, + &[Some(color_target)], + 1, + &shader_module, + ); - // Blur pipelines all sample one texture into another; the downsample and gaussian passes - // overwrite their (intermediate) target, while the composite blends over the scene. - let no_blend_target = wgpu::ColorTargetState { - format: surface_format, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - }; + // Blur pipelines all sample one texture into another; the downsample and gaussian passes + // overwrite their (intermediate) target, while the composite blends over the scene. + let no_blend_target = wgpu::ColorTargetState { + format: surface_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }; - let blur_downsample = create_pipeline( - "blur_downsample", - "vs_blur_fullscreen", - "fs_blur_downsample", - &layouts.globals, - &layouts.blur, - wgpu::PrimitiveTopology::TriangleList, - &[Some(no_blend_target.clone())], - 1, - &shader_module, - ); + let blur_downsample = create_pipeline( + "blur_downsample", + "vs_blur_fullscreen", + "fs_blur_downsample", + &layouts.globals, + &layouts.blur, + wgpu::PrimitiveTopology::TriangleList, + &[Some(no_blend_target.clone())], + 1, + &shader_module, + ); - let blur = create_pipeline( - "blur", - "vs_blur_fullscreen", - "fs_blur", - &layouts.globals, - &layouts.blur, - wgpu::PrimitiveTopology::TriangleList, - &[Some(no_blend_target)], - 1, - &shader_module, - ); + let blur = create_pipeline( + "blur", + "vs_blur_fullscreen", + "fs_blur", + &layouts.globals, + &layouts.blur, + wgpu::PrimitiveTopology::TriangleList, + &[Some(no_blend_target)], + 1, + &shader_module, + ); - // The blurred sample is premultiplied (blurring against the transparent, rgb=0 region - // around the source scales rgb with the fading alpha), so the composite outputs - // premultiplied and blends premultiplied — straight alpha blending would multiply rgb by - // alpha a second time and darken the faded edges. Independent of the window's alpha mode. - let premultiplied_target = wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }; - let blur_composite = create_pipeline( - "blur_composite", - "vs_blur_composite", - "fs_blur_composite", - &layouts.globals, - &layouts.blur, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(premultiplied_target)], - 1, - &shader_module, - ); + // The blurred sample is premultiplied (blurring against the transparent, rgb=0 region + // around the source scales rgb with the fading alpha), so the composite outputs + // premultiplied and blends premultiplied — straight alpha blending would multiply rgb by + // alpha a second time and darken the faded edges. Independent of the window's alpha mode. + let premultiplied_target = wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + }; + let blur_composite = create_pipeline( + "blur_composite", + "vs_blur_composite", + "fs_blur_composite", + &layouts.globals, + &layouts.blur, + wgpu::PrimitiveTopology::TriangleStrip, + &[Some(premultiplied_target)], + 1, + &shader_module, + ); - WgpuPipelines { - quads, - shadows, - path_rasterization, - paths, - underlines, - mono_sprites, - subpixel_sprites, - poly_sprites, - surfaces, - blur_downsample, - blur, - blur_composite, - } - } + WgpuPipelines { + quads, + shadows, + path_rasterization, + paths, + underlines, + mono_sprites, + subpixel_sprites, + poly_sprites, + surfaces, + blur_downsample, + blur, + blur_composite, + } + } - 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_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)) - } + 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; + 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 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 {}. \ + 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(); - - // GPU resources may not exist yet, skip rather than panicking - 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(); - } - for texture in [ - &resources.scene_color_texture, - &resources.blur_ping_texture, - &resources.blur_pong_texture, - ] - .into_iter() - .flatten() - { - texture.destroy(); - } - for texture in &resources.group_textures { - 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; - } - - /// Lazily allocate the blur offscreen targets — the full-res scene texture, half-res - /// ping/pong, and one full-res group texture per nesting level. Called only on frames that - /// actually use a blur filter, so non-blurring apps never pay this VRAM. A no-op once - /// allocated (invalidated alongside the path intermediates on resize / device loss). - fn ensure_blur_textures(&mut self) { - if self.resources().scene_color_texture.is_some() { - return; - } - let format = self.surface_config.format; - let width = self.surface_config.width; - let height = self.surface_config.height; - let blur_width = (width / 2).max(1); - let blur_height = (height / 2).max(1); - let resources = self.resources_mut(); - - let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height); - resources.scene_color_texture = Some(t); - resources.scene_color_view = Some(v); - let (t, v) = - Self::create_path_intermediate(&resources.device, format, blur_width, blur_height); - resources.blur_ping_texture = Some(t); - resources.blur_ping_view = Some(v); - let (t, v) = - Self::create_path_intermediate(&resources.device, format, blur_width, blur_height); - resources.blur_pong_texture = Some(t); - resources.blur_pong_view = Some(v); - - for _ in 0..MAX_FILTER_DEPTH { - let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height); - resources.group_textures.push(t); - resources.group_views.push(v); - } - } - - 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 resources = self.resources_mut(); - 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, - ); - } - } - - #[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_context(&self) -> (Arc, Arc) { - let resources = self.resources(); - (resources.device.clone(), resources.queue.clone()) - } - - 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 { - // 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(); - - // Blur is the only thing that needs the offscreen scene texture; allocate it (and the - // ping/pong/group targets) lazily so non-blurring apps pay no extra VRAM or blit. - let use_offscreen = - !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); - if use_offscreen { - self.ensure_blur_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), - ); - } - - loop { - let mut instance_offset: u64 = 0; - // Reset the blur-params bump cursor each (re)render of the scene. - self.blur_params_slot.set(0); - let mut overflow = false; - - let mut encoder = - self.resources() - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("main_encoder"), - }); - - // When the scene contains blur filters, render into the offscreen scene texture (so - // filters can sample already-painted content mid-frame) and blit to the swapchain at - // the end; otherwise render straight to the swapchain. `use_offscreen` and the blur - // textures were computed/allocated above. - let scene_color_view = if use_offscreen { - Some( - self.resources() - .scene_color_view - .as_ref() - .expect("scene_color_view allocated by ensure_blur_textures") - .clone(), - ) - } else { - None - }; - // The active render target. While inside a content-filter (`filter`) group it points - // at a group texture so the group renders in isolation. - let mut current_target = match &scene_color_view { - Some(view) => view.clone(), - None => frame_view.clone(), - }; - // One group texture per nesting depth; empty when not blurring. - let group_views = if use_offscreen { - self.resources().group_views.clone() - } else { - Vec::new() - }; - // (boundary, parent target to composite back into, whether this level is isolated). - let mut filter_stack: Vec<(FilterBoundary, wgpu::TextureView, bool)> = Vec::new(); - - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: ¤t_target, - 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() - }); - - for batch in scene.batches() { - let ok = match batch { - PrimitiveBatch::Quads(range) => { - self.draw_quads(&scene.quads[range], &mut instance_offset, &mut pass) - } - PrimitiveBatch::Shadows(range) => self.draw_shadows( - &scene.shadows[range], - &mut instance_offset, - &mut pass, - ), - PrimitiveBatch::Paths(range) => { - let paths = &scene.paths[range]; - if paths.is_empty() { - continue; - } - - drop(pass); - - let did_draw = self.draw_paths_to_intermediate( - &mut encoder, - paths, - &mut instance_offset, - ); - - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass_continued"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: ¤t_target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - if did_draw { - self.draw_paths_from_intermediate( - paths, - &mut instance_offset, - &mut pass, - ) - } else { - false - } - } - PrimitiveBatch::Underlines(range) => self.draw_underlines( - &scene.underlines[range], - &mut instance_offset, - &mut pass, - ), - PrimitiveBatch::MonochromeSprites { texture_id, range } => self - .draw_monochrome_sprites( - &scene.monochrome_sprites[range], - texture_id, - &mut instance_offset, - &mut pass, - ), - PrimitiveBatch::SubpixelSprites { texture_id, range } => self - .draw_subpixel_sprites( - &scene.subpixel_sprites[range], - texture_id, - &mut instance_offset, - &mut pass, - ), - PrimitiveBatch::PolychromeSprites { texture_id, range } => self - .draw_polychrome_sprites( - &scene.polychrome_sprites[range], - texture_id, - &mut instance_offset, - &mut pass, - ), - PrimitiveBatch::Surfaces(range) => { - self.draw_surfaces(&scene.surfaces[range], &mut pass) - } - PrimitiveBatch::BackdropFilters(range) => { - // Interrupt the current pass, blur the content painted so far behind - // each backdrop's rounded rect, then resume drawing on top. - drop(pass); - for filter in &scene.backdrop_filters[range] { - self.draw_backdrop_filter(&mut encoder, filter, ¤t_target); - } - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass_continued"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: ¤t_target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - true - } - PrimitiveBatch::FilterBoundary(ix) => { - let boundary = scene.filter_boundaries[ix].clone(); - if boundary.is_start { - // Each isolated nesting level uses its own group texture from the - // pool (indexed by current isolation depth). Beyond the pool size - // (MAX_FILTER_DEPTH) deeper filters render inline without isolation - // rather than corrupting an outer group. - let depth = filter_stack.iter().filter(|entry| entry.2).count(); - if depth < group_views.len() { - drop(pass); - let parent = current_target.clone(); - current_target = group_views[depth].clone(); - filter_stack.push((boundary, parent, true)); - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("filter_group"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: ¤t_target, - 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() - }); - } else { - filter_stack.push((boundary, current_target.clone(), false)); - } - } else if let Some((boundary, parent, isolated)) = filter_stack.pop() { - if isolated { - drop(pass); - self.blur_and_composite( - &mut encoder, - ¤t_target, - &parent, - boundary.bounds, - boundary.content_mask.bounds, - [ - boundary.corner_radii.top_left.0, - boundary.corner_radii.top_right.0, - boundary.corner_radii.bottom_right.0, - boundary.corner_radii.bottom_left.0, - ], - max_blur_radius(&boundary.filters), - boundary.opacity, - false, - ); - current_target = parent; - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass_continued"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: ¤t_target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - }, - )], - depth_stencil_attachment: None, - ..Default::default() - }); - } - } - true - } - }; - if !ok { - overflow = true; - break; - } - } - } - - if overflow { - drop(encoder); - if self.instance_buffer_capacity >= self.max_buffer_size { - log::error!( - "instance buffer size grew too large: {}", - self.instance_buffer_capacity - ); - frame.present(); - return true; - } - self.grow_instance_buffer(); - continue; - } - - // Present the offscreen scene by copying it into the swapchain texture. Skipped when - // rendering went straight to the swapchain (no filters this frame). - if let Some(scene_color_view) = &scene_color_view { - self.blit_to_frame(&mut encoder, scene_color_view, &frame_view); - } - - self.resources() - .queue - .submit(std::iter::once(encoder.finish())); - frame.present(); - return true; - } - } - - fn draw_quads( - &self, - quads: &[Quad], - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let data = unsafe { Self::instance_bytes(quads) }; - self.draw_instances( - data, - quads.len() as u32, - &self.resources().pipelines.quads, - instance_offset, - pass, - ) - } - - fn draw_shadows( - &self, - shadows: &[Shadow], - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let data = unsafe { Self::instance_bytes(shadows) }; - self.draw_instances( - data, - shadows.len() as u32, - &self.resources().pipelines.shadows, - instance_offset, - pass, - ) - } - - fn draw_underlines( - &self, - underlines: &[Underline], - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let data = unsafe { Self::instance_bytes(underlines) }; - self.draw_instances( - data, - underlines.len() as u32, - &self.resources().pipelines.underlines, - instance_offset, - pass, - ) - } - - fn draw_monochrome_sprites( - &self, - sprites: &[MonochromeSprite], - texture_id: AtlasTextureId, - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); - let data = unsafe { Self::instance_bytes(sprites) }; - self.draw_instances_with_texture( - data, - sprites.len() as u32, - &tex_info.view, - &self.resources().pipelines.mono_sprites, - instance_offset, - pass, - ) - } - - fn draw_subpixel_sprites( - &self, - sprites: &[SubpixelSprite], - texture_id: AtlasTextureId, - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); - let data = unsafe { Self::instance_bytes(sprites) }; - let resources = self.resources(); - let pipeline = resources - .pipelines - .subpixel_sprites - .as_ref() - .unwrap_or(&resources.pipelines.mono_sprites); - self.draw_instances_with_texture( - data, - sprites.len() as u32, - &tex_info.view, - pipeline, - instance_offset, - pass, - ) - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn draw_surfaces(&self, surfaces: &[PaintSurface], pass: &mut wgpu::RenderPass<'_>) -> bool { - let resources = self.resources(); - for surface in surfaces { - let Some(wgpu_texture) = surface.texture.downcast_ref::() else { - continue; - }; - - let texture_view = wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let params = SurfaceParams { - bounds: surface.bounds.into(), - content_mask: surface.content_mask.bounds.into(), - }; - - resources.queue.write_buffer( - &resources.surface_uniform_buffer, - 0, - bytemuck::bytes_of(¶ms), - ); - - let bind_group = resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("surface_bind_group"), - layout: &resources.bind_group_layouts.surfaces, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: resources.surface_uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&texture_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&resources.surface_sampler), - }, - ], - }); - - pass.set_pipeline(&resources.pipelines.surfaces); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.draw(0..4, 0..1); - } - true - } - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - fn draw_surfaces(&self, _surfaces: &[PaintSurface], _pass: &mut wgpu::RenderPass<'_>) -> bool { - true - } - - /// Build a bind group for a blur pass. Writes `params` into the next slot of the shared - /// `blur_params_buffer` (no per-pass allocation) and references that slot, the source texture, - /// and the filtering sampler. Distinct per-pass offsets keep `write_buffer`'s - /// last-write-at-submit semantics from clobbering earlier passes within a frame. - fn make_blur_bind_group( - &self, - params: BlurParams, - source: &wgpu::TextureView, - ) -> wgpu::BindGroup { - let resources = self.resources(); - let slot = self.blur_params_slot.get() % BLUR_PARAMS_SLOTS; - self.blur_params_slot.set(slot + 1); - let offset = slot * self.blur_params_stride; - resources.queue.write_buffer( - &resources.blur_params_buffer, - offset, - bytemuck::bytes_of(¶ms), - ); - resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("blur_bind_group"), - layout: &resources.bind_group_layouts.blur, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &resources.blur_params_buffer, - offset, - size: NonZeroU64::new(std::mem::size_of::() as u64), - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(source), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&resources.surface_sampler), - }, - ], - }) - } - - /// Run a full-screen (3-vertex) blur pass that overwrites `target` by sampling `source`. - /// `scissor` (x, y, w, h, in `target` pixels) limits fragment work to the region that - /// actually feeds the composite — the element bounds dilated by the kernel radius. - fn run_blur_pass( - &self, - encoder: &mut wgpu::CommandEncoder, - label: &str, - pipeline: &wgpu::RenderPipeline, - target: &wgpu::TextureView, - source: &wgpu::TextureView, - params: BlurParams, - scissor: [u32; 4], - ) { - let bind_group = self.make_blur_bind_group(params, source); - let resources = self.resources(); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(label), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - 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_pipeline(pipeline); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); - pass.draw(0..3, 0..1); - } - - /// Blur `source` (full-resolution) and composite the result into `target`, clipped to - /// `bounds`/`corner_radii`/`content_mask` and modulated by `opacity`. Shared by the backdrop - /// and content-filter paths. Uses the half-resolution ping/pong textures as scratch. - #[allow(clippy::too_many_arguments)] - fn blur_and_composite( - &self, - encoder: &mut wgpu::CommandEncoder, - source: &wgpu::TextureView, - target: &wgpu::TextureView, - bounds: Bounds, - content_mask: Bounds, - corner_radii: [f32; 4], - blur_radius: f32, - opacity: f32, - // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. - clip_rounded: bool, - ) { - // Sigma is halved because the blur runs at half resolution. - let sigma = (blur_radius * 0.5).max(0.0); - if sigma <= 0.0 { - return; - } - // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather - // than truncating the kernel — keeps very large radii from clipping (review #6). - let ideal_taps = (3.0 * sigma).ceil(); - let tap_count = ideal_taps.clamp(1.0, 32.0); - let tap_step = (ideal_taps / tap_count).max(1.0); - let full_w = self.surface_config.width; - let full_h = self.surface_config.height; - let blur_width = (full_w / 2).max(1) as f32; - let blur_height = (full_h / 2).max(1) as f32; - - // Limit the half-res passes to the element bounds dilated by the kernel radius (3·sigma, - // full-res) — outside that the composite never samples, so there's no reason to blur it. - let dilation = 3.0 * blur_radius; - let hw = (full_w / 2).max(1); - let hh = (full_h / 2).max(1); - let x0 = (((bounds.origin.x.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hw); - let y0 = (((bounds.origin.y.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hh); - let x1 = ((((bounds.origin.x.0 + bounds.size.width.0 + dilation) * 0.5) - .ceil() - .max(0.0) as u32) - .min(hw)) - .max(x0); - let y1 = ((((bounds.origin.y.0 + bounds.size.height.0 + dilation) * 0.5) - .ceil() - .max(0.0) as u32) - .min(hh)) - .max(y0); - let scissor = [x0, y0, x1 - x0, y1 - y0]; - if scissor[2] == 0 || scissor[3] == 0 { - return; - } - - // Owned handles so the passes below don't borrow `self`. - let (ping, pong) = { - let resources = self.resources(); - match ( - resources.blur_ping_view.as_ref(), - resources.blur_pong_view.as_ref(), - ) { - (Some(ping), Some(pong)) => (ping.clone(), pong.clone()), - _ => return, - } - }; - - // Downsample source -> ping, then separable gaussian ping -> pong -> ping. - self.run_blur_pass( - encoder, - "blur_downsample", - &self.resources().pipelines.blur_downsample, - &ping, - source, - BlurParams { - downsample: 1.0, - ..Default::default() - }, - scissor, - ); - self.run_blur_pass( - encoder, - "blur_horizontal", - &self.resources().pipelines.blur, - &pong, - &ping, - BlurParams { - direction: [1.0 / blur_width, 0.0], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - scissor, - ); - self.run_blur_pass( - encoder, - "blur_vertical", - &self.resources().pipelines.blur, - &ping, - &pong, - BlurParams { - direction: [0.0, 1.0 / blur_height], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - scissor, - ); - - // Composite the blurred result into the target (loads existing content). For content blur - // the quad covers the dilated region so the blur can fade out past the element box (no - // sharp clip); for backdrop the quad is the element bounds and the shader clips to the - // rounded rect. - let composite_bounds = if clip_rounded { - bounds - } else { - bounds.dilate(ScaledPixels(dilation)) - }; - let params = BlurParams { - bounds: composite_bounds.into(), - content_mask: content_mask.into(), - corner_radii, - opacity, - clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, - ..Default::default() - }; - let bind_group = self.make_blur_bind_group(params, &ping); - let resources = self.resources(); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("blur_composite"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - 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_pipeline(&resources.pipelines.blur_composite); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.draw(0..4, 0..1); - } - - /// Blur the scene painted so far behind `filter.bounds` and composite it back as frosted glass. - fn draw_backdrop_filter( - &self, - encoder: &mut wgpu::CommandEncoder, - filter: &BackdropFilter, - scene_color_view: &wgpu::TextureView, - ) { - self.blur_and_composite( - encoder, - scene_color_view, - scene_color_view, - filter.bounds, - filter.content_mask.bounds, - [ - filter.corner_radii.top_left.0, - filter.corner_radii.top_right.0, - filter.corner_radii.bottom_right.0, - filter.corner_radii.bottom_left.0, - ], - max_blur_radius(&filter.filters), - filter.opacity, - true, - ); - } - - /// Copy the offscreen scene texture into the swapchain texture. - fn blit_to_frame( - &self, - encoder: &mut wgpu::CommandEncoder, - source: &wgpu::TextureView, - frame_view: &wgpu::TextureView, - ) { - let bind_group = self.make_blur_bind_group(BlurParams::default(), source); - let resources = self.resources(); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("scene_blit"), - 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_pipeline(&resources.pipelines.blur_downsample); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.draw(0..3, 0..1); - } - - fn draw_polychrome_sprites( - &self, - sprites: &[PolychromeSprite], - texture_id: AtlasTextureId, - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); - let data = unsafe { Self::instance_bytes(sprites) }; - self.draw_instances_with_texture( - data, - sprites.len() as u32, - &tex_info.view, - &self.resources().pipelines.poly_sprites, - instance_offset, - pass, - ) - } - - fn draw_instances( - &self, - data: &[u8], - instance_count: u32, - pipeline: &wgpu::RenderPipeline, - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - if instance_count == 0 { - return true; - } - let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else { - return false; - }; - let resources = self.resources(); - let bind_group = resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &resources.bind_group_layouts.instances, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: self.instance_binding(offset, size), - }], - }); - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.draw(0..4, 0..instance_count); - true - } - - fn draw_instances_with_texture( - &self, - data: &[u8], - instance_count: u32, - texture_view: &wgpu::TextureView, - pipeline: &wgpu::RenderPipeline, - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - if instance_count == 0 { - return true; - } - let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else { - return false; - }; - let resources = self.resources(); - let bind_group = resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &resources.bind_group_layouts.instances_with_texture, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: self.instance_binding(offset, size), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(texture_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler), - }, - ], - }); - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &bind_group, &[]); - pass.draw(0..4, 0..instance_count); - true - } - - 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( - &self, - paths: &[Path], - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> bool { - 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 resources = self.resources(); - let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { - return true; - }; - - let sprite_data = unsafe { Self::instance_bytes(&sprites) }; - self.draw_instances_with_texture( - sprite_data, - sprites.len() as u32, - path_intermediate_view, - &resources.pipelines.paths, - instance_offset, - pass, - ) - } - - fn draw_paths_to_intermediate( - &self, - encoder: &mut wgpu::CommandEncoder, - paths: &[Path], - instance_offset: &mut u64, - ) -> bool { - 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, - })); - } - - if vertices.is_empty() { - return true; - } - - let vertex_data = unsafe { Self::instance_bytes(&vertices) }; - let Some((vertex_offset, vertex_size)) = - self.write_to_instance_buffer(instance_offset, vertex_data) - else { - return false; - }; - - let resources = self.resources(); - let data_bind_group = resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("path_rasterization_bind_group"), - layout: &resources.bind_group_layouts.instances, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: self.instance_binding(vertex_offset, vertex_size), - }], - }); - - let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { - return true; - }; - - 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_pipeline(&resources.pipelines.path_rasterization); - pass.set_bind_group(0, &resources.path_globals_bind_group, &[]); - pass.set_bind_group(1, &data_bind_group, &[]); - pass.draw(0..vertices.len() as u32, 0..1); - } - - true - } - - fn grow_instance_buffer(&mut self) { - let new_capacity = (self.instance_buffer_capacity * 2).min(self.max_buffer_size); - log::info!("increased instance buffer size to {}", new_capacity); - let resources = self.resources_mut(); - resources.instance_buffer = resources.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: new_capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - self.instance_buffer_capacity = new_capacity; - } - - fn write_to_instance_buffer( - &self, - instance_offset: &mut u64, - data: &[u8], - ) -> Option<(u64, NonZeroU64)> { - let offset = (*instance_offset).next_multiple_of(self.storage_buffer_alignment); - let size = (data.len() as u64).max(16); - if offset + size > self.instance_buffer_capacity { - return None; - } - let resources = self.resources(); - resources - .queue - .write_buffer(&resources.instance_buffer, offset, data); - *instance_offset = offset + size; - Some((offset, NonZeroU64::new(size).expect("size is at least 16"))) - } - - fn instance_binding(&self, offset: u64, size: NonZeroU64) -> wgpu::BindingResource<'_> { - wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &self.resources().instance_buffer, - offset, - size: Some(size), - }) - } - - /// 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, - self.extra_requirements.as_ref(), - )?; - *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); - - let extra_reqs = self.extra_requirements.clone(); - *self = Self::new_internal( - Some(gpu_context.clone()), - context, - surface, - config, - self.compositor_gpu, - extra_reqs, - self.atlas.clone(), - )?; - - log::info!("GPU recovery complete"); - Ok(()) - } + 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(); + + // GPU resources may not exist yet, skip rather than panicking + 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(); + } + for texture in [ + &resources.scene_color_texture, + &resources.blur_ping_texture, + &resources.blur_pong_texture, + ] + .into_iter() + .flatten() + { + texture.destroy(); + } + for texture in &resources.group_textures { + 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; + } + + /// Lazily allocate the blur offscreen targets — the full-res scene texture, half-res + /// ping/pong, and one full-res group texture per nesting level. Called only on frames that + /// actually use a blur filter, so non-blurring apps never pay this VRAM. A no-op once + /// allocated (invalidated alongside the path intermediates on resize / device loss). + fn ensure_blur_textures(&mut self) { + if self.resources().scene_color_texture.is_some() { + return; + } + let format = self.surface_config.format; + let width = self.surface_config.width; + let height = self.surface_config.height; + let blur_width = (width / 2).max(1); + let blur_height = (height / 2).max(1); + let resources = self.resources_mut(); + + let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height); + resources.scene_color_texture = Some(t); + resources.scene_color_view = Some(v); + let (t, v) = + Self::create_path_intermediate(&resources.device, format, blur_width, blur_height); + resources.blur_ping_texture = Some(t); + resources.blur_ping_view = Some(v); + let (t, v) = + Self::create_path_intermediate(&resources.device, format, blur_width, blur_height); + resources.blur_pong_texture = Some(t); + resources.blur_pong_view = Some(v); + + for _ in 0..MAX_FILTER_DEPTH { + let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height); + resources.group_textures.push(t); + resources.group_views.push(v); + } + } + + 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 resources = self.resources_mut(); + 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, + ); + } + } + + #[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_context(&self) -> (Arc, Arc) { + let resources = self.resources(); + (resources.device.clone(), resources.queue.clone()) + } + + 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 { + // 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(); + + // Blur is the only thing that needs the offscreen scene texture; allocate it (and the + // ping/pong/group targets) lazily so non-blurring apps pay no extra VRAM or blit. + let use_offscreen = + !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); + if use_offscreen { + self.ensure_blur_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), + ); + } + + loop { + let mut instance_offset: u64 = 0; + // Reset the blur-params bump cursor each (re)render of the scene. + self.blur_params_slot.set(0); + let mut overflow = false; + + let mut encoder = + self.resources() + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("main_encoder"), + }); + + // When the scene contains blur filters, render into the offscreen scene texture (so + // filters can sample already-painted content mid-frame) and blit to the swapchain at + // the end; otherwise render straight to the swapchain. `use_offscreen` and the blur + // textures were computed/allocated above. + let scene_color_view = if use_offscreen { + Some( + self.resources() + .scene_color_view + .as_ref() + .expect("scene_color_view allocated by ensure_blur_textures") + .clone(), + ) + } else { + None + }; + // The active render target. While inside a content-filter (`filter`) group it points + // at a group texture so the group renders in isolation. + let mut current_target = match &scene_color_view { + Some(view) => view.clone(), + None => frame_view.clone(), + }; + // One group texture per nesting depth; empty when not blurring. + let group_views = if use_offscreen { + self.resources().group_views.clone() + } else { + Vec::new() + }; + // (boundary, parent target to composite back into, whether this level is isolated). + let mut filter_stack: Vec<(FilterBoundary, wgpu::TextureView, bool)> = Vec::new(); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: ¤t_target, + 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() + }); + + for batch in scene.batches() { + let ok = match batch { + PrimitiveBatch::Quads(range) => { + self.draw_quads(&scene.quads[range], &mut instance_offset, &mut pass) + } + PrimitiveBatch::Shadows(range) => self.draw_shadows( + &scene.shadows[range], + &mut instance_offset, + &mut pass, + ), + PrimitiveBatch::Paths(range) => { + let paths = &scene.paths[range]; + if paths.is_empty() { + continue; + } + + drop(pass); + + let did_draw = self.draw_paths_to_intermediate( + &mut encoder, + paths, + &mut instance_offset, + ); + + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass_continued"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: ¤t_target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + + if did_draw { + self.draw_paths_from_intermediate( + paths, + &mut instance_offset, + &mut pass, + ) + } else { + false + } + } + PrimitiveBatch::Underlines(range) => self.draw_underlines( + &scene.underlines[range], + &mut instance_offset, + &mut pass, + ), + PrimitiveBatch::MonochromeSprites { texture_id, range } => self + .draw_monochrome_sprites( + &scene.monochrome_sprites[range], + texture_id, + &mut instance_offset, + &mut pass, + ), + PrimitiveBatch::SubpixelSprites { texture_id, range } => self + .draw_subpixel_sprites( + &scene.subpixel_sprites[range], + texture_id, + &mut instance_offset, + &mut pass, + ), + PrimitiveBatch::PolychromeSprites { texture_id, range } => self + .draw_polychrome_sprites( + &scene.polychrome_sprites[range], + texture_id, + &mut instance_offset, + &mut pass, + ), + PrimitiveBatch::Surfaces(range) => { + self.draw_surfaces(&scene.surfaces[range], &mut pass) + } + PrimitiveBatch::BackdropFilters(range) => { + // Interrupt the current pass, blur the content painted so far behind + // each backdrop's rounded rect, then resume drawing on top. + drop(pass); + for filter in &scene.backdrop_filters[range] { + self.draw_backdrop_filter(&mut encoder, filter, ¤t_target); + } + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass_continued"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: ¤t_target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + true + } + PrimitiveBatch::FilterBoundary(ix) => { + let boundary = scene.filter_boundaries[ix].clone(); + if boundary.is_start { + // Each isolated nesting level uses its own group texture from the + // pool (indexed by current isolation depth). Beyond the pool size + // (MAX_FILTER_DEPTH) deeper filters render inline without isolation + // rather than corrupting an outer group. + let depth = filter_stack.iter().filter(|entry| entry.2).count(); + if depth < group_views.len() { + drop(pass); + let parent = current_target.clone(); + current_target = group_views[depth].clone(); + filter_stack.push((boundary, parent, true)); + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("filter_group"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: ¤t_target, + 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() + }); + } else { + filter_stack.push((boundary, current_target.clone(), false)); + } + } else if let Some((boundary, parent, isolated)) = filter_stack.pop() { + if isolated { + drop(pass); + self.blur_and_composite( + &mut encoder, + ¤t_target, + &parent, + boundary.bounds, + boundary.content_mask.bounds, + [ + boundary.corner_radii.top_left.0, + boundary.corner_radii.top_right.0, + boundary.corner_radii.bottom_right.0, + boundary.corner_radii.bottom_left.0, + ], + max_blur_radius(&boundary.filters), + boundary.opacity, + false, + ); + current_target = parent; + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass_continued"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: ¤t_target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + }, + )], + depth_stencil_attachment: None, + ..Default::default() + }); + } + } + true + } + }; + if !ok { + overflow = true; + break; + } + } + } + + if overflow { + drop(encoder); + if self.instance_buffer_capacity >= self.max_buffer_size { + log::error!( + "instance buffer size grew too large: {}", + self.instance_buffer_capacity + ); + frame.present(); + return true; + } + self.grow_instance_buffer(); + continue; + } + + // Present the offscreen scene by copying it into the swapchain texture. Skipped when + // rendering went straight to the swapchain (no filters this frame). + if let Some(scene_color_view) = &scene_color_view { + self.blit_to_frame(&mut encoder, scene_color_view, &frame_view); + } + + self.resources() + .queue + .submit(std::iter::once(encoder.finish())); + frame.present(); + return true; + } + } + + fn draw_quads( + &self, + quads: &[Quad], + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let data = unsafe { Self::instance_bytes(quads) }; + self.draw_instances( + data, + quads.len() as u32, + &self.resources().pipelines.quads, + instance_offset, + pass, + ) + } + + fn draw_shadows( + &self, + shadows: &[Shadow], + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let data = unsafe { Self::instance_bytes(shadows) }; + self.draw_instances( + data, + shadows.len() as u32, + &self.resources().pipelines.shadows, + instance_offset, + pass, + ) + } + + fn draw_underlines( + &self, + underlines: &[Underline], + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let data = unsafe { Self::instance_bytes(underlines) }; + self.draw_instances( + data, + underlines.len() as u32, + &self.resources().pipelines.underlines, + instance_offset, + pass, + ) + } + + fn draw_monochrome_sprites( + &self, + sprites: &[MonochromeSprite], + texture_id: AtlasTextureId, + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let tex_info = self.atlas.get_texture_info(texture_id); + let data = unsafe { Self::instance_bytes(sprites) }; + self.draw_instances_with_texture( + data, + sprites.len() as u32, + &tex_info.view, + &self.resources().pipelines.mono_sprites, + instance_offset, + pass, + ) + } + + fn draw_subpixel_sprites( + &self, + sprites: &[SubpixelSprite], + texture_id: AtlasTextureId, + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let tex_info = self.atlas.get_texture_info(texture_id); + let data = unsafe { Self::instance_bytes(sprites) }; + let resources = self.resources(); + let pipeline = resources + .pipelines + .subpixel_sprites + .as_ref() + .unwrap_or(&resources.pipelines.mono_sprites); + self.draw_instances_with_texture( + data, + sprites.len() as u32, + &tex_info.view, + pipeline, + instance_offset, + pass, + ) + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn draw_surfaces(&self, surfaces: &[PaintSurface], pass: &mut wgpu::RenderPass<'_>) -> bool { + let resources = self.resources(); + for surface in surfaces { + let Some(wgpu_texture) = surface.texture.downcast_ref::() else { + continue; + }; + + let texture_view = wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let params = SurfaceParams { + bounds: surface.bounds.into(), + content_mask: surface.content_mask.bounds.into(), + }; + + resources.queue.write_buffer( + &resources.surface_uniform_buffer, + 0, + bytemuck::bytes_of(¶ms), + ); + + let bind_group = resources + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("surface_bind_group"), + layout: &resources.bind_group_layouts.surfaces, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: resources.surface_uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&resources.surface_sampler), + }, + ], + }); + + pass.set_pipeline(&resources.pipelines.surfaces); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.draw(0..4, 0..1); + } + true + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + fn draw_surfaces(&self, _surfaces: &[PaintSurface], _pass: &mut wgpu::RenderPass<'_>) -> bool { + true + } + + /// Build a bind group for a blur pass. Writes `params` into the next slot of the shared + /// `blur_params_buffer` (no per-pass allocation) and references that slot, the source texture, + /// and the filtering sampler. Distinct per-pass offsets keep `write_buffer`'s + /// last-write-at-submit semantics from clobbering earlier passes within a frame. + fn make_blur_bind_group( + &self, + params: BlurParams, + source: &wgpu::TextureView, + ) -> wgpu::BindGroup { + let resources = self.resources(); + let slot = self.blur_params_slot.get() % BLUR_PARAMS_SLOTS; + self.blur_params_slot.set(slot + 1); + let offset = slot * self.blur_params_stride; + resources.queue.write_buffer( + &resources.blur_params_buffer, + offset, + bytemuck::bytes_of(¶ms), + ); + resources + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("blur_bind_group"), + layout: &resources.bind_group_layouts.blur, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &resources.blur_params_buffer, + offset, + size: NonZeroU64::new(std::mem::size_of::() as u64), + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(source), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&resources.surface_sampler), + }, + ], + }) + } + + /// Run a full-screen (3-vertex) blur pass that overwrites `target` by sampling `source`. + /// `scissor` (x, y, w, h, in `target` pixels) limits fragment work to the region that + /// actually feeds the composite — the element bounds dilated by the kernel radius. + fn run_blur_pass( + &self, + encoder: &mut wgpu::CommandEncoder, + label: &str, + pipeline: &wgpu::RenderPipeline, + target: &wgpu::TextureView, + source: &wgpu::TextureView, + params: BlurParams, + scissor: [u32; 4], + ) { + let bind_group = self.make_blur_bind_group(params, source); + let resources = self.resources(); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + 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_pipeline(pipeline); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); + pass.draw(0..3, 0..1); + } + + /// Blur `source` (full-resolution) and composite the result into `target`, clipped to + /// `bounds`/`corner_radii`/`content_mask` and modulated by `opacity`. Shared by the backdrop + /// and content-filter paths. Uses the half-resolution ping/pong textures as scratch. + #[allow(clippy::too_many_arguments)] + fn blur_and_composite( + &self, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::TextureView, + target: &wgpu::TextureView, + bounds: Bounds, + content_mask: Bounds, + corner_radii: [f32; 4], + blur_radius: f32, + opacity: f32, + // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. + clip_rounded: bool, + ) { + // Sigma is halved because the blur runs at half resolution. + let sigma = (blur_radius * 0.5).max(0.0); + if sigma <= 0.0 { + return; + } + // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather + // than truncating the kernel — keeps very large radii from clipping (review #6). + let ideal_taps = (3.0 * sigma).ceil(); + let tap_count = ideal_taps.clamp(1.0, 32.0); + let tap_step = (ideal_taps / tap_count).max(1.0); + let full_w = self.surface_config.width; + let full_h = self.surface_config.height; + let blur_width = (full_w / 2).max(1) as f32; + let blur_height = (full_h / 2).max(1) as f32; + + // Limit the half-res passes to the element bounds dilated by the kernel radius (3·sigma, + // full-res) — outside that the composite never samples, so there's no reason to blur it. + let dilation = 3.0 * blur_radius; + let hw = (full_w / 2).max(1); + let hh = (full_h / 2).max(1); + let x0 = (((bounds.origin.x.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hw); + let y0 = (((bounds.origin.y.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hh); + let x1 = ((((bounds.origin.x.0 + bounds.size.width.0 + dilation) * 0.5) + .ceil() + .max(0.0) as u32) + .min(hw)) + .max(x0); + let y1 = ((((bounds.origin.y.0 + bounds.size.height.0 + dilation) * 0.5) + .ceil() + .max(0.0) as u32) + .min(hh)) + .max(y0); + let scissor = [x0, y0, x1 - x0, y1 - y0]; + if scissor[2] == 0 || scissor[3] == 0 { + return; + } + + // Owned handles so the passes below don't borrow `self`. + let (ping, pong) = { + let resources = self.resources(); + match ( + resources.blur_ping_view.as_ref(), + resources.blur_pong_view.as_ref(), + ) { + (Some(ping), Some(pong)) => (ping.clone(), pong.clone()), + _ => return, + } + }; + + // Downsample source -> ping, then separable gaussian ping -> pong -> ping. + self.run_blur_pass( + encoder, + "blur_downsample", + &self.resources().pipelines.blur_downsample, + &ping, + source, + BlurParams { + downsample: 1.0, + ..Default::default() + }, + scissor, + ); + self.run_blur_pass( + encoder, + "blur_horizontal", + &self.resources().pipelines.blur, + &pong, + &ping, + BlurParams { + direction: [1.0 / blur_width, 0.0], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + scissor, + ); + self.run_blur_pass( + encoder, + "blur_vertical", + &self.resources().pipelines.blur, + &ping, + &pong, + BlurParams { + direction: [0.0, 1.0 / blur_height], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + scissor, + ); + + // Composite the blurred result into the target (loads existing content). For content blur + // the quad covers the dilated region so the blur can fade out past the element box (no + // sharp clip); for backdrop the quad is the element bounds and the shader clips to the + // rounded rect. + let composite_bounds = if clip_rounded { + bounds + } else { + bounds.dilate(ScaledPixels(dilation)) + }; + let params = BlurParams { + bounds: composite_bounds.into(), + content_mask: content_mask.into(), + corner_radii, + opacity, + clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, + ..Default::default() + }; + let bind_group = self.make_blur_bind_group(params, &ping); + let resources = self.resources(); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("blur_composite"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + 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_pipeline(&resources.pipelines.blur_composite); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.draw(0..4, 0..1); + } + + /// Blur the scene painted so far behind `filter.bounds` and composite it back as frosted glass. + fn draw_backdrop_filter( + &self, + encoder: &mut wgpu::CommandEncoder, + filter: &BackdropFilter, + scene_color_view: &wgpu::TextureView, + ) { + self.blur_and_composite( + encoder, + scene_color_view, + scene_color_view, + filter.bounds, + filter.content_mask.bounds, + [ + filter.corner_radii.top_left.0, + filter.corner_radii.top_right.0, + filter.corner_radii.bottom_right.0, + filter.corner_radii.bottom_left.0, + ], + max_blur_radius(&filter.filters), + filter.opacity, + true, + ); + } + + /// Copy the offscreen scene texture into the swapchain texture. + fn blit_to_frame( + &self, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::TextureView, + frame_view: &wgpu::TextureView, + ) { + let bind_group = self.make_blur_bind_group(BlurParams::default(), source); + let resources = self.resources(); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("scene_blit"), + 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_pipeline(&resources.pipelines.blur_downsample); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.draw(0..3, 0..1); + } + + fn draw_polychrome_sprites( + &self, + sprites: &[PolychromeSprite], + texture_id: AtlasTextureId, + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let tex_info = self.atlas.get_texture_info(texture_id); + let data = unsafe { Self::instance_bytes(sprites) }; + self.draw_instances_with_texture( + data, + sprites.len() as u32, + &tex_info.view, + &self.resources().pipelines.poly_sprites, + instance_offset, + pass, + ) + } + + fn draw_instances( + &self, + data: &[u8], + instance_count: u32, + pipeline: &wgpu::RenderPipeline, + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + if instance_count == 0 { + return true; + } + let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else { + return false; + }; + let resources = self.resources(); + let bind_group = resources + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &resources.bind_group_layouts.instances, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: self.instance_binding(offset, size), + }], + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.draw(0..4, 0..instance_count); + true + } + + fn draw_instances_with_texture( + &self, + data: &[u8], + instance_count: u32, + texture_view: &wgpu::TextureView, + pipeline: &wgpu::RenderPipeline, + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + if instance_count == 0 { + return true; + } + let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else { + return false; + }; + let resources = self.resources(); + let bind_group = resources + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &resources.bind_group_layouts.instances_with_texture, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: self.instance_binding(offset, size), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler), + }, + ], + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &resources.globals_bind_group, &[]); + pass.set_bind_group(1, &bind_group, &[]); + pass.draw(0..4, 0..instance_count); + true + } + + 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( + &self, + paths: &[Path], + instance_offset: &mut u64, + pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + 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 resources = self.resources(); + let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { + return true; + }; + + let sprite_data = unsafe { Self::instance_bytes(&sprites) }; + self.draw_instances_with_texture( + sprite_data, + sprites.len() as u32, + path_intermediate_view, + &resources.pipelines.paths, + instance_offset, + pass, + ) + } + + fn draw_paths_to_intermediate( + &self, + encoder: &mut wgpu::CommandEncoder, + paths: &[Path], + instance_offset: &mut u64, + ) -> bool { + 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, + })); + } + + if vertices.is_empty() { + return true; + } + + let vertex_data = unsafe { Self::instance_bytes(&vertices) }; + let Some((vertex_offset, vertex_size)) = + self.write_to_instance_buffer(instance_offset, vertex_data) + else { + return false; + }; + + let resources = self.resources(); + let data_bind_group = resources + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("path_rasterization_bind_group"), + layout: &resources.bind_group_layouts.instances, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: self.instance_binding(vertex_offset, vertex_size), + }], + }); + + let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { + return true; + }; + + 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_pipeline(&resources.pipelines.path_rasterization); + pass.set_bind_group(0, &resources.path_globals_bind_group, &[]); + pass.set_bind_group(1, &data_bind_group, &[]); + pass.draw(0..vertices.len() as u32, 0..1); + } + + true + } + + fn grow_instance_buffer(&mut self) { + let new_capacity = (self.instance_buffer_capacity * 2).min(self.max_buffer_size); + log::info!("increased instance buffer size to {}", new_capacity); + let resources = self.resources_mut(); + resources.instance_buffer = resources.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("instance_buffer"), + size: new_capacity, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + self.instance_buffer_capacity = new_capacity; + } + + fn write_to_instance_buffer( + &self, + instance_offset: &mut u64, + data: &[u8], + ) -> Option<(u64, NonZeroU64)> { + let offset = (*instance_offset).next_multiple_of(self.storage_buffer_alignment); + let size = (data.len() as u64).max(16); + if offset + size > self.instance_buffer_capacity { + return None; + } + let resources = self.resources(); + resources + .queue + .write_buffer(&resources.instance_buffer, offset, data); + *instance_offset = offset + size; + Some((offset, NonZeroU64::new(size).expect("size is at least 16"))) + } + + fn instance_binding(&self, offset: u64, size: NonZeroU64) -> wgpu::BindingResource<'_> { + wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.resources().instance_buffer, + offset, + size: Some(size), + }) + } + + /// 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, + self.extra_requirements.as_ref(), + )?; + *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); + + let extra_reqs = self.extra_requirements.clone(); + *self = Self::new_internal( + Some(gpu_context.clone()), + context, + surface, + config, + self.compositor_gpu, + extra_reqs, + self.atlas.clone(), + )?; + + log::info!("GPU recovery complete"); + Ok(()) + } } #[cfg(not(target_family = "wasm"))] fn create_surface( - instance: &wgpu::Instance, - raw_window_handle: raw_window_handle::RawWindowHandle, + 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}")) - } + 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, + 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; + 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 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 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 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); + 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, - } - } + Self { + path_sample_count, + gamma_ratios, + grayscale_enhanced_contrast, + subpixel_enhanced_contrast, + } + } } diff --git a/crates/gpui_widgets/examples/controls.rs b/crates/gpui_widgets/examples/controls.rs index a43a3d3ac2..29cbcaf331 100644 --- a/crates/gpui_widgets/examples/controls.rs +++ b/crates/gpui_widgets/examples/controls.rs @@ -6,8 +6,8 @@ //! apply it through its engine instead. use gpui::{ - App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, - colors::DefaultColors, div, prelude::*, px, size, + App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, + colors::DefaultColors, div, prelude::*, px, size, }; use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState}; use gpui_widgets::color::{ColorPicker, ColorPickerEvent}; @@ -20,262 +20,270 @@ use gpui_widgets::spinbox::{SpinBox, SpinBoxEvent}; use gpui_widgets::value::ValueKind; struct Example { - exposure: Entity, - frame_rate: Entity, - shutter_angle: Entity, - iso: Entity, - format: Entity, - muted: Entity, - solo: Entity, - playback_mode: Entity, - color: Entity, - remap: Entity, + exposure: Entity, + frame_rate: Entity, + shutter_angle: Entity, + iso: Entity, + format: Entity, + muted: Entity, + solo: Entity, + playback_mode: Entity, + color: Entity, + remap: Entity, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - // Float slider with a key at the current frame. - let exposure = cx.new(|cx| { - Slider::new( - 1, - SliderModel::new(ValueKind::Float, -5.0, 5.0, 0.1, 0.0), - window, - cx, - ) - .with_keying(KeyingState::AtCurrentFrame) - }); - cx.subscribe( - &exposure, - |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { - println!("exposure request: {event:?}"); - }, - ) - .detach(); + fn new(window: &mut Window, cx: &mut Context) -> Self { + // Float slider with a key at the current frame. + let exposure = cx.new(|cx| { + Slider::new( + 1, + SliderModel::new(ValueKind::Float, -5.0, 5.0, 0.1, 0.0), + window, + cx, + ) + .with_keying(KeyingState::AtCurrentFrame) + }); + cx.subscribe( + &exposure, + |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { + println!("exposure request: {event:?}"); + }, + ) + .detach(); - // Rational slider: 1/24 .. 24/24 in numerator steps. - let frame_rate = cx.new(|cx| { - Slider::new( - 2, - SliderModel::new(ValueKind::Rational, 1.0, 24.0, 1.0, 24.0) - .with_rational_den(24), - window, - cx, - ) - .with_keying(KeyingState::HasKey) - }); - cx.subscribe( - &frame_rate, - |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { - println!("frame rate request: {event:?}"); - }, - ) - .detach(); + // Rational slider: 1/24 .. 24/24 in numerator steps. + let frame_rate = cx.new(|cx| { + Slider::new( + 2, + SliderModel::new(ValueKind::Rational, 1.0, 24.0, 1.0, 24.0).with_rational_den(24), + window, + cx, + ) + .with_keying(KeyingState::HasKey) + }); + cx.subscribe( + &frame_rate, + |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { + println!("frame rate request: {event:?}"); + }, + ) + .detach(); - // Angle slider (degrees). - let shutter_angle = cx.new(|cx| { - Slider::new( - 3, - SliderModel::new(ValueKind::Angle, 0.0, 360.0, 1.0, 180.0), - window, - cx, - ) - .with_keying(KeyingState::NoKey) - }); - cx.subscribe( - &shutter_angle, - |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { - println!("shutter request: {event:?}"); - }, - ) - .detach(); + // Angle slider (degrees). + let shutter_angle = cx.new(|cx| { + Slider::new( + 3, + SliderModel::new(ValueKind::Angle, 0.0, 360.0, 1.0, 180.0), + window, + cx, + ) + .with_keying(KeyingState::NoKey) + }); + cx.subscribe( + &shutter_angle, + |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { + println!("shutter request: {event:?}"); + }, + ) + .detach(); - let iso = cx.new(|cx| { - SpinBox::new( - 4, - SliderModel::new(ValueKind::Integer, 100.0, 12800.0, 100.0, 800.0), - window, - cx, - ) - }); - cx.subscribe( - &iso, - |_this: &mut Self, _s: Entity, event: &SpinBoxEvent, _cx| { - println!("iso request: {event:?}"); - }, - ) - .detach(); + let iso = cx.new(|cx| { + SpinBox::new( + 4, + SliderModel::new(ValueKind::Integer, 100.0, 12800.0, 100.0, 800.0), + window, + cx, + ) + }); + cx.subscribe( + &iso, + |_this: &mut Self, _s: Entity, event: &SpinBoxEvent, _cx| { + println!("iso request: {event:?}"); + }, + ) + .detach(); - let format = cx.new(|cx| { - ComboBox::new( - 5, - vec![ - ComboBoxOption::new(1, "Frame"), - ComboBoxOption::new(2, "Timecode"), - ComboBoxOption::new(3, "Frames"), - ], - window, - cx, - ) - .with_placeholder("Choose…") - }); - cx.subscribe( - &format, - |_this: &mut Self, _s: Entity, event: &ComboBoxEvent, _cx| { - println!("format request: {event:?}"); - }, - ) - .detach(); + let format = cx.new(|cx| { + ComboBox::new( + 5, + vec![ + ComboBoxOption::new(1, "Frame"), + ComboBoxOption::new(2, "Timecode"), + ComboBoxOption::new(3, "Frames"), + ], + window, + cx, + ) + .with_placeholder("Choose…") + }); + cx.subscribe( + &format, + |_this: &mut Self, _s: Entity, event: &ComboBoxEvent, _cx| { + println!("format request: {event:?}"); + }, + ) + .detach(); - let muted = cx.new(|cx| { - CheckBox::new(6, CheckState::Unchecked, window, cx).with_label("Mute") - }); - cx.subscribe( - &muted, - |_this: &mut Self, _s: Entity, event: &CheckBoxEvent, _cx| { - println!("mute request: {event:?}"); - }, - ) - .detach(); + let muted = + cx.new(|cx| CheckBox::new(6, CheckState::Unchecked, window, cx).with_label("Mute")); + cx.subscribe( + &muted, + |_this: &mut Self, _s: Entity, event: &CheckBoxEvent, _cx| { + println!("mute request: {event:?}"); + }, + ) + .detach(); - let solo = cx.new(|cx| { - CheckBox::new(7, CheckState::Indeterminate, window, cx) - .with_label("Solo") - .with_tri_state(true) - }); - cx.subscribe( - &solo, - |_this: &mut Self, _s: Entity, event: &CheckBoxEvent, _cx| { - println!("solo request: {event:?}"); - }, - ) - .detach(); + let solo = cx.new(|cx| { + CheckBox::new(7, CheckState::Indeterminate, window, cx) + .with_label("Solo") + .with_tri_state(true) + }); + cx.subscribe( + &solo, + |_this: &mut Self, _s: Entity, event: &CheckBoxEvent, _cx| { + println!("solo request: {event:?}"); + }, + ) + .detach(); - let playback_mode = cx.new(|cx| { - RadioGroup::new( - 8, - vec![ - RadioOption::new(1, "Loop"), - RadioOption::new(2, "Ping-pong"), - RadioOption::new(3, "Once"), - ], - window, - cx, - ) - }); - cx.subscribe( - &playback_mode, - |_this: &mut Self, _s: Entity, event: &RadioGroupEvent, _cx| { - println!("playback mode request: {event:?}"); - }, - ) - .detach(); + let playback_mode = cx.new(|cx| { + RadioGroup::new( + 8, + vec![ + RadioOption::new(1, "Loop"), + RadioOption::new(2, "Ping-pong"), + RadioOption::new(3, "Once"), + ], + window, + cx, + ) + }); + cx.subscribe( + &playback_mode, + |_this: &mut Self, _s: Entity, event: &RadioGroupEvent, _cx| { + println!("playback mode request: {event:?}"); + }, + ) + .detach(); - let color = cx.new(|cx| { - ColorPicker::new(9, gpui::Rgba { r: 1.0, g: 0.4, b: 0.1, a: 1.0 }, window, cx) - }); - cx.subscribe( - &color, - |_this: &mut Self, _s: Entity, event: &ColorPickerEvent, _cx| { - println!("color request: {event:?}"); - }, - ) - .detach(); + let color = cx.new(|cx| { + ColorPicker::new( + 9, + gpui::Rgba { + r: 1.0, + g: 0.4, + b: 0.1, + a: 1.0, + }, + window, + cx, + ) + }); + cx.subscribe( + &color, + |_this: &mut Self, _s: Entity, event: &ColorPickerEvent, _cx| { + println!("color request: {event:?}"); + }, + ) + .detach(); - let remap = cx.new(|cx| { - CurveEditor::new( - 10, - vec![ - CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 0.5)), - CurvePoint::with_handles(1.0, 1.0, CurveVec2::new(0.0, -0.5)), - ], - window, - cx, - ) - }); - cx.subscribe( - &remap, - |_this: &mut Self, _s: Entity, event: &CurveEditorEvent, _cx| { - println!("remap request: {event:?}"); - }, - ) - .detach(); + let remap = cx.new(|cx| { + CurveEditor::new( + 10, + vec![ + CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 0.5)), + CurvePoint::with_handles(1.0, 1.0, CurveVec2::new(0.0, -0.5)), + ], + window, + cx, + ) + }); + cx.subscribe( + &remap, + |_this: &mut Self, _s: Entity, event: &CurveEditorEvent, _cx| { + println!("remap request: {event:?}"); + }, + ) + .detach(); - Self { - exposure, - frame_rate, - shutter_angle, - iso, - format, - muted, - solo, - playback_mode, - color, - remap, - } - } + Self { + exposure, + frame_rate, + shutter_angle, + iso, + format, + muted, + solo, + playback_mode, + color, + remap, + } + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - div() - .size_full() - .bg(colors.background) - .flex() - .flex_col() - .p_4() - .gap_3() - .id("example-scroll") - .overflow_y_scroll() - .child(section("Exposure (float)", self.exposure.clone())) - .child(section("Frame rate (rational)", self.frame_rate.clone())) - .child(section("Shutter (angle)", self.shutter_angle.clone())) - .child(section("ISO (spinbox)", self.iso.clone())) - .child(section("Time format (combo)", self.format.clone())) - .child(section("Audio", self.muted.clone())) - .child(section("Track", self.solo.clone())) - .child(section("Playback mode", self.playback_mode.clone())) - .child(section("Accent color", self.color.clone())) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child(div().text_color(colors.text).child("Time remap (curve)")) - .child(self.remap.clone()), - ) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .size_full() + .bg(colors.background) + .flex() + .flex_col() + .p_4() + .gap_3() + .id("example-scroll") + .overflow_y_scroll() + .child(section("Exposure (float)", self.exposure.clone())) + .child(section("Frame rate (rational)", self.frame_rate.clone())) + .child(section("Shutter (angle)", self.shutter_angle.clone())) + .child(section("ISO (spinbox)", self.iso.clone())) + .child(section("Time format (combo)", self.format.clone())) + .child(section("Audio", self.muted.clone())) + .child(section("Track", self.solo.clone())) + .child(section("Playback mode", self.playback_mode.clone())) + .child(section("Accent color", self.color.clone())) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child(div().text_color(colors.text).child("Time remap (curve)")) + .child(self.remap.clone()), + ) + } } /// A labeled row used by the demo panel. fn section(label: impl Into, widget: impl IntoElement) -> impl IntoElement { - div() - .flex() - .items_center() - .gap_3() - .child(div().w(px(130.0)).child(label.into())) - .child(widget) + div() + .flex() + .items_center() + .gap_3() + .child(div().w(px(130.0)).child(label.into())) + .child(widget) } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(620.0), px(760.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(620.0), px(760.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/examples/menus_dialogs.rs b/crates/gpui_widgets/examples/menus_dialogs.rs index 33bbcf5397..c208378a15 100644 --- a/crates/gpui_widgets/examples/menus_dialogs.rs +++ b/crates/gpui_widgets/examples/menus_dialogs.rs @@ -3,266 +3,269 @@ //! printed as a request event. use gpui::{ - App, Bounds, Context, Entity, Focusable, MouseButton, MouseDownEvent, Render, Window, - WindowBounds, WindowOptions, colors::DefaultColors, div, prelude::*, px, size, + App, Bounds, Context, Entity, Focusable, MouseButton, MouseDownEvent, Render, Window, + WindowBounds, WindowOptions, colors::DefaultColors, div, prelude::*, px, size, }; use gpui_widgets::dialog::file_dialog::{FileDialogContent, file_dialog}; use gpui_widgets::dialog::message_box::{MessageBoxLevel, message_box}; use gpui_widgets::dialog::progress::{ProgressContent, progress_dialog}; use gpui_widgets::dialog::{Modal, ModalEvent}; -use gpui_widgets::menu::{ContextMenu, ContextMenuEvent, Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem}; +use gpui_widgets::menu::{ + ContextMenu, ContextMenuEvent, Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem, +}; struct Example { - menu_bar: Entity, - context_menu: Entity, - message: Entity, - progress: Entity, - progress_content: Entity, - file: Entity, - file_content: Entity, - show_message: bool, - show_progress: bool, - show_file: bool, + menu_bar: Entity, + context_menu: Entity, + message: Entity, + progress: Entity, + progress_content: Entity, + file: Entity, + file_content: Entity, + show_message: bool, + show_progress: bool, + show_file: bool, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let menu_bar = cx.new(|cx| { - MenuBar::new( - 1, - vec![ - MenuBarEntry::new( - "File", - Menu::new(vec![ - MenuItem::new(10, "New…").with_shortcut("⌘N"), - MenuItem::new(11, "Open…").with_shortcut("⌘O"), - MenuItem::new(12, "Save").with_shortcut("⌘S").separated(), - MenuItem::new(13, "Export").disabled(), - ]), - ), - MenuBarEntry::new( - "Edit", - Menu::new(vec![ - MenuItem::new(20, "Undo").with_shortcut("⌘Z"), - MenuItem::new(21, "Redo").with_shortcut("⇧⌘Z").separated(), - MenuItem::new(22, "Show Diagnostics").with_checked(false), - ]), - ), - MenuBarEntry::new( - "View", - Menu::new(vec![ - MenuItem::new(30, "Toolbars"), - MenuItem::new(31, "Scopes"), - MenuItem::new(32, "Theme").with_submenu(Menu::new(vec![ - MenuItem::new(33, "Olive Dark").with_checked(true), - MenuItem::new(34, "Olive Light"), - ])), - ]), - ), - ], - window, - cx, - ) - }); - cx.subscribe( - &menu_bar, - |_this: &mut Self, _m: Entity, event: &MenuBarEvent, _cx| { - println!("menu bar: {event:?}"); - }, - ) - .detach(); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let menu_bar = cx.new(|cx| { + MenuBar::new( + 1, + vec![ + MenuBarEntry::new( + "File", + Menu::new(vec![ + MenuItem::new(10, "New…").with_shortcut("⌘N"), + MenuItem::new(11, "Open…").with_shortcut("⌘O"), + MenuItem::new(12, "Save").with_shortcut("⌘S").separated(), + MenuItem::new(13, "Export").disabled(), + ]), + ), + MenuBarEntry::new( + "Edit", + Menu::new(vec![ + MenuItem::new(20, "Undo").with_shortcut("⌘Z"), + MenuItem::new(21, "Redo").with_shortcut("⇧⌘Z").separated(), + MenuItem::new(22, "Show Diagnostics").with_checked(false), + ]), + ), + MenuBarEntry::new( + "View", + Menu::new(vec![ + MenuItem::new(30, "Toolbars"), + MenuItem::new(31, "Scopes"), + MenuItem::new(32, "Theme").with_submenu(Menu::new(vec![ + MenuItem::new(33, "Olive Dark").with_checked(true), + MenuItem::new(34, "Olive Light"), + ])), + ]), + ), + ], + window, + cx, + ) + }); + cx.subscribe( + &menu_bar, + |_this: &mut Self, _m: Entity, event: &MenuBarEvent, _cx| { + println!("menu bar: {event:?}"); + }, + ) + .detach(); - let context_menu = cx.new(|cx| ContextMenu::new(2, window, cx)); - cx.subscribe( - &context_menu, - |_this: &mut Self, _m: Entity, event: &ContextMenuEvent, _cx| { - println!("context menu: {event:?}"); - }, - ) - .detach(); + let context_menu = cx.new(|cx| ContextMenu::new(2, window, cx)); + cx.subscribe( + &context_menu, + |_this: &mut Self, _m: Entity, event: &ContextMenuEvent, _cx| { + println!("context menu: {event:?}"); + }, + ) + .detach(); - let message = message_box( - 3, - MessageBoxLevel::Warning, - "Unsaved changes", - "Your project has unsaved changes. Export anyway?", - window, - cx, - ); - cx.subscribe( - &message, - |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { - println!("message box: {event:?}"); - if matches!(event, ModalEvent::ButtonClicked { .. }) { - this.show_message = false; - } - }, - ) - .detach(); + let message = message_box( + 3, + MessageBoxLevel::Warning, + "Unsaved changes", + "Your project has unsaved changes. Export anyway?", + window, + cx, + ); + cx.subscribe( + &message, + |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { + println!("message box: {event:?}"); + if matches!(event, ModalEvent::ButtonClicked { .. }) { + this.show_message = false; + } + }, + ) + .detach(); - let (progress, progress_content) = progress_dialog(4, "Exporting…", "Encoding video", window, cx); - cx.subscribe( - &progress, - |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { - println!("progress: {event:?}"); - if matches!(event, ModalEvent::ButtonClicked { .. }) { - this.show_progress = false; - } - }, - ) - .detach(); + let (progress, progress_content) = + progress_dialog(4, "Exporting…", "Encoding video", window, cx); + cx.subscribe( + &progress, + |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { + println!("progress: {event:?}"); + if matches!(event, ModalEvent::ButtonClicked { .. }) { + this.show_progress = false; + } + }, + ) + .detach(); - let (file, file_content) = file_dialog(5, "Open media…", window, cx); - cx.subscribe( - &file, - |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { - if let ModalEvent::ButtonClicked { button: 0, .. } = event { - let path = this.file_content.read(_cx).path(_cx); - println!("open file: {path}"); - } - println!("file dialog: {event:?}"); - if matches!(event, ModalEvent::ButtonClicked { .. }) { - this.show_file = false; - } - }, - ) - .detach(); + let (file, file_content) = file_dialog(5, "Open media…", window, cx); + cx.subscribe( + &file, + |this: &mut Self, _m: Entity, event: &ModalEvent, _cx| { + if let ModalEvent::ButtonClicked { button: 0, .. } = event { + let path = this.file_content.read(_cx).path(_cx); + println!("open file: {path}"); + } + println!("file dialog: {event:?}"); + if matches!(event, ModalEvent::ButtonClicked { .. }) { + this.show_file = false; + } + }, + ) + .detach(); - Self { - menu_bar, - context_menu, - message, - progress, - progress_content, - file, - file_content, - show_message: false, - show_progress: false, - show_file: false, - } - } + Self { + menu_bar, + context_menu, + message, + progress, + progress_content, + file, + file_content, + show_message: false, + show_progress: false, + show_file: false, + } + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); - let mut root = div() - .size_full() - .bg(colors.background) - .flex() - .flex_col() - .child(self.menu_bar.clone()) - .child( - div() - .flex_1() - .flex() - .items_center() - .justify_center() - .gap_2() - .text_color(colors.text) - .id("example-main") - .on_mouse_down( - MouseButton::Right, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - this.context_menu.update(cx, |menu, cx| { - menu.show( - event.position, - Menu::new(vec![ - MenuItem::new(40, "Cut").with_shortcut("⌘X"), - MenuItem::new(41, "Copy").with_shortcut("⌘C"), - MenuItem::new(42, "Paste").with_shortcut("⌘V"), - MenuItem::new(43, "Delete").disabled().separated(), - MenuItem::new(44, "Properties…"), - ]), - cx, - ); - }); - }), - ) - .child(div().child("Right-click for a context menu")) - .child( - div() - .id("example-btn-message") - .px_3() - .py_1() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .cursor_pointer() - .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { - this.show_message = true; - window.focus(&this.message.read(cx).focus_handle(cx), cx); - })) - .child("Message box"), - ) - .child( - div() - .id("example-btn-progress") - .px_3() - .py_1() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .cursor_pointer() - .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { - this.show_progress = true; - this.progress_content - .update(cx, |content, cx| content.set_progress(0.4, cx)); - window.focus(&this.progress.read(cx).focus_handle(cx), cx); - })) - .child("Progress"), - ) - .child( - div() - .id("example-btn-file") - .px_3() - .py_1() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .cursor_pointer() - .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { - this.show_file = true; - window.focus(&this.file.read(cx).focus_handle(cx), cx); - })) - .child("Open file…"), - ), - ) - .child(self.context_menu.clone()); + let mut root = div() + .size_full() + .bg(colors.background) + .flex() + .flex_col() + .child(self.menu_bar.clone()) + .child( + div() + .flex_1() + .flex() + .items_center() + .justify_center() + .gap_2() + .text_color(colors.text) + .id("example-main") + .on_mouse_down( + MouseButton::Right, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.context_menu.update(cx, |menu, cx| { + menu.show( + event.position, + Menu::new(vec![ + MenuItem::new(40, "Cut").with_shortcut("⌘X"), + MenuItem::new(41, "Copy").with_shortcut("⌘C"), + MenuItem::new(42, "Paste").with_shortcut("⌘V"), + MenuItem::new(43, "Delete").disabled().separated(), + MenuItem::new(44, "Properties…"), + ]), + cx, + ); + }); + }), + ) + .child(div().child("Right-click for a context menu")) + .child( + div() + .id("example-btn-message") + .px_3() + .py_1() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .cursor_pointer() + .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { + this.show_message = true; + window.focus(&this.message.read(cx).focus_handle(cx), cx); + })) + .child("Message box"), + ) + .child( + div() + .id("example-btn-progress") + .px_3() + .py_1() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .cursor_pointer() + .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { + this.show_progress = true; + this.progress_content + .update(cx, |content, cx| content.set_progress(0.4, cx)); + window.focus(&this.progress.read(cx).focus_handle(cx), cx); + })) + .child("Progress"), + ) + .child( + div() + .id("example-btn-file") + .px_3() + .py_1() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .cursor_pointer() + .on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| { + this.show_file = true; + window.focus(&this.file.read(cx).focus_handle(cx), cx); + })) + .child("Open file…"), + ), + ) + .child(self.context_menu.clone()); - if self.show_message { - root = root.child(self.message.clone()); - } - if self.show_progress { - root = root.child(self.progress.clone()); - } - if self.show_file { - root = root.child(self.file.clone()); - } - root - } + if self.show_message { + root = root.child(self.message.clone()); + } + if self.show_progress { + root = root.child(self.progress.clone()); + } + if self.show_file { + root = root.child(self.file.clone()); + } + root + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(640.0), px(480.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(640.0), px(480.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/examples/project_explorer.rs b/crates/gpui_widgets/examples/project_explorer.rs index d760746b49..e54d3204ca 100644 --- a/crates/gpui_widgets/examples/project_explorer.rs +++ b/crates/gpui_widgets/examples/project_explorer.rs @@ -3,106 +3,106 @@ //! drop). use gpui::{ - App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, - px, size, + App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, + size, }; use gpui_widgets::project_explorer::{ - ProjectDataSource, ProjectEntry, ProjectExplorer, ProjectExplorerEvent, + ProjectDataSource, ProjectEntry, ProjectExplorer, ProjectExplorerEvent, }; struct MockProject { - entries: Vec, + entries: Vec, } impl MockProject { - fn demo() -> Self { - Self { - entries: vec![ - ProjectEntry::new(1, "Footage", true), - ProjectEntry::new(2, "Bins", true), - ProjectEntry::new(3, "Notes.md", false), - ], - } - } + fn demo() -> Self { + Self { + entries: vec![ + ProjectEntry::new(1, "Footage", true), + ProjectEntry::new(2, "Bins", true), + ProjectEntry::new(3, "Notes.md", false), + ], + } + } } impl ProjectDataSource for MockProject { - fn roots(&self) -> Vec { - self.entries - .iter() - .filter(|e| matches!(e.id, 1..=3)) - .cloned() - .collect() - } + fn roots(&self) -> Vec { + self.entries + .iter() + .filter(|e| matches!(e.id, 1..=3)) + .cloned() + .collect() + } - fn children(&self, parent_id: u64) -> Vec { - match parent_id { - 1 => vec![ - ProjectEntry::new(10, "intro.mov", false).with_thumbnail("assets/thumb.png"), - ProjectEntry::new(11, "b-roll.mov", false), - ProjectEntry::new(12, "interview.mov", false), - ], - 2 => vec![ - ProjectEntry::new(20, "Selects", true), - ProjectEntry::new(21, "Music", true), - ], - 20 => vec![ProjectEntry::new(200, "best-take.mov", false)], - 21 => vec![ - ProjectEntry::new(210, "track-01.wav", false), - ProjectEntry::new(211, "track-02.wav", false), - ], - _ => Vec::new(), - } - } + fn children(&self, parent_id: u64) -> Vec { + match parent_id { + 1 => vec![ + ProjectEntry::new(10, "intro.mov", false).with_thumbnail("assets/thumb.png"), + ProjectEntry::new(11, "b-roll.mov", false), + ProjectEntry::new(12, "interview.mov", false), + ], + 2 => vec![ + ProjectEntry::new(20, "Selects", true), + ProjectEntry::new(21, "Music", true), + ], + 20 => vec![ProjectEntry::new(200, "best-take.mov", false)], + 21 => vec![ + ProjectEntry::new(210, "track-01.wav", false), + ProjectEntry::new(211, "track-02.wav", false), + ], + _ => Vec::new(), + } + } } struct Example { - explorer: Entity>, + explorer: Entity>, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let project = cx.new(|_| MockProject::demo()); - let explorer = cx.new(|cx| ProjectExplorer::new(1, project, window, cx)); - cx.subscribe( - &explorer, - |_this: &mut Self, - _e: Entity>, - event: &ProjectExplorerEvent, - _cx| { - println!("explorer request: {event:?}"); - }, - ) - .detach(); - Self { explorer } - } + fn new(window: &mut Window, cx: &mut Context) -> Self { + let project = cx.new(|_| MockProject::demo()); + let explorer = cx.new(|cx| ProjectExplorer::new(1, project, window, cx)); + cx.subscribe( + &explorer, + |_this: &mut Self, + _e: Entity>, + event: &ProjectExplorerEvent, + _cx| { + println!("explorer request: {event:?}"); + }, + ) + .detach(); + Self { explorer } + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.explorer.clone()) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.explorer.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(420.0), px(560.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(420.0), px(560.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/examples/scopes.rs b/crates/gpui_widgets/examples/scopes.rs index 18613edae0..edbfb7a78b 100644 --- a/crates/gpui_widgets/examples/scopes.rs +++ b/crates/gpui_widgets/examples/scopes.rs @@ -5,150 +5,148 @@ //! Run with `cargo run -p gpui_widgets --example scopes`. use gpui::{ - App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, - px, size, + App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, + size, }; use gpui_widgets::audio_meter::{AudioLevelMeter, AudioMeterDataSource}; use gpui_widgets::scopes::{ChromaDataSource, Histogram, LumaDataSource, Vectorscope, Waveform}; struct MockLuma { - frame: u64, + frame: u64, } impl LumaDataSource for MockLuma { - fn luma_samples(&self) -> Vec { - // A moving gradient + noise-ish bars. - (0..4096) - .map(|i| { - let x = i as f32 / 4096.0; - let t = self.frame as f32 / 60.0; - ((x + t * 0.25).fract() * 0.8 + 0.1).clamp(0.0, 1.0) - }) - .collect() - } + fn luma_samples(&self) -> Vec { + // A moving gradient + noise-ish bars. + (0..4096) + .map(|i| { + let x = i as f32 / 4096.0; + let t = self.frame as f32 / 60.0; + ((x + t * 0.25).fract() * 0.8 + 0.1).clamp(0.0, 1.0) + }) + .collect() + } } struct MockChroma { - frame: u64, + frame: u64, } impl ChromaDataSource for MockChroma { - fn chroma_samples(&self) -> Vec<(f32, f32)> { - // A rotating ring in chroma space. - let t = self.frame as f32 / 60.0; - (0..2048) - .map(|i| { - let angle = i as f32 / 2048.0 * std::f32::consts::TAU; - ( - 0.5 + 0.4 * (angle + t).cos(), - 0.5 + 0.4 * (angle + t).sin(), - ) - }) - .collect() - } + fn chroma_samples(&self) -> Vec<(f32, f32)> { + // A rotating ring in chroma space. + let t = self.frame as f32 / 60.0; + (0..2048) + .map(|i| { + let angle = i as f32 / 2048.0 * std::f32::consts::TAU; + (0.5 + 0.4 * (angle + t).cos(), 0.5 + 0.4 * (angle + t).sin()) + }) + .collect() + } } struct MockAudio { - frame: u64, + frame: u64, } impl AudioMeterDataSource for MockAudio { - fn levels(&self) -> Vec { - let t = self.frame as f32 / 60.0; - vec![ - (0.5 + 0.5 * (t * 2.0).sin()).clamp(0.0, 1.0), - (0.5 + 0.5 * (t * 1.7).cos()).clamp(0.0, 1.0), - ] - } + fn levels(&self) -> Vec { + let t = self.frame as f32 / 60.0; + vec![ + (0.5 + 0.5 * (t * 2.0).sin()).clamp(0.0, 1.0), + (0.5 + 0.5 * (t * 1.7).cos()).clamp(0.0, 1.0), + ] + } } struct Example { - luma: Entity, - chroma: Entity, - audio: Entity, - histogram: Entity>, - waveform: Entity>, - vectorscope: Entity>, - meter: Entity>, + luma: Entity, + chroma: Entity, + audio: Entity, + histogram: Entity>, + waveform: Entity>, + vectorscope: Entity>, + meter: Entity>, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let luma = cx.new(|_| MockLuma { frame: 0 }); - let chroma = cx.new(|_| MockChroma { frame: 0 }); - let audio = cx.new(|_| MockAudio { frame: 0 }); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let luma = cx.new(|_| MockLuma { frame: 0 }); + let chroma = cx.new(|_| MockChroma { frame: 0 }); + let audio = cx.new(|_| MockAudio { frame: 0 }); - let histogram = cx.new(|cx| Histogram::new(1, luma.clone(), window, cx)); - let waveform = cx.new(|cx| Waveform::new(2, luma.clone(), window, cx)); - let vectorscope = cx.new(|cx| Vectorscope::new(3, chroma.clone(), window, cx)); - let meter = cx.new(|cx| AudioLevelMeter::new(4, audio.clone(), window, cx)); + let histogram = cx.new(|cx| Histogram::new(1, luma.clone(), window, cx)); + let waveform = cx.new(|cx| Waveform::new(2, luma.clone(), window, cx)); + let vectorscope = cx.new(|cx| Vectorscope::new(3, chroma.clone(), window, cx)); + let meter = cx.new(|cx| AudioLevelMeter::new(4, audio.clone(), window, cx)); - let this = cx.weak_entity(); - window.spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { - loop { - cx.background_executor() - .timer(std::time::Duration::from_millis(16)) - .await; - let _ = cx.update(|_window, app| { - if let Some(this) = this.upgrade() { - this.update(app, |this, cx| this.tick(cx)); - } - }); - } - }) - .detach(); + let this = cx.weak_entity(); + window + .spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { + loop { + cx.background_executor() + .timer(std::time::Duration::from_millis(16)) + .await; + let _ = cx.update(|_window, app| { + if let Some(this) = this.upgrade() { + this.update(app, |this, cx| this.tick(cx)); + } + }); + } + }) + .detach(); - Self { - luma, - chroma, - audio, - histogram, - waveform, - vectorscope, - meter, - } - } + Self { + luma, + chroma, + audio, + histogram, + waveform, + vectorscope, + meter, + } + } - fn tick(&mut self, cx: &mut Context) { - self.luma.update(cx, |luma, _| luma.frame += 1); - self.chroma.update(cx, |chroma, _| chroma.frame += 1); - self.audio.update(cx, |audio, _| audio.frame += 1); - self.meter.update(cx, |meter, cx| meter.update(cx)); - cx.notify(); - } + fn tick(&mut self, cx: &mut Context) { + self.luma.update(cx, |luma, _| luma.frame += 1); + self.chroma.update(cx, |chroma, _| chroma.frame += 1); + self.audio.update(cx, |audio, _| audio.frame += 1); + self.meter.update(cx, |meter, cx| meter.update(cx)); + cx.notify(); + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .flex_col() - .gap_2() - .p_2() - .child(div().h(px(120.0)).child(self.histogram.clone())) - .child(div().h(px(120.0)).child(self.waveform.clone())) - .child(div().h(px(140.0)).child(self.vectorscope.clone())) - .child(div().h(px(60.0)).child(self.meter.clone())) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .flex_col() + .gap_2() + .p_2() + .child(div().h(px(120.0)).child(self.histogram.clone())) + .child(div().h(px(120.0)).child(self.waveform.clone())) + .child(div().h(px(140.0)).child(self.vectorscope.clone())) + .child(div().h(px(60.0)).child(self.meter.clone())) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(520.0), px(560.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(520.0), px(560.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/examples/themes.rs b/crates/gpui_widgets/examples/themes.rs index c001f29fc8..19e5ce49b2 100644 --- a/crates/gpui_widgets/examples/themes.rs +++ b/crates/gpui_widgets/examples/themes.rs @@ -2,8 +2,8 @@ //! widget that reads `cx.default_colors()` re-themes immediately. use gpui::{ - App, Bounds, ClickEvent, Context, Entity, Render, Window, WindowBounds, WindowOptions, - colors::DefaultColors, div, prelude::*, px, size, + App, Bounds, ClickEvent, Context, Entity, Render, Window, WindowBounds, WindowOptions, + colors::DefaultColors, div, prelude::*, px, size, }; use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState}; use gpui_widgets::slider::{Slider, SliderEvent, SliderModel}; @@ -11,114 +11,120 @@ use gpui_widgets::theme::{OakTheme, apply_theme}; use gpui_widgets::value::ValueKind; struct Example { - slider: Entity, - checkbox: Entity, - dark: bool, + slider: Entity, + checkbox: Entity, + dark: bool, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let slider = cx.new(|cx| { - Slider::new( - 1, - SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.05, 0.5), - window, - cx, - ) - }); - cx.subscribe( - &slider, - |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { - println!("slider: {event:?}"); - }, - ) - .detach(); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let slider = cx.new(|cx| { + Slider::new( + 1, + SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.05, 0.5), + window, + cx, + ) + }); + cx.subscribe( + &slider, + |_this: &mut Self, _s: Entity, event: &SliderEvent, _cx| { + println!("slider: {event:?}"); + }, + ) + .detach(); - let checkbox = cx.new(|cx| { - CheckBox::new(2, CheckState::Checked, window, cx).with_label("Track enabled") - }); - cx.subscribe( - &checkbox, - |_this: &mut Self, _c: Entity, event: &CheckBoxEvent, _cx| { - println!("checkbox: {event:?}"); - }, - ) - .detach(); + let checkbox = cx.new(|cx| { + CheckBox::new(2, CheckState::Checked, window, cx).with_label("Track enabled") + }); + cx.subscribe( + &checkbox, + |_this: &mut Self, _c: Entity, event: &CheckBoxEvent, _cx| { + println!("checkbox: {event:?}"); + }, + ) + .detach(); - Self { - slider, - checkbox, - dark: true, - } - } + Self { + slider, + checkbox, + dark: true, + } + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let theme = if self.dark { - OakTheme::olive_dark() - } else { - OakTheme::olive_light() - }; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let theme = if self.dark { + OakTheme::olive_dark() + } else { + OakTheme::olive_light() + }; - div() - .size_full() - .bg(theme.base) - .flex() - .flex_col() - .gap_3() - .p_6() - .child( - div() - .text_color(colors.text) - .child(format!("Current theme: {}", theme.name)), - ) - .child(div().flex().items_center().gap_2().child(self.slider.clone())) - .child(self.checkbox.clone()) - .child( - div() - .id("theme-toggle") - .px_3() - .py_1() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .cursor_pointer() - .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.dark = !this.dark; - let theme = if this.dark { - OakTheme::olive_dark() - } else { - OakTheme::olive_light() - }; - apply_theme(cx, &theme); - cx.notify(); - })) - .child("Toggle theme"), - ) - } + div() + .size_full() + .bg(theme.base) + .flex() + .flex_col() + .gap_3() + .p_6() + .child( + div() + .text_color(colors.text) + .child(format!("Current theme: {}", theme.name)), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .child(self.slider.clone()), + ) + .child(self.checkbox.clone()) + .child( + div() + .id("theme-toggle") + .px_3() + .py_1() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .cursor_pointer() + .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.dark = !this.dark; + let theme = if this.dark { + OakTheme::olive_dark() + } else { + OakTheme::olive_light() + }; + apply_theme(cx, &theme); + cx.notify(); + })) + .child("Toggle theme"), + ) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - apply_theme(cx, &OakTheme::olive_dark()); - let bounds = Bounds::centered(None, size(px(480.0), px(320.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + apply_theme(cx, &OakTheme::olive_dark()); + let bounds = Bounds::centered(None, size(px(480.0), px(320.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/examples/viewer.rs b/crates/gpui_widgets/examples/viewer.rs index be9c21375e..ae6af4f31b 100644 --- a/crates/gpui_widgets/examples/viewer.rs +++ b/crates/gpui_widgets/examples/viewer.rs @@ -3,184 +3,186 @@ //! //! Run with `cargo run -p gpui_widgets --example viewer`. -use gpui::{ - App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, - px, size, -}; use gpui::timeline::{Frame, FrameRate}; +use gpui::{ + App, Bounds, Context, Entity, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, + size, +}; use gpui_widgets::viewer::{PlaybackClock, ViewerEvent, ViewerWidget}; #[cfg(target_os = "macos")] mod frame_gen { - use core_video::pixel_buffer::CVPixelBuffer; - use std::ffi::c_void; + use core_video::pixel_buffer::CVPixelBuffer; + use std::ffi::c_void; - const WIDTH: u32 = 1280; - const HEIGHT: u32 = 720; + const WIDTH: u32 = 1280; + const HEIGHT: u32 = 720; - /// Generates cheap test frames (a moving hue band) into CVPixelBuffers. - pub struct FrameGen { - frame: u64, - } + /// Generates cheap test frames (a moving hue band) into CVPixelBuffers. + pub struct FrameGen { + frame: u64, + } - impl FrameGen { - pub fn new() -> Self { - Self { frame: 0 } - } + impl FrameGen { + pub fn new() -> Self { + Self { frame: 0 } + } - pub fn next(&mut self) -> CVPixelBuffer { - let mut bytes = vec![0u8; (WIDTH * HEIGHT * 4) as usize]; - for y in 0..HEIGHT { - for x in 0..WIDTH { - let i = ((y * WIDTH + x) * 4) as usize; - let t = self.frame as f32 / 60.0; - let band = if (x as f32 / 128.0 + t * 8.0).fract() < 0.5 { - 1.0 - } else { - 0.35 - }; - bytes[i] = (255.0f32 * band).round() as u8; - bytes[i + 1] = (255.0f32 * band * (y as f32 / HEIGHT as f32)).round() as u8; - bytes[i + 2] = (255.0f32 * band * (1.0 - y as f32 / HEIGHT as f32)).round() as u8; - bytes[i + 3] = 255; - } - } - let callback_ref: Box> = Box::new(bytes); - let release_con = Box::into_raw(callback_ref) as *mut c_void; - self.frame += 1; - unsafe { - CVPixelBuffer::new_with_bytes( - 0x42475241, // kCVPixelFormatType_32BGRA - WIDTH as usize, - HEIGHT as usize, - release_con as *mut c_void, - (WIDTH * 4) as usize, - free_bytes, - release_con, - None, - ) - } - .expect("failed to create test pixel buffer") - } + pub fn next(&mut self) -> CVPixelBuffer { + let mut bytes = vec![0u8; (WIDTH * HEIGHT * 4) as usize]; + for y in 0..HEIGHT { + for x in 0..WIDTH { + let i = ((y * WIDTH + x) * 4) as usize; + let t = self.frame as f32 / 60.0; + let band = if (x as f32 / 128.0 + t * 8.0).fract() < 0.5 { + 1.0 + } else { + 0.35 + }; + bytes[i] = (255.0f32 * band).round() as u8; + bytes[i + 1] = (255.0f32 * band * (y as f32 / HEIGHT as f32)).round() as u8; + bytes[i + 2] = + (255.0f32 * band * (1.0 - y as f32 / HEIGHT as f32)).round() as u8; + bytes[i + 3] = 255; + } + } + let callback_ref: Box> = Box::new(bytes); + let release_con = Box::into_raw(callback_ref) as *mut c_void; + self.frame += 1; + unsafe { + CVPixelBuffer::new_with_bytes( + 0x42475241, // kCVPixelFormatType_32BGRA + WIDTH as usize, + HEIGHT as usize, + release_con as *mut c_void, + (WIDTH * 4) as usize, + free_bytes, + release_con, + None, + ) + } + .expect("failed to create test pixel buffer") + } + } - } - - extern "C" fn free_bytes(release_ref_con: *mut c_void, _base: *const *const c_void) { - if !release_ref_con.is_null() { - unsafe { - drop(Box::from_raw(release_ref_con as *mut Vec)); - } - } - } + extern "C" fn free_bytes(release_ref_con: *mut c_void, _base: *const *const c_void) { + if !release_ref_con.is_null() { + unsafe { + drop(Box::from_raw(release_ref_con as *mut Vec)); + } + } + } } struct MockClock { - frame: Frame, - playing: bool, - rate: FrameRate, + frame: Frame, + playing: bool, + rate: FrameRate, } impl PlaybackClock for MockClock { - fn current_frame(&self) -> Frame { - self.frame - } - fn is_playing(&self) -> bool { - self.playing - } - fn frame_rate(&self) -> FrameRate { - self.rate - } + fn current_frame(&self) -> Frame { + self.frame + } + fn is_playing(&self) -> bool { + self.playing + } + fn frame_rate(&self) -> FrameRate { + self.rate + } } struct Example { - clock: Entity, - viewer: Entity>, - #[cfg(target_os = "macos")] - frame_gen: frame_gen::FrameGen, + clock: Entity, + viewer: Entity>, + #[cfg(target_os = "macos")] + frame_gen: frame_gen::FrameGen, } impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let clock = cx.new(|_| MockClock { - frame: Frame(0), - playing: true, - rate: FrameRate::new(30, 1), - }); - let viewer = cx.new(|cx| ViewerWidget::new(1, clock.clone(), window, cx)); - cx.subscribe( - &viewer, - |_this: &mut Self, _v: Entity>, event: &ViewerEvent, _cx| { - println!("viewer request: {event:?}"); - }, - ) - .detach(); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let clock = cx.new(|_| MockClock { + frame: Frame(0), + playing: true, + rate: FrameRate::new(30, 1), + }); + let viewer = cx.new(|cx| ViewerWidget::new(1, clock.clone(), window, cx)); + cx.subscribe( + &viewer, + |_this: &mut Self, _v: Entity>, event: &ViewerEvent, _cx| { + println!("viewer request: {event:?}"); + }, + ) + .detach(); - let this = cx.weak_entity(); - window.spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { - loop { - cx.background_executor() - .timer(std::time::Duration::from_millis(16)) - .await; - let _ = cx.update(|_window, app| { - if let Some(this) = this.upgrade() { - this.update(app, |this, cx| this.tick(cx)); - } - }); - } - }) - .detach(); + let this = cx.weak_entity(); + window + .spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { + loop { + cx.background_executor() + .timer(std::time::Duration::from_millis(16)) + .await; + let _ = cx.update(|_window, app| { + if let Some(this) = this.upgrade() { + this.update(app, |this, cx| this.tick(cx)); + } + }); + } + }) + .detach(); - Self { - clock, - viewer, - #[cfg(target_os = "macos")] - frame_gen: frame_gen::FrameGen::new(), - } - } + Self { + clock, + viewer, + #[cfg(target_os = "macos")] + frame_gen: frame_gen::FrameGen::new(), + } + } - fn tick(&mut self, cx: &mut Context) { - // Simulate the engine: advance the clock while playing. - self.clock.update(cx, |clock, _cx| { - if clock.playing { - clock.frame = Frame((clock.frame.0 + 1) % (30 * 60 * 5)); - } - }); - // Feed a fresh frame (macOS only). - #[cfg(target_os = "macos")] - { - let buffer = self.frame_gen.next(); - self.viewer - .update(cx, |viewer, cx| viewer.set_frame_source(Some(buffer.into()), cx)); - } - cx.notify(); - } + fn tick(&mut self, cx: &mut Context) { + // Simulate the engine: advance the clock while playing. + self.clock.update(cx, |clock, _cx| { + if clock.playing { + clock.frame = Frame((clock.frame.0 + 1) % (30 * 60 * 5)); + } + }); + // Feed a fresh frame (macOS only). + #[cfg(target_os = "macos")] + { + let buffer = self.frame_gen.next(); + self.viewer.update(cx, |viewer, cx| { + viewer.set_frame_source(Some(buffer.into()), cx) + }); + } + cx.notify(); + } } impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.viewer.clone()) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.viewer.clone()) + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(960.0), px(540.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(960.0), px(540.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/gpui_widgets/src/audio_meter.rs b/crates/gpui_widgets/src/audio_meter.rs index ba9352de3e..7276f37cc5 100644 --- a/crates/gpui_widgets/src/audio_meter.rs +++ b/crates/gpui_widgets/src/audio_meter.rs @@ -5,8 +5,8 @@ //! hold decays locally; the pure arithmetic is in [`scopes::math`]. use gpui::{ - App, Bounds, Context, Entity, FocusHandle, Focusable, Hsla, Render, Window, canvas, - colors::DefaultColors, fill, point, prelude::*, px, size, + App, Bounds, Context, Entity, FocusHandle, Focusable, Hsla, Render, Window, canvas, + colors::DefaultColors, fill, point, prelude::*, px, size, }; use crate::scopes::{decay_peak, meter_lit_segments}; @@ -23,252 +23,272 @@ const PEAK_DECAY: f32 = 0.01; /// places channels side by side and lights segments bottom to top. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum MeterOrientation { - /// Channels stacked vertically, segments lit left to right. - #[default] - Horizontal, - /// Channels side by side, segments lit bottom to top. - Vertical, + /// Channels stacked vertically, segments lit left to right. + #[default] + Horizontal, + /// Channels side by side, segments lit bottom to top. + Vertical, } /// Provides per-channel levels in `0..1` (linear or dB-normalized). pub trait AudioMeterDataSource: 'static { - /// The current level of each channel, `0..1`. - fn levels(&self) -> Vec; + /// The current level of each channel, `0..1`. + fn levels(&self) -> Vec; } /// An audio level meter. pub struct AudioLevelMeter { - data: Entity, - focus_handle: FocusHandle, - peak: Vec, - orientation: MeterOrientation, + data: Entity, + focus_handle: FocusHandle, + peak: Vec, + orientation: MeterOrientation, } impl AudioLevelMeter { - /// Create a meter over `data`. - pub fn new( - _control: usize, - data: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - data, - focus_handle: cx.focus_handle(), - peak: Vec::new(), - orientation: MeterOrientation::Horizontal, - } - } + /// Create a meter over `data`. + pub fn new( + _control: usize, + data: Entity, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + data, + focus_handle: cx.focus_handle(), + peak: Vec::new(), + orientation: MeterOrientation::Horizontal, + } + } - /// Set the orientation (builder-style, callable after `new`). - pub fn with_orientation(mut self, orientation: MeterOrientation) -> Self { - self.orientation = orientation; - self - } + /// Set the orientation (builder-style, callable after `new`). + pub fn with_orientation(mut self, orientation: MeterOrientation) -> Self { + self.orientation = orientation; + self + } - /// The current orientation. - pub fn orientation(&self) -> MeterOrientation { - self.orientation - } + /// The current orientation. + pub fn orientation(&self) -> MeterOrientation { + self.orientation + } - /// The current per-channel levels. - pub fn levels(&self, cx: &App) -> Vec { - self.data.read(cx).levels() - } + /// The current per-channel levels. + pub fn levels(&self, cx: &App) -> Vec { + self.data.read(cx).levels() + } - /// Update the peak-hold state from the current levels (call each frame). - pub fn update(&mut self, cx: &mut Context) { - let levels = self.data.read(cx).levels(); - self.peak.resize(levels.len(), 0.0); - for (peak, level) in self.peak.iter_mut().zip(&levels) { - *peak = decay_peak(*peak, *level, PEAK_DECAY); - } - cx.notify(); - } + /// Update the peak-hold state from the current levels (call each frame). + pub fn update(&mut self, cx: &mut Context) { + let levels = self.data.read(cx).levels(); + self.peak.resize(levels.len(), 0.0); + for (peak, level) in self.peak.iter_mut().zip(&levels) { + *peak = decay_peak(*peak, *level, PEAK_DECAY); + } + cx.notify(); + } } impl Focusable for AudioLevelMeter { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for AudioLevelMeter { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let levels = self.data.read(cx).levels(); - self.peak.resize(levels.len(), 0.0); - let peaks = self.peak.clone(); - let lit_counts: Vec = levels - .iter() - .map(|level| meter_lit_segments(*level, SEGMENTS)) - .collect(); - let orientation = self.orientation; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let levels = self.data.read(cx).levels(); + self.peak.resize(levels.len(), 0.0); + let peaks = self.peak.clone(); + let lit_counts: Vec = levels + .iter() + .map(|level| meter_lit_segments(*level, SEGMENTS)) + .collect(); + let orientation = self.orientation; - canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, _cx| { - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - let lit_color = Hsla::from(colors.selected); - let dim_color = Hsla::from(colors.border); - let peak_color = Hsla::from(colors.text); + canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, _cx| { + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + let lit_color = Hsla::from(colors.selected); + let dim_color = Hsla::from(colors.border); + let peak_color = Hsla::from(colors.text); - match orientation { - MeterOrientation::Horizontal => { - let channel_h = if lit_counts.is_empty() { - height - } else { - height / lit_counts.len() as f32 - }; - let seg_w = width / SEGMENTS as f32; - for (channel, &lit) in lit_counts.iter().enumerate() { - let y = bounds.top() + px(channel as f32 * channel_h); - for segment in 0..SEGMENTS { - let seg = Bounds::new( - point(bounds.left() + px(segment as f32 * seg_w), y), - size(px((seg_w - 1.0).max(1.0)), px((channel_h - 2.0).max(2.0))), - ); - window.paint_quad(fill(seg, if segment < lit { lit_color } else { dim_color })); - } - // Peak marker. - if let Some(peak) = peaks.get(channel) { - let x = bounds.left() + px((peak.clamp(0.0, 1.0) * width) - 1.0); - let marker = Bounds::new(point(x, y), size(px(2.0), px((channel_h - 2.0).max(2.0)))); - window.paint_quad(fill(marker, peak_color)); - } - } - } - MeterOrientation::Vertical => { - // Channels side by side; segments stack bottom to top, - // lit from the bottom like an equalizer column. - let channel_w = if lit_counts.is_empty() { - width - } else { - width / lit_counts.len() as f32 - }; - let seg_h = height / SEGMENTS as f32; - for (channel, &lit) in lit_counts.iter().enumerate() { - let x = bounds.left() + px(channel as f32 * channel_w); - for segment in 0..SEGMENTS { - let y = bounds.bottom() - px((segment + 1) as f32 * seg_h); - let seg = Bounds::new( - point(x, y), - size(px((channel_w - 2.0).max(2.0)), px((seg_h - 1.0).max(1.0))), - ); - window.paint_quad(fill(seg, if segment < lit { lit_color } else { dim_color })); - } - // Peak marker. - if let Some(peak) = peaks.get(channel) { - let y = bounds.bottom() - px(peak.clamp(0.0, 1.0) * height); - let marker = Bounds::new(point(x, y), size(px((channel_w - 2.0).max(2.0)), px(2.0))); - window.paint_quad(fill(marker, peak_color)); - } - } - } - } - }, - ) - .size_full() - } + match orientation { + MeterOrientation::Horizontal => { + let channel_h = if lit_counts.is_empty() { + height + } else { + height / lit_counts.len() as f32 + }; + let seg_w = width / SEGMENTS as f32; + for (channel, &lit) in lit_counts.iter().enumerate() { + let y = bounds.top() + px(channel as f32 * channel_h); + for segment in 0..SEGMENTS { + let seg = Bounds::new( + point(bounds.left() + px(segment as f32 * seg_w), y), + size( + px((seg_w - 1.0).max(1.0)), + px((channel_h - 2.0).max(2.0)), + ), + ); + window.paint_quad(fill( + seg, + if segment < lit { lit_color } else { dim_color }, + )); + } + // Peak marker. + if let Some(peak) = peaks.get(channel) { + let x = bounds.left() + px((peak.clamp(0.0, 1.0) * width) - 1.0); + let marker = Bounds::new( + point(x, y), + size(px(2.0), px((channel_h - 2.0).max(2.0))), + ); + window.paint_quad(fill(marker, peak_color)); + } + } + } + MeterOrientation::Vertical => { + // Channels side by side; segments stack bottom to top, + // lit from the bottom like an equalizer column. + let channel_w = if lit_counts.is_empty() { + width + } else { + width / lit_counts.len() as f32 + }; + let seg_h = height / SEGMENTS as f32; + for (channel, &lit) in lit_counts.iter().enumerate() { + let x = bounds.left() + px(channel as f32 * channel_w); + for segment in 0..SEGMENTS { + let y = bounds.bottom() - px((segment + 1) as f32 * seg_h); + let seg = Bounds::new( + point(x, y), + size( + px((channel_w - 2.0).max(2.0)), + px((seg_h - 1.0).max(1.0)), + ), + ); + window.paint_quad(fill( + seg, + if segment < lit { lit_color } else { dim_color }, + )); + } + // Peak marker. + if let Some(peak) = peaks.get(channel) { + let y = bounds.bottom() - px(peak.clamp(0.0, 1.0) * height); + let marker = Bounds::new( + point(x, y), + size(px((channel_w - 2.0).max(2.0)), px(2.0)), + ); + window.paint_quad(fill(marker, peak_color)); + } + } + } + } + }, + ) + .size_full() + } } #[cfg(test)] mod tests { - use super::*; - use crate::scopes::meter_lit_segments; - use gpui::{Entity, Render, TestAppContext, Window, div, px, size}; + use super::*; + use crate::scopes::meter_lit_segments; + use gpui::{Entity, Render, TestAppContext, Window, div, px, size}; - #[test] - fn meter_math_matches_scope_core() { - assert_eq!(meter_lit_segments(0.0, SEGMENTS), 0); - assert_eq!(meter_lit_segments(0.5, SEGMENTS), SEGMENTS / 2); - } + #[test] + fn meter_math_matches_scope_core() { + assert_eq!(meter_lit_segments(0.0, SEGMENTS), 0); + assert_eq!(meter_lit_segments(0.5, SEGMENTS), SEGMENTS / 2); + } - struct MockAudio(Vec); - impl AudioMeterDataSource for MockAudio { - fn levels(&self) -> Vec { - self.0.clone() - } - } + struct MockAudio(Vec); + impl AudioMeterDataSource for MockAudio { + fn levels(&self) -> Vec { + self.0.clone() + } + } - struct Host { - meter: Entity>, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.meter.clone()) - } - } + struct Host { + meter: Entity>, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.meter.clone()) + } + } - #[gpui::test] - async fn meter_renders_and_decays_peak(cx: &mut TestAppContext) { - - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { - let audio = cx.new(|_| MockAudio(vec![0.8, 0.2])); - let meter = cx.new(|cx| AudioLevelMeter::new(4, audio, window, cx)); - Host { meter } - }); - cx.run_until_parked(); + #[gpui::test] + async fn meter_renders_and_decays_peak(cx: &mut TestAppContext) { + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { + let audio = cx.new(|_| MockAudio(vec![0.8, 0.2])); + let meter = cx.new(|cx| AudioLevelMeter::new(4, audio, window, cx)); + Host { meter } + }); + cx.run_until_parked(); - // update() refreshes peaks from levels. - let (peaks, levels) = window - .update(cx, |host, _, cx| { - host.meter.update(cx, |meter, cx| meter.update(cx)); - let levels = host.meter.read(cx).levels(cx); - let peaks = host.meter.read(cx).peak.clone(); - (peaks, levels) - }) - .unwrap(); - assert_eq!(levels, vec![0.8, 0.2]); - // Peaks track the levels on the first update. - assert!((peaks[0] - 0.8).abs() < 0.001); - } + // update() refreshes peaks from levels. + let (peaks, levels) = window + .update(cx, |host, _, cx| { + host.meter.update(cx, |meter, cx| meter.update(cx)); + let levels = host.meter.read(cx).levels(cx); + let peaks = host.meter.read(cx).peak.clone(); + (peaks, levels) + }) + .unwrap(); + assert_eq!(levels, vec![0.8, 0.2]); + // Peaks track the levels on the first update. + assert!((peaks[0] - 0.8).abs() < 0.001); + } - #[gpui::test] - async fn vertical_meter_renders_in_a_narrow_strip(cx: &mut TestAppContext) { - // The Oak transport design needs a 26px-wide vertical strip: channels - // side by side, segments lit bottom to top. Render one at that exact - // size and exercise the paint path (the orientation default is - // horizontal, so this also covers the builder). - struct StripHost { - meter: Entity>, - } - impl Render for StripHost { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .child(self.meter.clone()) - } - } + #[gpui::test] + async fn vertical_meter_renders_in_a_narrow_strip(cx: &mut TestAppContext) { + // The Oak transport design needs a 26px-wide vertical strip: channels + // side by side, segments lit bottom to top. Render one at that exact + // size and exercise the paint path (the orientation default is + // horizontal, so this also covers the builder). + struct StripHost { + meter: Entity>, + } + impl Render for StripHost { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.meter.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(26.0), px(200.0)), |window, cx| { - let audio = cx.new(|_| MockAudio(vec![0.7])); - let meter = cx.new(|cx| { - AudioLevelMeter::new(5, audio, window, cx) - .with_orientation(MeterOrientation::Vertical) - }); - assert_eq!(meter.read(cx).orientation(), MeterOrientation::Vertical); - StripHost { meter } - }); - cx.run_until_parked(); - window - .update(cx, |host, _, cx| { - host.meter.update(cx, |meter, cx| meter.update(cx)); - }) - .unwrap(); - // Still valid after a vertical render + peak update. - assert!(window - .update(cx, |host, _, cx| host.meter.read(cx).orientation()) - .unwrap() - == MeterOrientation::Vertical); - } + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(26.0), px(200.0)), |window, cx| { + let audio = cx.new(|_| MockAudio(vec![0.7])); + let meter = cx.new(|cx| { + AudioLevelMeter::new(5, audio, window, cx) + .with_orientation(MeterOrientation::Vertical) + }); + assert_eq!(meter.read(cx).orientation(), MeterOrientation::Vertical); + StripHost { meter } + }); + cx.run_until_parked(); + window + .update(cx, |host, _, cx| { + host.meter.update(cx, |meter, cx| meter.update(cx)); + }) + .unwrap(); + // Still valid after a vertical render + peak update. + assert!( + window + .update(cx, |host, _, cx| host.meter.read(cx).orientation()) + .unwrap() == MeterOrientation::Vertical + ); + } - #[test] - fn meter_orientation_defaults_to_horizontal() { - assert_eq!(MeterOrientation::default(), MeterOrientation::Horizontal); - assert_ne!(MeterOrientation::Horizontal, MeterOrientation::Vertical); - } + #[test] + fn meter_orientation_defaults_to_horizontal() { + assert_eq!(MeterOrientation::default(), MeterOrientation::Horizontal); + assert_ne!(MeterOrientation::Horizontal, MeterOrientation::Vertical); + } } diff --git a/crates/gpui_widgets/src/checkbox.rs b/crates/gpui_widgets/src/checkbox.rs index 9de6d7db73..8e1db7ae2b 100644 --- a/crates/gpui_widgets/src/checkbox.rs +++ b/crates/gpui_widgets/src/checkbox.rs @@ -5,295 +5,318 @@ //! its model and calls [`CheckBox::set_state`] (which also repaints) when it //! accepts. The widget never changes its own state on click. -use gpui::{ - App, Bounds, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, - Hsla, KeyDownEvent, Pixels, Render, Window, canvas, colors::DefaultColors, div, fill, point, - prelude::*, px, size, -}; use gpui::PathBuilder; +use gpui::{ + App, Bounds, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Hsla, + KeyDownEvent, Pixels, Render, Window, canvas, colors::DefaultColors, div, fill, point, + prelude::*, px, size, +}; /// The display state of a checkbox. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CheckState { - /// The box is empty. - Unchecked, - /// The box is filled with a check mark. - Checked, - /// The box shows a horizontal bar (partially checked). - Indeterminate, + /// The box is empty. + Unchecked, + /// The box is filled with a check mark. + Checked, + /// The box shows a horizontal bar (partially checked). + Indeterminate, } impl CheckState { - /// The next state after a click. - /// - /// With tri-state enabled the cycle is - /// `Unchecked -> Checked -> Indeterminate -> Unchecked`; otherwise - /// `Unchecked <-> Checked`. - pub fn toggled(self, tri_state: bool) -> Self { - match (self, tri_state) { - (CheckState::Unchecked, _) => CheckState::Checked, - (CheckState::Checked, true) => CheckState::Indeterminate, - (CheckState::Checked, false) => CheckState::Unchecked, - (CheckState::Indeterminate, _) => CheckState::Unchecked, - } - } + /// The next state after a click. + /// + /// With tri-state enabled the cycle is + /// `Unchecked -> Checked -> Indeterminate -> Unchecked`; otherwise + /// `Unchecked <-> Checked`. + pub fn toggled(self, tri_state: bool) -> Self { + match (self, tri_state) { + (CheckState::Unchecked, _) => CheckState::Checked, + (CheckState::Checked, true) => CheckState::Indeterminate, + (CheckState::Checked, false) => CheckState::Unchecked, + (CheckState::Indeterminate, _) => CheckState::Unchecked, + } + } } /// A request emitted when a checkbox is toggled. #[derive(Debug, Clone, PartialEq)] pub enum CheckBoxEvent { - /// The user clicked (or pressed space/enter on) the box. - Toggled { - /// The control's stable id. - control: usize, - /// The state the control should move to. - state: CheckState, - }, + /// The user clicked (or pressed space/enter on) the box. + Toggled { + /// The control's stable id. + control: usize, + /// The state the control should move to. + state: CheckState, + }, } /// A single checkbox row (box + optional label). pub struct CheckBox { - control: usize, - state: CheckState, - label: Option, - enabled: bool, - tri_state: bool, - focus_handle: FocusHandle, + control: usize, + state: CheckState, + label: Option, + enabled: bool, + tri_state: bool, + focus_handle: FocusHandle, } impl CheckBox { - /// Create a checkbox for `control` in `state`. - pub fn new( - control: usize, - state: CheckState, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - state, - label: None, - enabled: true, - tri_state: false, - focus_handle: cx.focus_handle(), - } - } + /// Create a checkbox for `control` in `state`. + pub fn new( + control: usize, + state: CheckState, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + state, + label: None, + enabled: true, + tri_state: false, + focus_handle: cx.focus_handle(), + } + } - /// Attach a label shown to the right of the box. - pub fn with_label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } + /// Attach a label shown to the right of the box. + pub fn with_label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } - /// Enable the tri-state cycle. - pub fn with_tri_state(mut self, tri_state: bool) -> Self { - self.tri_state = tri_state; - self - } + /// Enable the tri-state cycle. + pub fn with_tri_state(mut self, tri_state: bool) -> Self { + self.tri_state = tri_state; + self + } - /// Enable or disable the control (disabled boxes ignore clicks). - pub fn with_enabled(mut self, enabled: bool) -> Self { - self.enabled = enabled; - self - } + /// Enable or disable the control (disabled boxes ignore clicks). + pub fn with_enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } - /// The current display state. - pub fn state(&self) -> CheckState { - self.state - } + /// The current display state. + pub fn state(&self) -> CheckState { + self.state + } - /// Apply a new display state (from the host) and repaint. - pub fn set_state(&mut self, state: CheckState, cx: &mut Context) { - if self.state != state { - self.state = state; - cx.notify(); - } - } + /// Apply a new display state (from the host) and repaint. + pub fn set_state(&mut self, state: CheckState, cx: &mut Context) { + if self.state != state { + self.state = state; + cx.notify(); + } + } - fn emit_toggle(&self, cx: &mut Context) { - if !self.enabled { - return; - } - cx.emit(CheckBoxEvent::Toggled { - control: self.control, - state: self.state.toggled(self.tri_state), - }); - cx.notify(); - } + fn emit_toggle(&self, cx: &mut Context) { + if !self.enabled { + return; + } + cx.emit(CheckBoxEvent::Toggled { + control: self.control, + state: self.state.toggled(self.tri_state), + }); + cx.notify(); + } } impl EventEmitter for CheckBox {} impl Focusable for CheckBox { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for CheckBox { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let state = self.state; - let enabled = self.enabled; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let state = self.state; + let enabled = self.enabled; - let mut box_el = div() - .id(ElementId::named_usize("gpui-widgets-checkbox", self.control)) - .size(px(18.0)) - .rounded(px(4.0)) - .border_1() - .border_color(if enabled { colors.border } else { colors.disabled }) - .bg(if state == CheckState::Checked { - colors.selected - } else { - colors.background - }) - .track_focus(&self.focus_handle) - .cursor_pointer() - .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit_toggle(cx); - cx.stop_propagation(); - })) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - if matches!(event.keystroke.key.as_str(), "space" | "enter") { - this.emit_toggle(cx); - } - })) - .child(canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, cx| { - paint_check(bounds, state, enabled, window, cx); - }, - )); + let mut box_el = div() + .id(ElementId::named_usize( + "gpui-widgets-checkbox", + self.control, + )) + .size(px(18.0)) + .rounded(px(4.0)) + .border_1() + .border_color(if enabled { + colors.border + } else { + colors.disabled + }) + .bg(if state == CheckState::Checked { + colors.selected + } else { + colors.background + }) + .track_focus(&self.focus_handle) + .cursor_pointer() + .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit_toggle(cx); + cx.stop_propagation(); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + if matches!(event.keystroke.key.as_str(), "space" | "enter") { + this.emit_toggle(cx); + } + })) + .child(canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, cx| { + paint_check(bounds, state, enabled, window, cx); + }, + )); - if !enabled { - box_el = box_el.opacity(0.45); - } + if !enabled { + box_el = box_el.opacity(0.45); + } - let mut row = div() - .flex() - .items_center() - .gap(px(6.0)) - .child(box_el); - if let Some(label) = self.label.clone() { - row = row.child( - div() - .text_color(if enabled { colors.text } else { colors.disabled }) - .child(label), - ); - } - row - } + let mut row = div().flex().items_center().gap(px(6.0)).child(box_el); + if let Some(label) = self.label.clone() { + row = row.child( + div() + .text_color(if enabled { + colors.text + } else { + colors.disabled + }) + .child(label), + ); + } + row + } } fn paint_check( - bounds: Bounds, - state: CheckState, - enabled: bool, - window: &mut Window, - cx: &mut App, + bounds: Bounds, + state: CheckState, + enabled: bool, + window: &mut Window, + cx: &mut App, ) { - let colors = cx.default_colors().clone(); - let stroke_color = if enabled { Hsla::from(colors.selected_text) } else { Hsla::from(colors.disabled) }; - let mid_y = bounds.center().y; + let colors = cx.default_colors().clone(); + let stroke_color = if enabled { + Hsla::from(colors.selected_text) + } else { + Hsla::from(colors.disabled) + }; + let mid_y = bounds.center().y; - match state { - CheckState::Unchecked => {} - CheckState::Indeterminate => { - // A centered horizontal bar. - let bar = Bounds::new( - point(bounds.left() + px(3.0), mid_y - px(1.0)), - size(bounds.size.width - px(6.0), px(2.0)), - ); - window.paint_quad(fill(bar, stroke_color)); - } - CheckState::Checked => { - let mut check = PathBuilder::stroke(px(2.0)); - check.move_to(point(bounds.left() + px(4.0), mid_y)); - check.line_to(point(bounds.left() + px(8.0), bounds.bottom() - px(4.0))); - check.line_to(point(bounds.right() - px(3.0), bounds.top() + px(4.0))); - if let Ok(path) = check.build() { - window.paint_path(path, stroke_color); - } - } - } + match state { + CheckState::Unchecked => {} + CheckState::Indeterminate => { + // A centered horizontal bar. + let bar = Bounds::new( + point(bounds.left() + px(3.0), mid_y - px(1.0)), + size(bounds.size.width - px(6.0), px(2.0)), + ); + window.paint_quad(fill(bar, stroke_color)); + } + CheckState::Checked => { + let mut check = PathBuilder::stroke(px(2.0)); + check.move_to(point(bounds.left() + px(4.0), mid_y)); + check.line_to(point(bounds.left() + px(8.0), bounds.bottom() - px(4.0))); + check.line_to(point(bounds.right() - px(3.0), bounds.top() + px(4.0))); + if let Ok(path) = check.build() { + window.paint_path(path, stroke_color); + } + } + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext}; + use super::*; + use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext}; - #[test] - fn binary_toggle_cycles() { - assert_eq!(CheckState::Unchecked.toggled(false), CheckState::Checked); - assert_eq!(CheckState::Checked.toggled(false), CheckState::Unchecked); - } + #[test] + fn binary_toggle_cycles() { + assert_eq!(CheckState::Unchecked.toggled(false), CheckState::Checked); + assert_eq!(CheckState::Checked.toggled(false), CheckState::Unchecked); + } - #[test] - fn tri_state_toggle_cycles() { - assert_eq!(CheckState::Unchecked.toggled(true), CheckState::Checked); - assert_eq!(CheckState::Checked.toggled(true), CheckState::Indeterminate); - assert_eq!(CheckState::Indeterminate.toggled(true), CheckState::Unchecked); - } + #[test] + fn tri_state_toggle_cycles() { + assert_eq!(CheckState::Unchecked.toggled(true), CheckState::Checked); + assert_eq!(CheckState::Checked.toggled(true), CheckState::Indeterminate); + assert_eq!( + CheckState::Indeterminate.toggled(true), + CheckState::Unchecked + ); + } - #[test] - fn states_are_distinct() { - assert_ne!(CheckState::Unchecked, CheckState::Checked); - assert_ne!(CheckState::Checked, CheckState::Indeterminate); - } + #[test] + fn states_are_distinct() { + assert_ne!(CheckState::Unchecked, CheckState::Checked); + assert_ne!(CheckState::Checked, CheckState::Indeterminate); + } - #[gpui::test] - async fn click_emits_toggle_request(cx: &mut TestAppContext) { + #[gpui::test] + async fn click_emits_toggle_request(cx: &mut TestAppContext) { + struct Host { + checkbox: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.checkbox.clone()) + } + } - struct Host { - checkbox: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.checkbox.clone()) - } - } + // `default_colors()` requires the global (not initialized in tests). + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { + let checkbox = + cx.new(|cx| CheckBox::new(1, CheckState::Unchecked, window, cx).with_label("Mute")); + let host = Host { + checkbox, + events: Vec::new(), + }; + cx.subscribe( + &host.checkbox, + |host: &mut Host, + _c: Entity, + event: &CheckBoxEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - // `default_colors()` requires the global (not initialized in tests). - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { - let checkbox = cx.new(|cx| { - CheckBox::new(1, CheckState::Unchecked, window, cx).with_label("Mute") - }); let host = Host { - checkbox, - events: Vec::new(), - }; - cx.subscribe( - &host.checkbox, - |host: &mut Host, - _c: Entity, - event: &CheckBoxEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + cx.simulate_click(point(px(9.0), px(9.0)), Modifiers::none()); + cx.run_until_parked(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - cx.simulate_click(point(px(9.0), px(9.0)), Modifiers::none()); - cx.run_until_parked(); - - let (state, emitted) = cx.read(|app| { - let host = host.read(app); - ( - host.checkbox.read(app).state(), - host.events.iter().any(|e| { - matches!(e, CheckBoxEvent::Toggled { control: 1, state: CheckState::Checked }) - }), - ) - }); - // The widget does not mutate itself; the host must apply the request. - assert_eq!(state, CheckState::Unchecked); - assert!(emitted); - } + let (state, emitted) = cx.read(|app| { + let host = host.read(app); + ( + host.checkbox.read(app).state(), + host.events.iter().any(|e| { + matches!( + e, + CheckBoxEvent::Toggled { + control: 1, + state: CheckState::Checked + } + ) + }), + ) + }); + // The widget does not mutate itself; the host must apply the request. + assert_eq!(state, CheckState::Unchecked); + assert!(emitted); + } } diff --git a/crates/gpui_widgets/src/color/hsv.rs b/crates/gpui_widgets/src/color/hsv.rs index 8d4bb21a69..0a3bcb0b99 100644 --- a/crates/gpui_widgets/src/color/hsv.rs +++ b/crates/gpui_widgets/src/color/hsv.rs @@ -3,139 +3,139 @@ /// A color in the HSV model: `h` in degrees `0..360`, `s` and `v` in `0..1`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct HsvColor { - /// Hue in degrees, `0..360` (wraps). - pub h: f32, - /// Saturation, `0..1`. - pub s: f32, - /// Value (brightness), `0..1`. - pub v: f32, + /// Hue in degrees, `0..360` (wraps). + pub h: f32, + /// Saturation, `0..1`. + pub s: f32, + /// Value (brightness), `0..1`. + pub v: f32, } impl HsvColor { - /// Create a color, clamping components into range. - pub fn new(h: f32, s: f32, v: f32) -> Self { - Self { - h: h.rem_euclid(360.0), - s: s.clamp(0.0, 1.0), - v: v.clamp(0.0, 1.0), - } - } + /// Create a color, clamping components into range. + pub fn new(h: f32, s: f32, v: f32) -> Self { + Self { + h: h.rem_euclid(360.0), + s: s.clamp(0.0, 1.0), + v: v.clamp(0.0, 1.0), + } + } - /// Convert from linear RGB components in `0..1`. - pub fn from_rgb(r: f32, g: f32, b: f32) -> Self { - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let delta = max - min; - let h = if delta == 0.0 { - 0.0 - } else if max == r { - 60.0 * (((g - b) / delta).rem_euclid(6.0)) - } else if max == g { - 60.0 * ((b - r) / delta + 2.0) - } else { - 60.0 * ((r - g) / delta + 4.0) - }; - let s = if max == 0.0 { 0.0 } else { delta / max }; - Self::new(h, s, max) - } + /// Convert from linear RGB components in `0..1`. + pub fn from_rgb(r: f32, g: f32, b: f32) -> Self { + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let delta = max - min; + let h = if delta == 0.0 { + 0.0 + } else if max == r { + 60.0 * (((g - b) / delta).rem_euclid(6.0)) + } else if max == g { + 60.0 * ((b - r) / delta + 2.0) + } else { + 60.0 * ((r - g) / delta + 4.0) + }; + let s = if max == 0.0 { 0.0 } else { delta / max }; + Self::new(h, s, max) + } - /// Convert to linear RGB components in `0..1`. - pub fn to_rgb(self) -> (f32, f32, f32) { - let h = (self.h.rem_euclid(360.0)) / 60.0; - let c = self.v * self.s; - let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs()); - let m = self.v - c; - let (r, g, b) = match h as u32 { - 0 => (c, x, 0.0), - 1 => (x, c, 0.0), - 2 => (0.0, c, x), - 3 => (0.0, x, c), - 4 => (x, 0.0, c), - _ => (c, 0.0, x), - }; - (r + m, g + m, b + m) - } + /// Convert to linear RGB components in `0..1`. + pub fn to_rgb(self) -> (f32, f32, f32) { + let h = (self.h.rem_euclid(360.0)) / 60.0; + let c = self.v * self.s; + let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs()); + let m = self.v - c; + let (r, g, b) = match h as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + (r + m, g + m, b + m) + } - /// Convert to RGBA components in `0..255` (alpha given separately). - pub fn to_rgba_u8(self, alpha: u8) -> [u8; 4] { - let (r, g, b) = self.to_rgb(); - [ - (r * 255.0).round().clamp(0.0, 255.0) as u8, - (g * 255.0).round().clamp(0.0, 255.0) as u8, - (b * 255.0).round().clamp(0.0, 255.0) as u8, - alpha, - ] - } + /// Convert to RGBA components in `0..255` (alpha given separately). + pub fn to_rgba_u8(self, alpha: u8) -> [u8; 4] { + let (r, g, b) = self.to_rgb(); + [ + (r * 255.0).round().clamp(0.0, 255.0) as u8, + (g * 255.0).round().clamp(0.0, 255.0) as u8, + (b * 255.0).round().clamp(0.0, 255.0) as u8, + alpha, + ] + } - /// The fully-saturated hue at maximum value: `hsv(h, 1, 1)`. - pub fn hue_swatch(h: f32) -> Self { - Self::new(h, 1.0, 1.0) - } + /// The fully-saturated hue at maximum value: `hsv(h, 1, 1)`. + pub fn hue_swatch(h: f32) -> Self { + Self::new(h, 1.0, 1.0) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn approx(a: f32, b: f32) -> bool { - (a - b).abs() < 0.001 - } + fn approx(a: f32, b: f32) -> bool { + (a - b).abs() < 0.001 + } - #[test] - fn primary_colors_round_trip() { - // Red. - assert!(approx(HsvColor::from_rgb(1.0, 0.0, 0.0).h, 0.0)); - // Green. - assert!(approx(HsvColor::from_rgb(0.0, 1.0, 0.0).h, 120.0)); - // Blue. - assert!(approx(HsvColor::from_rgb(0.0, 0.0, 1.0).h, 240.0)); - // Yellow. - assert!(approx(HsvColor::from_rgb(1.0, 1.0, 0.0).h, 60.0)); - } + #[test] + fn primary_colors_round_trip() { + // Red. + assert!(approx(HsvColor::from_rgb(1.0, 0.0, 0.0).h, 0.0)); + // Green. + assert!(approx(HsvColor::from_rgb(0.0, 1.0, 0.0).h, 120.0)); + // Blue. + assert!(approx(HsvColor::from_rgb(0.0, 0.0, 1.0).h, 240.0)); + // Yellow. + assert!(approx(HsvColor::from_rgb(1.0, 1.0, 0.0).h, 60.0)); + } - #[test] - fn rgb_to_hsv_to_rgb_round_trip() { - for (r, g, b) in [ - (0.2, 0.4, 0.6), - (1.0, 0.5, 0.25), - (0.9, 0.9, 0.9), - (0.1, 0.1, 0.1), - ] { - let hsv = HsvColor::from_rgb(r, g, b); - let (r2, g2, b2) = hsv.to_rgb(); - assert!(approx(r, r2) && approx(g, g2) && approx(b, b2), "{hsv:?}"); - } - } + #[test] + fn rgb_to_hsv_to_rgb_round_trip() { + for (r, g, b) in [ + (0.2, 0.4, 0.6), + (1.0, 0.5, 0.25), + (0.9, 0.9, 0.9), + (0.1, 0.1, 0.1), + ] { + let hsv = HsvColor::from_rgb(r, g, b); + let (r2, g2, b2) = hsv.to_rgb(); + assert!(approx(r, r2) && approx(g, g2) && approx(b, b2), "{hsv:?}"); + } + } - #[test] - fn saturation_and_value_extremes() { - // Gray has zero saturation. - let gray = HsvColor::from_rgb(0.5, 0.5, 0.5); - assert!(approx(gray.s, 0.0)); - assert!(approx(gray.v, 0.5)); - // Black has zero value. - let black = HsvColor::from_rgb(0.0, 0.0, 0.0); - assert!(approx(black.v, 0.0)); - // White is full value, zero saturation. - let white = HsvColor::from_rgb(1.0, 1.0, 1.0); - assert!(approx(white.v, 1.0) && approx(white.s, 0.0)); - } + #[test] + fn saturation_and_value_extremes() { + // Gray has zero saturation. + let gray = HsvColor::from_rgb(0.5, 0.5, 0.5); + assert!(approx(gray.s, 0.0)); + assert!(approx(gray.v, 0.5)); + // Black has zero value. + let black = HsvColor::from_rgb(0.0, 0.0, 0.0); + assert!(approx(black.v, 0.0)); + // White is full value, zero saturation. + let white = HsvColor::from_rgb(1.0, 1.0, 1.0); + assert!(approx(white.v, 1.0) && approx(white.s, 0.0)); + } - #[test] - fn to_rgba_u8_clamps() { - let red = HsvColor::new(0.0, 1.0, 1.0); - assert_eq!(red.to_rgba_u8(255), [255, 0, 0, 255]); - let black = HsvColor::new(120.0, 1.0, 0.0); - assert_eq!(black.to_rgba_u8(128), [0, 0, 0, 128]); - } + #[test] + fn to_rgba_u8_clamps() { + let red = HsvColor::new(0.0, 1.0, 1.0); + assert_eq!(red.to_rgba_u8(255), [255, 0, 0, 255]); + let black = HsvColor::new(120.0, 1.0, 0.0); + assert_eq!(black.to_rgba_u8(128), [0, 0, 0, 128]); + } - #[test] - fn hue_wraps() { - assert!(approx(HsvColor::new(360.0, 1.0, 1.0).h, 0.0)); - assert!(approx(HsvColor::new(-30.0, 1.0, 1.0).h, 330.0)); - // Hue 360 == hue 0: same color. - let (r1, g1, b1) = HsvColor::new(0.0, 1.0, 1.0).to_rgb(); - let (r2, g2, b2) = HsvColor::new(360.0, 1.0, 1.0).to_rgb(); - assert!((r1 - r2).abs() < 0.001 && (g1 - g2).abs() < 0.001 && (b1 - b2).abs() < 0.001); - } + #[test] + fn hue_wraps() { + assert!(approx(HsvColor::new(360.0, 1.0, 1.0).h, 0.0)); + assert!(approx(HsvColor::new(-30.0, 1.0, 1.0).h, 330.0)); + // Hue 360 == hue 0: same color. + let (r1, g1, b1) = HsvColor::new(0.0, 1.0, 1.0).to_rgb(); + let (r2, g2, b2) = HsvColor::new(360.0, 1.0, 1.0).to_rgb(); + assert!((r1 - r2).abs() < 0.001 && (g1 - g2).abs() < 0.001 && (b1 - b2).abs() < 0.001); + } } diff --git a/crates/gpui_widgets/src/color/mod.rs b/crates/gpui_widgets/src/color/mod.rs index 06990a93f3..91be8bdeb4 100644 --- a/crates/gpui_widgets/src/color/mod.rs +++ b/crates/gpui_widgets/src/color/mod.rs @@ -9,11 +9,10 @@ mod hsv; use gpui::{ - Anchor, App, Bounds, ClickEvent, Context, DragMoveEvent, ElementId, Entity, EventEmitter, - FocusHandle, Focusable, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, Pixels, Point, Rgba, - Render, Window, anchored, canvas, colors::DefaultColors, deferred, div, fill, - linear_color_stop, linear_gradient, - point, prelude::*, px, size, + Anchor, App, Bounds, ClickEvent, Context, DragMoveEvent, ElementId, Entity, EventEmitter, + FocusHandle, Focusable, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, Pixels, Point, Render, + Rgba, Window, anchored, canvas, colors::DefaultColors, deferred, div, fill, linear_color_stop, + linear_gradient, point, prelude::*, px, size, }; pub use hsv::HsvColor; @@ -30,32 +29,32 @@ const HUE_STRIP_SEGMENTS: usize = 48; /// A request emitted by a color picker. #[derive(Debug, Clone, PartialEq)] pub enum ColorPickerEvent { - /// The color changed (SV/hue drag, RGBA edit). - ColorChanged { - /// The control's stable id. - control: usize, - /// The new color. - color: Rgba, - }, - /// The popup was opened. - MenuOpened { - /// The control's stable id. - control: usize, - }, - /// The popup was closed. - MenuClosed { - /// The control's stable id. - control: usize, - }, + /// The color changed (SV/hue drag, RGBA edit). + ColorChanged { + /// The control's stable id. + control: usize, + /// The new color. + color: Rgba, + }, + /// The popup was opened. + MenuOpened { + /// The control's stable id. + control: usize, + }, + /// The popup was closed. + MenuClosed { + /// The control's stable id. + control: usize, + }, } /// Which RGBA channel a spinbox edits. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Channel { - R, - G, - B, - A, + R, + G, + B, + A, } /// Marker payloads for the SV-square and hue-strip drags. @@ -67,591 +66,605 @@ struct HueDrag; struct PickGhost; impl Render for PickGhost { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().w(px(0.0)).h(px(0.0)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(0.0)).h(px(0.0)) + } } /// A swatch button with a popup color picker. pub struct ColorPicker { - control: usize, - hsv: HsvColor, - alpha: u8, - focus_handle: FocusHandle, - open: bool, - popup_position: Point, - was_open_at_down: bool, - /// Bounds of the SV square / hue strip, refreshed each frame so clicks - /// can be converted to coordinates. - sv_bounds: Bounds, - hue_bounds: Bounds, - r_spin: Entity, - g_spin: Entity, - b_spin: Entity, - a_spin: Entity, + control: usize, + hsv: HsvColor, + alpha: u8, + focus_handle: FocusHandle, + open: bool, + popup_position: Point, + was_open_at_down: bool, + /// Bounds of the SV square / hue strip, refreshed each frame so clicks + /// can be converted to coordinates. + sv_bounds: Bounds, + hue_bounds: Bounds, + r_spin: Entity, + g_spin: Entity, + b_spin: Entity, + a_spin: Entity, } impl ColorPicker { - /// Create a picker for `control` showing `color`. - pub fn new(control: usize, color: Rgba, window: &mut Window, cx: &mut Context) -> Self { - let hsv = HsvColor::from_rgb(color.r, color.g, color.b); - let alpha = (color.a * 255.0).round().clamp(0.0, 255.0) as u8; - let r_spin = Self::channel_spinbox(cx, window, Channel::R, (color.r * 255.0) as u8); - let g_spin = Self::channel_spinbox(cx, window, Channel::G, (color.g * 255.0) as u8); - let b_spin = Self::channel_spinbox(cx, window, Channel::B, (color.b * 255.0) as u8); - let a_spin = Self::channel_spinbox(cx, window, Channel::A, alpha); - Self { - control, - hsv, - alpha, - focus_handle: cx.focus_handle(), - open: false, - popup_position: Point::default(), - was_open_at_down: false, - sv_bounds: Bounds::default(), - hue_bounds: Bounds::default(), - r_spin, - g_spin, - b_spin, - a_spin, - } - } + /// Create a picker for `control` showing `color`. + pub fn new(control: usize, color: Rgba, window: &mut Window, cx: &mut Context) -> Self { + let hsv = HsvColor::from_rgb(color.r, color.g, color.b); + let alpha = (color.a * 255.0).round().clamp(0.0, 255.0) as u8; + let r_spin = Self::channel_spinbox(cx, window, Channel::R, (color.r * 255.0) as u8); + let g_spin = Self::channel_spinbox(cx, window, Channel::G, (color.g * 255.0) as u8); + let b_spin = Self::channel_spinbox(cx, window, Channel::B, (color.b * 255.0) as u8); + let a_spin = Self::channel_spinbox(cx, window, Channel::A, alpha); + Self { + control, + hsv, + alpha, + focus_handle: cx.focus_handle(), + open: false, + popup_position: Point::default(), + was_open_at_down: false, + sv_bounds: Bounds::default(), + hue_bounds: Bounds::default(), + r_spin, + g_spin, + b_spin, + a_spin, + } + } - fn channel_spinbox( - cx: &mut Context, - window: &mut Window, - channel: Channel, - value: u8, - ) -> Entity { - let model = SliderModel::new(ValueKind::Integer, 0.0, 255.0, 1.0, value as f64); - let spin = cx.new(|cx| SpinBox::new(channel as usize * 1000 + 0, model, window, cx)); - cx.subscribe( - &spin, - move |this: &mut Self, - _s: Entity, - event: &SpinBoxEvent, - cx| { - this.on_channel_edit(channel, event, cx); - }, - ) - .detach(); - spin - } + fn channel_spinbox( + cx: &mut Context, + window: &mut Window, + channel: Channel, + value: u8, + ) -> Entity { + let model = SliderModel::new(ValueKind::Integer, 0.0, 255.0, 1.0, value as f64); + let spin = cx.new(|cx| SpinBox::new(channel as usize * 1000 + 0, model, window, cx)); + cx.subscribe( + &spin, + move |this: &mut Self, _s: Entity, event: &SpinBoxEvent, cx| { + this.on_channel_edit(channel, event, cx); + }, + ) + .detach(); + spin + } - /// The current color. - pub fn color(&self) -> Rgba { - let [r, g, b, a] = self.hsv.to_rgba_u8(self.alpha); - Rgba { - r: r as f32 / 255.0, - g: g as f32 / 255.0, - b: b as f32 / 255.0, - a: a as f32 / 255.0, - } - } + /// The current color. + pub fn color(&self) -> Rgba { + let [r, g, b, a] = self.hsv.to_rgba_u8(self.alpha); + Rgba { + r: r as f32 / 255.0, + g: g as f32 / 255.0, + b: b as f32 / 255.0, + a: a as f32 / 255.0, + } + } - /// Whether the popup is open. - pub fn is_open(&self) -> bool { - self.open - } + /// Whether the popup is open. + pub fn is_open(&self) -> bool { + self.open + } - /// Apply a color from the host and repaint (also syncs the RGBA fields). - pub fn set_color(&mut self, color: Rgba, cx: &mut Context) { - self.hsv = HsvColor::from_rgb(color.r, color.g, color.b); - self.alpha = (color.a * 255.0).round().clamp(0.0, 255.0) as u8; - self.sync_channels(cx); - cx.notify(); - } + /// Apply a color from the host and repaint (also syncs the RGBA fields). + pub fn set_color(&mut self, color: Rgba, cx: &mut Context) { + self.hsv = HsvColor::from_rgb(color.r, color.g, color.b); + self.alpha = (color.a * 255.0).round().clamp(0.0, 255.0) as u8; + self.sync_channels(cx); + cx.notify(); + } - /// Push the current channels into the RGBA spinboxes (no events emitted). - fn sync_channels(&self, cx: &mut Context) { - let [r, g, b, a] = self.hsv.to_rgba_u8(self.alpha); - cx.update_entity(&self.r_spin, |spin, cx| { - spin.set_value(SliderValue::Integer(r as i64), cx); - }); - cx.update_entity(&self.g_spin, |spin, cx| { - spin.set_value(SliderValue::Integer(g as i64), cx); - }); - cx.update_entity(&self.b_spin, |spin, cx| { - spin.set_value(SliderValue::Integer(b as i64), cx); - }); - cx.update_entity(&self.a_spin, |spin, cx| { - spin.set_value(SliderValue::Integer(a as i64), cx); - }); - } + /// Push the current channels into the RGBA spinboxes (no events emitted). + fn sync_channels(&self, cx: &mut Context) { + let [r, g, b, a] = self.hsv.to_rgba_u8(self.alpha); + cx.update_entity(&self.r_spin, |spin, cx| { + spin.set_value(SliderValue::Integer(r as i64), cx); + }); + cx.update_entity(&self.g_spin, |spin, cx| { + spin.set_value(SliderValue::Integer(g as i64), cx); + }); + cx.update_entity(&self.b_spin, |spin, cx| { + spin.set_value(SliderValue::Integer(b as i64), cx); + }); + cx.update_entity(&self.a_spin, |spin, cx| { + spin.set_value(SliderValue::Integer(a as i64), cx); + }); + } - fn on_channel_edit(&mut self, channel: Channel, event: &SpinBoxEvent, cx: &mut Context) { - let value = match event { - SpinBoxEvent::ValueChanged { value, .. } | SpinBoxEvent::EditCommitted { value, .. } => { - *value - } - SpinBoxEvent::EditCancelled { .. } => return, - }; - let v = value.to_f64().round().clamp(0.0, 255.0) as u8; - match channel { - Channel::A => self.alpha = v, - _ => { - let [r, g, b, _] = self.hsv.to_rgba_u8(self.alpha); - let (r, g, b) = match channel { - Channel::R => (v, g, b), - Channel::G => (r, v, b), - Channel::B => (r, g, v), - Channel::A => unreachable!(), - }; - self.hsv = HsvColor::from_rgb( - r as f32 / 255.0, - g as f32 / 255.0, - b as f32 / 255.0, - ); - } - } - self.emit_changed(cx); - } + fn on_channel_edit(&mut self, channel: Channel, event: &SpinBoxEvent, cx: &mut Context) { + let value = match event { + SpinBoxEvent::ValueChanged { value, .. } + | SpinBoxEvent::EditCommitted { value, .. } => *value, + SpinBoxEvent::EditCancelled { .. } => return, + }; + let v = value.to_f64().round().clamp(0.0, 255.0) as u8; + match channel { + Channel::A => self.alpha = v, + _ => { + let [r, g, b, _] = self.hsv.to_rgba_u8(self.alpha); + let (r, g, b) = match channel { + Channel::R => (v, g, b), + Channel::G => (r, v, b), + Channel::B => (r, g, v), + Channel::A => unreachable!(), + }; + self.hsv = HsvColor::from_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0); + } + } + self.emit_changed(cx); + } - fn set_hsv(&mut self, hsv: HsvColor, cx: &mut Context) { - if self.hsv != hsv { - self.hsv = hsv; - self.sync_channels(cx); - self.emit_changed(cx); - } - } + fn set_hsv(&mut self, hsv: HsvColor, cx: &mut Context) { + if self.hsv != hsv { + self.hsv = hsv; + self.sync_channels(cx); + self.emit_changed(cx); + } + } - fn emit_changed(&mut self, cx: &mut Context) { - cx.emit(ColorPickerEvent::ColorChanged { - control: self.control, - color: self.color(), - }); - cx.notify(); - } + fn emit_changed(&mut self, cx: &mut Context) { + cx.emit(ColorPickerEvent::ColorChanged { + control: self.control, + color: self.color(), + }); + cx.notify(); + } - fn open_menu(&mut self, position: Point, cx: &mut Context) { - if !self.open { - self.open = true; - self.popup_position = position; - cx.emit(ColorPickerEvent::MenuOpened { - control: self.control, - }); - cx.notify(); - } - } + fn open_menu(&mut self, position: Point, cx: &mut Context) { + if !self.open { + self.open = true; + self.popup_position = position; + cx.emit(ColorPickerEvent::MenuOpened { + control: self.control, + }); + cx.notify(); + } + } - fn close_menu(&mut self, cx: &mut Context) { - if self.open { - self.open = false; - cx.emit(ColorPickerEvent::MenuClosed { - control: self.control, - }); - cx.notify(); - } - } + fn close_menu(&mut self, cx: &mut Context) { + if self.open { + self.open = false; + cx.emit(ColorPickerEvent::MenuClosed { + control: self.control, + }); + cx.notify(); + } + } - /// Pick a color from the SV square at a window-space position. - fn pick_sv(&mut self, position: Point, cx: &mut Context) { - let bounds = self.sv_bounds; - if f32::from(bounds.size.width) <= 0.0 { - return; - } - let x = ((f32::from(position.x) - f32::from(bounds.left())) / f32::from(bounds.size.width)) - .clamp(0.0, 1.0); - let y = - ((f32::from(position.y) - f32::from(bounds.top())) / f32::from(bounds.size.height)) - .clamp(0.0, 1.0); - self.set_hsv(HsvColor::new(self.hsv.h, x, 1.0 - y), cx); - } + /// Pick a color from the SV square at a window-space position. + fn pick_sv(&mut self, position: Point, cx: &mut Context) { + let bounds = self.sv_bounds; + if f32::from(bounds.size.width) <= 0.0 { + return; + } + let x = ((f32::from(position.x) - f32::from(bounds.left())) / f32::from(bounds.size.width)) + .clamp(0.0, 1.0); + let y = ((f32::from(position.y) - f32::from(bounds.top())) / f32::from(bounds.size.height)) + .clamp(0.0, 1.0); + self.set_hsv(HsvColor::new(self.hsv.h, x, 1.0 - y), cx); + } - fn pick_hue(&mut self, position: Point, cx: &mut Context) { - let bounds = self.hue_bounds; - if f32::from(bounds.size.height) <= 0.0 { - return; - } - let y = ((f32::from(position.y) - f32::from(bounds.top())) / f32::from(bounds.size.height)) - .clamp(0.0, 1.0); - self.set_hsv(HsvColor::new(y * 360.0, self.hsv.s, self.hsv.v), cx); - } + fn pick_hue(&mut self, position: Point, cx: &mut Context) { + let bounds = self.hue_bounds; + if f32::from(bounds.size.height) <= 0.0 { + return; + } + let y = ((f32::from(position.y) - f32::from(bounds.top())) / f32::from(bounds.size.height)) + .clamp(0.0, 1.0); + self.set_hsv(HsvColor::new(y * 360.0, self.hsv.s, self.hsv.v), cx); + } - fn on_key_down(&mut self, event: &KeyDownEvent, cx: &mut Context) { - match event.keystroke.key.as_str() { - "escape" => self.close_menu(cx), - "enter" | "space" => { - if self.open { - self.close_menu(cx); - } else { - self.open_menu(self.popup_position, cx); - } - } - _ => {} - } - } + fn on_key_down(&mut self, event: &KeyDownEvent, cx: &mut Context) { + match event.keystroke.key.as_str() { + "escape" => self.close_menu(cx), + "enter" | "space" => { + if self.open { + self.close_menu(cx); + } else { + self.open_menu(self.popup_position, cx); + } + } + _ => {} + } + } } impl EventEmitter for ColorPicker {} impl Focusable for ColorPicker { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for ColorPicker { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let entity = cx.entity(); - let hue_entity = entity.clone(); - let control = self.control; - let color_hsla = hsv_to_hsla(self.hsv); - let hue = self.hsv.h; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let entity = cx.entity(); + let hue_entity = entity.clone(); + let control = self.control; + let color_hsla = hsv_to_hsla(self.hsv); + let hue = self.hsv.h; - // Swatch field. - let swatch = div() - .id(ElementId::named_usize("gpui-widgets-color-swatch", control)) - .w(px(28.0)) - .h(px(28.0)) - .rounded_md() - .border_1() - .border_color(if self.open { colors.selected } else { colors.border }) - .bg(color_hsla) - .cursor_pointer() - .track_focus(&self.focus_handle) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _event: &MouseDownEvent, _window, _cx| { - this.was_open_at_down = this.open; - }), - ) - .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { - if this.was_open_at_down { - this.close_menu(cx); - } else { - this.open_menu(event.position(), cx); - } - cx.stop_propagation(); - })) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - this.on_key_down(event, cx); - })); + // Swatch field. + let swatch = div() + .id(ElementId::named_usize("gpui-widgets-color-swatch", control)) + .w(px(28.0)) + .h(px(28.0)) + .rounded_md() + .border_1() + .border_color(if self.open { + colors.selected + } else { + colors.border + }) + .bg(color_hsla) + .cursor_pointer() + .track_focus(&self.focus_handle) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _event: &MouseDownEvent, _window, _cx| { + this.was_open_at_down = this.open; + }), + ) + .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { + if this.was_open_at_down { + this.close_menu(cx); + } else { + this.open_menu(event.position(), cx); + } + cx.stop_propagation(); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + this.on_key_down(event, cx); + })); - // SV square + hue strip. - let sv_square = div() - .id(ElementId::named_usize("gpui-widgets-color-sv", control)) - .w(px(SV_SIZE)) - .h(px(SV_SIZE)) - .cursor_crosshair() - .debug_selector(|| "color-sv".into()) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - this.pick_sv(event.position, cx); - }), - ) - .on_drag(SvDrag, pick_ghost) - .on_drag_move( - cx.listener(|this, event: &DragMoveEvent, _window, cx| { - this.pick_sv(event.event.position, cx); - }), - ) - .child( - canvas( - move |bounds, _window, cx| { - entity.update(cx, |this, _| this.sv_bounds = bounds); - bounds - }, - move |bounds, content, window, cx| { - paint_sv_square(bounds, content, hue, window, cx); - }, - ) - .size_full(), - ); + // SV square + hue strip. + let sv_square = div() + .id(ElementId::named_usize("gpui-widgets-color-sv", control)) + .w(px(SV_SIZE)) + .h(px(SV_SIZE)) + .cursor_crosshair() + .debug_selector(|| "color-sv".into()) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.pick_sv(event.position, cx); + }), + ) + .on_drag(SvDrag, pick_ghost) + .on_drag_move( + cx.listener(|this, event: &DragMoveEvent, _window, cx| { + this.pick_sv(event.event.position, cx); + }), + ) + .child( + canvas( + move |bounds, _window, cx| { + entity.update(cx, |this, _| this.sv_bounds = bounds); + bounds + }, + move |bounds, content, window, cx| { + paint_sv_square(bounds, content, hue, window, cx); + }, + ) + .size_full(), + ); - let hue_strip = div() - .id(ElementId::named_usize("gpui-widgets-color-hue", control)) - .w(px(HUE_STRIP_WIDTH)) - .h(px(SV_SIZE)) - .cursor_crosshair() - .debug_selector(|| "color-hue".into()) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - this.pick_hue(event.position, cx); - }), - ) - .on_drag(HueDrag, pick_ghost) - .on_drag_move( - cx.listener(|this, event: &DragMoveEvent, _window, cx| { - this.pick_hue(event.event.position, cx); - }), - ) - .child( - canvas( - move |bounds, _window, cx| { - hue_entity.update(cx, |this, _| this.hue_bounds = bounds); - bounds - }, - move |bounds, content, window, cx| { - paint_hue_strip(bounds, content, window, cx); - }, - ) - .size_full(), - ); + let hue_strip = div() + .id(ElementId::named_usize("gpui-widgets-color-hue", control)) + .w(px(HUE_STRIP_WIDTH)) + .h(px(SV_SIZE)) + .cursor_crosshair() + .debug_selector(|| "color-hue".into()) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.pick_hue(event.position, cx); + }), + ) + .on_drag(HueDrag, pick_ghost) + .on_drag_move( + cx.listener(|this, event: &DragMoveEvent, _window, cx| { + this.pick_hue(event.event.position, cx); + }), + ) + .child( + canvas( + move |bounds, _window, cx| { + hue_entity.update(cx, |this, _| this.hue_bounds = bounds); + bounds + }, + move |bounds, content, window, cx| { + paint_hue_strip(bounds, content, window, cx); + }, + ) + .size_full(), + ); - // The RGBA inputs + eyedropper placeholder. - let rgba_row = div() - .flex() - .gap(px(4.0)) - .child(self.r_spin.clone()) - .child(self.g_spin.clone()) - .child(self.b_spin.clone()) - .child(self.a_spin.clone()); + // The RGBA inputs + eyedropper placeholder. + let rgba_row = div() + .flex() + .gap(px(4.0)) + .child(self.r_spin.clone()) + .child(self.g_spin.clone()) + .child(self.b_spin.clone()) + .child(self.a_spin.clone()); - let eyedropper = div() - .id(ElementId::named_usize("gpui-widgets-color-eye", control)) - .px_2() - .py_1() - .rounded_md() - .border_1() - .border_color(colors.border) - .text_color(colors.disabled) - .opacity(0.7) - .child("吸管(占位)"); + let eyedropper = div() + .id(ElementId::named_usize("gpui-widgets-color-eye", control)) + .px_2() + .py_1() + .rounded_md() + .border_1() + .border_color(colors.border) + .text_color(colors.disabled) + .opacity(0.7) + .child("吸管(占位)"); - let popup = if self.open { - deferred( - anchored() - .position(self.popup_position) - .anchor(Anchor::TopLeft) - .offset(point(px(0.0), px(32.0))) - .snap_to_window_with_margin(px(8.0)) - .child( - div() - .p_2() - .rounded_lg() - .border_1() - .border_color(colors.border) - .bg(colors.container) - .debug_selector(|| "color-popup".into()) - .flex() - .flex_col() - .gap(px(6.0)) - .on_mouse_up_out( - MouseButton::Left, - cx.listener( - |this, _event: &gpui::MouseUpEvent, _window, cx| { - this.close_menu(cx); - }, - ), - ) - .child( - div() - .flex() - .gap(px(4.0)) - .debug_selector(|| "color-row".into()) - .child(sv_square) - .child(hue_strip), - ) - .child(rgba_row) - .child(eyedropper), - ), - ) - .with_priority(1) - } else { - deferred(div()) - }; + let popup = if self.open { + deferred( + anchored() + .position(self.popup_position) + .anchor(Anchor::TopLeft) + .offset(point(px(0.0), px(32.0))) + .snap_to_window_with_margin(px(8.0)) + .child( + div() + .p_2() + .rounded_lg() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .debug_selector(|| "color-popup".into()) + .flex() + .flex_col() + .gap(px(6.0)) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event: &gpui::MouseUpEvent, _window, cx| { + this.close_menu(cx); + }), + ) + .child( + div() + .flex() + .gap(px(4.0)) + .debug_selector(|| "color-row".into()) + .child(sv_square) + .child(hue_strip), + ) + .child(rgba_row) + .child(eyedropper), + ), + ) + .with_priority(1) + } else { + deferred(div()) + }; - div().relative().child(swatch).child(popup) - } + div().relative().child(swatch).child(popup) + } } /// Build the invisible ghost view for SV/hue drags. -fn pick_ghost(_drag: &impl Copy, _offset: Point, _window: &mut Window, cx: &mut App) -> Entity { - cx.new(|_| PickGhost) +fn pick_ghost( + _drag: &impl Copy, + _offset: Point, + _window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|_| PickGhost) } /// Convert HSV to gpui's HSLA (lightness/saturation model). fn hsv_to_hsla(hsv: HsvColor) -> Hsla { - let h = hsv.h.rem_euclid(360.0) / 360.0; - let l = hsv.v * (1.0 - hsv.s / 2.0); - let s = if l <= 0.0 || l >= 1.0 { - 0.0 - } else { - ((hsv.v - l) / l.min(1.0 - l)).clamp(0.0, 1.0) - }; - Hsla { - h, - s, - l, - a: 1.0, - } + let h = hsv.h.rem_euclid(360.0) / 360.0; + let l = hsv.v * (1.0 - hsv.s / 2.0); + let s = if l <= 0.0 || l >= 1.0 { + 0.0 + } else { + ((hsv.v - l) / l.min(1.0 - l)).clamp(0.0, 1.0) + }; + Hsla { h, s, l, a: 1.0 } } fn hue_hsla(h: f32) -> Hsla { - Hsla { - h: h.rem_euclid(360.0) / 360.0, - s: 1.0, - l: 0.5, - a: 1.0, - } + Hsla { + h: h.rem_euclid(360.0) / 360.0, + s: 1.0, + l: 0.5, + a: 1.0, + } } fn paint_sv_square( - bounds: Bounds, - _content: Bounds, - hue: f32, - window: &mut Window, - cx: &mut App, + bounds: Bounds, + _content: Bounds, + hue: f32, + window: &mut Window, + cx: &mut App, ) { - let _ = cx; - // Base: pure hue. - window.paint_quad(fill(bounds, hue_hsla(hue))); - // White fade left -> right (saturation axis). - let white = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 1.0, - }; - let transparent_white = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 0.0, - }; - window.paint_quad(fill( - bounds, - linear_gradient(90.0, linear_color_stop(white, 0.0), linear_color_stop(transparent_white, 1.0)), - )); - // Black fade top -> bottom (value axis). - let transparent_black = Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 0.0, - }; - let black = Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 1.0, - }; - window.paint_quad(fill( - bounds, - linear_gradient(180.0, linear_color_stop(transparent_black, 0.0), linear_color_stop(black, 1.0)), - )); + let _ = cx; + // Base: pure hue. + window.paint_quad(fill(bounds, hue_hsla(hue))); + // White fade left -> right (saturation axis). + let white = Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: 1.0, + }; + let transparent_white = Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: 0.0, + }; + window.paint_quad(fill( + bounds, + linear_gradient( + 90.0, + linear_color_stop(white, 0.0), + linear_color_stop(transparent_white, 1.0), + ), + )); + // Black fade top -> bottom (value axis). + let transparent_black = Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 0.0, + }; + let black = Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 1.0, + }; + window.paint_quad(fill( + bounds, + linear_gradient( + 180.0, + linear_color_stop(transparent_black, 0.0), + linear_color_stop(black, 1.0), + ), + )); } fn paint_hue_strip( - bounds: Bounds, - _content: Bounds, - window: &mut Window, - cx: &mut App, + bounds: Bounds, + _content: Bounds, + window: &mut Window, + cx: &mut App, ) { - let _ = cx; - let height = f32::from(bounds.size.height); - for i in 0..HUE_STRIP_SEGMENTS { - let t0 = i as f32 / HUE_STRIP_SEGMENTS as f32; - let t1 = (i + 1) as f32 / HUE_STRIP_SEGMENTS as f32; - let hue = (t0 + t1) * 180.0; - let segment = Bounds::new( - point(bounds.left(), bounds.top() + px(t0 * height)), - size(bounds.size.width, px((t1 - t0) * height)), - ); - window.paint_quad(fill(segment, hue_hsla(hue))); - } + let _ = cx; + let height = f32::from(bounds.size.height); + for i in 0..HUE_STRIP_SEGMENTS { + let t0 = i as f32 / HUE_STRIP_SEGMENTS as f32; + let t1 = (i + 1) as f32 / HUE_STRIP_SEGMENTS as f32; + let hue = (t0 + t1) * 180.0; + let segment = Bounds::new( + point(bounds.left(), bounds.top() + px(t0 * height)), + size(bounds.size.width, px((t1 - t0) * height)), + ); + window.paint_quad(fill(segment, hue_hsla(hue))); + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Modifiers, TestAppContext, VisualTestContext}; + use super::*; + use gpui::{Modifiers, TestAppContext, VisualTestContext}; - #[test] - fn hsv_to_hsla_matches_rgb() { - // Red: hsv(0,1,1) -> hsla(0,1,0.5) -> rgb(1,0,0). - let hsla = hsv_to_hsla(HsvColor::new(0.0, 1.0, 1.0)); - assert!((hsla.h - 0.0).abs() < 0.001); - assert!((hsla.s - 1.0).abs() < 0.001); - assert!((hsla.l - 0.5).abs() < 0.001); - // Gray: hsv(0,0,0.5) -> hsla(any,0,0.5). - let gray = hsv_to_hsla(HsvColor::new(120.0, 0.0, 0.5)); - assert!(gray.s < 0.001); - assert!((gray.l - 0.5).abs() < 0.001); - } + #[test] + fn hsv_to_hsla_matches_rgb() { + // Red: hsv(0,1,1) -> hsla(0,1,0.5) -> rgb(1,0,0). + let hsla = hsv_to_hsla(HsvColor::new(0.0, 1.0, 1.0)); + assert!((hsla.h - 0.0).abs() < 0.001); + assert!((hsla.s - 1.0).abs() < 0.001); + assert!((hsla.l - 0.5).abs() < 0.001); + // Gray: hsv(0,0,0.5) -> hsla(any,0,0.5). + let gray = hsv_to_hsla(HsvColor::new(120.0, 0.0, 0.5)); + assert!(gray.s < 0.001); + assert!((gray.l - 0.5).abs() < 0.001); + } - #[gpui::test] - async fn clicking_sv_square_changes_color(cx: &mut TestAppContext) { - struct Host { - picker: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.picker.clone()) - } - } + #[gpui::test] + async fn clicking_sv_square_changes_color(cx: &mut TestAppContext) { + struct Host { + picker: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.picker.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(300.0), px(300.0)), |window, cx| { - let picker = cx.new(|cx| { - ColorPicker::new( - 1, - Rgba { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - }, - window, - cx, - ) - }); - let host = Host { - picker, - events: Vec::new(), - }; - cx.subscribe( - &host.picker, - |host: &mut Host, - _p: Entity, - event: &ColorPickerEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(300.0), px(300.0)), |window, cx| { + let picker = cx.new(|cx| { + ColorPicker::new( + 1, + Rgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0, + }, + window, + cx, + ) + }); + let host = Host { + picker, + events: Vec::new(), + }; + cx.subscribe( + &host.picker, + |host: &mut Host, + _p: Entity, + event: &ColorPickerEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Open the picker by clicking the swatch. - cx.simulate_click(point(px(14.0), px(14.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Open the picker by clicking the swatch. + cx.simulate_click(point(px(14.0), px(14.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let sv_bounds = cx - .debug_bounds("color-sv") - .expect("SV square should be rendered"); - eprintln!("bounds: sv={:?} row={:?} popup={:?}", - cx.debug_bounds("color-sv"), - cx.debug_bounds("color-row"), - cx.debug_bounds("color-popup")); - // Click near the top-left of the SV square: that is white. - let click = point(sv_bounds.left() + px(10.0), sv_bounds.top() + px(10.0)); - cx.simulate_click(click, Modifiers::none()); - cx.run_until_parked(); + let sv_bounds = cx + .debug_bounds("color-sv") + .expect("SV square should be rendered"); + eprintln!( + "bounds: sv={:?} row={:?} popup={:?}", + cx.debug_bounds("color-sv"), + cx.debug_bounds("color-row"), + cx.debug_bounds("color-popup") + ); + // Click near the top-left of the SV square: that is white. + let click = point(sv_bounds.left() + px(10.0), sv_bounds.top() + px(10.0)); + cx.simulate_click(click, Modifiers::none()); + cx.run_until_parked(); - let (color, changed) = cx.read(|app| { - let host = host.read(app); - let color = host.picker.read(app).color(); - let changed = host.events.iter().any(|e| { - matches!(e, ColorPickerEvent::ColorChanged { .. }) - }); - (color, changed) - }); - assert!(changed, "expected a ColorChanged event"); - // Near the top-left the color is a pale, low-saturation tint of the - // hue (high value, low saturation) - clearly not the initial red. - assert!(color.r > 0.85 && color.g > 0.8 && color.b > 0.8, "got {color:?}"); - assert!(color.g > color.b - 0.01 || (color.r - color.g).abs() > 0.05, "got {color:?}"); - } + let (color, changed) = cx.read(|app| { + let host = host.read(app); + let color = host.picker.read(app).color(); + let changed = host + .events + .iter() + .any(|e| matches!(e, ColorPickerEvent::ColorChanged { .. })); + (color, changed) + }); + assert!(changed, "expected a ColorChanged event"); + // Near the top-left the color is a pale, low-saturation tint of the + // hue (high value, low saturation) - clearly not the initial red. + assert!( + color.r > 0.85 && color.g > 0.8 && color.b > 0.8, + "got {color:?}" + ); + assert!( + color.g > color.b - 0.01 || (color.r - color.g).abs() > 0.05, + "got {color:?}" + ); + } } diff --git a/crates/gpui_widgets/src/combo_box.rs b/crates/gpui_widgets/src/combo_box.rs index ddf134a9ef..3c67ccc183 100644 --- a/crates/gpui_widgets/src/combo_box.rs +++ b/crates/gpui_widgets/src/combo_box.rs @@ -10,469 +10,478 @@ //! or a click outside closes the menu. use gpui::{ - Anchor, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, KeyDownEvent, - Pixels, Point, Render, SharedString, Window, anchored, colors::DefaultColors, deferred, div, - point, prelude::*, px, + Anchor, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, + KeyDownEvent, Pixels, Point, Render, SharedString, Window, anchored, colors::DefaultColors, + deferred, div, point, prelude::*, px, }; /// A selectable option. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ComboBoxOption { - /// The option's value (stable id). - pub value: usize, - /// The label shown in the field and the menu. - pub label: SharedString, + /// The option's value (stable id). + pub value: usize, + /// The label shown in the field and the menu. + pub label: SharedString, } impl ComboBoxOption { - /// Create an option. - pub fn new(value: usize, label: impl Into) -> Self { - Self { - value, - label: label.into(), - } - } + /// Create an option. + pub fn new(value: usize, label: impl Into) -> Self { + Self { + value, + label: label.into(), + } + } } /// A request emitted by a combo box. #[derive(Debug, Clone, PartialEq)] pub enum ComboBoxEvent { - /// The user picked an option (click or arrow key). - Selected { - /// The control's stable id. - control: usize, - /// The chosen option value. - value: usize, - }, - /// The menu was opened. - MenuOpened { - /// The control's stable id. - control: usize, - }, - /// The menu was closed. - MenuClosed { - /// The control's stable id. - control: usize, - }, + /// The user picked an option (click or arrow key). + Selected { + /// The control's stable id. + control: usize, + /// The chosen option value. + value: usize, + }, + /// The menu was opened. + MenuOpened { + /// The control's stable id. + control: usize, + }, + /// The menu was closed. + MenuClosed { + /// The control's stable id. + control: usize, + }, } /// Move `selected` by `delta` positions over a list of `len` options, /// wrapping around. `None` moves to the first (or last) option. pub fn cycle_selection(selected: Option, len: usize, delta: i32) -> Option { - if len == 0 { - return None; - } - let current = match selected { - Some(index) => index, - None if delta > 0 => len - 1, - None => 0, - }; - Some(((current as i64 + delta as i64).rem_euclid(len as i64)) as usize) + if len == 0 { + return None; + } + let current = match selected { + Some(index) => index, + None if delta > 0 => len - 1, + None => 0, + }; + Some(((current as i64 + delta as i64).rem_euclid(len as i64)) as usize) } /// A drop-down selection control. pub struct ComboBox { - control: usize, - options: Vec, - selected: Option, - placeholder: Option, - open: bool, - /// Window position of the menu, recorded when the field is clicked. - popup_position: Point, - /// Whether the menu was open when the field was pressed, so the toggle on - /// mouse-up does not reopen a menu that an outside-click close already - /// dismissed on the same click. - was_open_at_down: bool, - focus_handle: FocusHandle, + control: usize, + options: Vec, + selected: Option, + placeholder: Option, + open: bool, + /// Window position of the menu, recorded when the field is clicked. + popup_position: Point, + /// Whether the menu was open when the field was pressed, so the toggle on + /// mouse-up does not reopen a menu that an outside-click close already + /// dismissed on the same click. + was_open_at_down: bool, + focus_handle: FocusHandle, } impl ComboBox { - /// Create a combo box for `control` over `options`. - pub fn new( - control: usize, - options: Vec, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - options, - selected: None, - placeholder: None, - open: false, - popup_position: Point::default(), - was_open_at_down: false, - focus_handle: cx.focus_handle(), - } - } + /// Create a combo box for `control` over `options`. + pub fn new( + control: usize, + options: Vec, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + options, + selected: None, + placeholder: None, + open: false, + popup_position: Point::default(), + was_open_at_down: false, + focus_handle: cx.focus_handle(), + } + } - /// Text shown when nothing is selected. - pub fn with_placeholder(mut self, placeholder: impl Into) -> Self { - self.placeholder = Some(placeholder.into()); - self - } + /// Text shown when nothing is selected. + pub fn with_placeholder(mut self, placeholder: impl Into) -> Self { + self.placeholder = Some(placeholder.into()); + self + } - /// The currently selected option value, if any. - pub fn selected(&self) -> Option { - self.selected - } + /// The currently selected option value, if any. + pub fn selected(&self) -> Option { + self.selected + } - /// Whether the menu is open. - pub fn is_open(&self) -> bool { - self.open - } + /// Whether the menu is open. + pub fn is_open(&self) -> bool { + self.open + } - /// Apply the host's selection and repaint. - pub fn set_selected(&mut self, selected: Option, cx: &mut Context) { - if self.selected != selected { - self.selected = selected; - cx.notify(); - } - } + /// Apply the host's selection and repaint. + pub fn set_selected(&mut self, selected: Option, cx: &mut Context) { + if self.selected != selected { + self.selected = selected; + cx.notify(); + } + } - fn selected_index(&self) -> Option { - self.selected - .and_then(|value| self.options.iter().position(|o| o.value == value)) - } + fn selected_index(&self) -> Option { + self.selected + .and_then(|value| self.options.iter().position(|o| o.value == value)) + } - fn open_menu(&mut self, position: Point, cx: &mut Context) { - if !self.open { - self.open = true; - self.popup_position = position; - cx.emit(ComboBoxEvent::MenuOpened { - control: self.control, - }); - cx.notify(); - } - } + fn open_menu(&mut self, position: Point, cx: &mut Context) { + if !self.open { + self.open = true; + self.popup_position = position; + cx.emit(ComboBoxEvent::MenuOpened { + control: self.control, + }); + cx.notify(); + } + } - fn close_menu(&mut self, cx: &mut Context) { - if self.open { - self.open = false; - cx.emit(ComboBoxEvent::MenuClosed { - control: self.control, - }); - cx.notify(); - } - } + fn close_menu(&mut self, cx: &mut Context) { + if self.open { + self.open = false; + cx.emit(ComboBoxEvent::MenuClosed { + control: self.control, + }); + cx.notify(); + } + } - fn toggle_menu(&mut self, position: Point, cx: &mut Context) { - if self.open { - self.close_menu(cx); - } else { - self.open_menu(position, cx); - } - } + fn toggle_menu(&mut self, position: Point, cx: &mut Context) { + if self.open { + self.close_menu(cx); + } else { + self.open_menu(position, cx); + } + } - fn pick(&mut self, value: usize, cx: &mut Context) { - self.selected = Some(value); - self.open = false; - cx.emit(ComboBoxEvent::Selected { - control: self.control, - value, - }); - cx.emit(ComboBoxEvent::MenuClosed { - control: self.control, - }); - cx.notify(); - } + fn pick(&mut self, value: usize, cx: &mut Context) { + self.selected = Some(value); + self.open = false; + cx.emit(ComboBoxEvent::Selected { + control: self.control, + value, + }); + cx.emit(ComboBoxEvent::MenuClosed { + control: self.control, + }); + cx.notify(); + } - fn cycle(&mut self, delta: i32, cx: &mut Context) { - if let Some(index) = cycle_selection(self.selected_index(), self.options.len(), delta) { - let value = self.options[index].value; - self.pick(value, cx); - } - } + fn cycle(&mut self, delta: i32, cx: &mut Context) { + if let Some(index) = cycle_selection(self.selected_index(), self.options.len(), delta) { + let value = self.options[index].value; + self.pick(value, cx); + } + } } impl EventEmitter for ComboBox {} impl Focusable for ComboBox { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for ComboBox { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let label = self - .selected - .and_then(|value| self.options.iter().find(|o| o.value == value)) - .map(|o| o.label.clone()) - .or_else(|| self.placeholder.clone()) - .unwrap_or_else(|| SharedString::from("")); - let open = self.open; - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let label = self + .selected + .and_then(|value| self.options.iter().find(|o| o.value == value)) + .map(|o| o.label.clone()) + .or_else(|| self.placeholder.clone()) + .unwrap_or_else(|| SharedString::from("")); + let open = self.open; + let control = self.control; - let field = div() - .id(ElementId::named_usize("gpui-widgets-combo-field", control)) - .min_w(px(140.0)) - .h(px(24.0)) - .rounded_md() - .border_1() - .border_color(if open { colors.selected } else { colors.border }) - .bg(colors.background) - .px_2() - .flex() - .items_center() - .justify_between() - .gap(px(6.0)) - .cursor_pointer() - .track_focus(&self.focus_handle) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(|this, _event: &gpui::MouseDownEvent, _window, _cx| { - this.was_open_at_down = this.open; - }), - ) - .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { - if this.was_open_at_down { - this.close_menu(cx); - } else { - this.open_menu(event.position(), cx); - } - cx.stop_propagation(); - })) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "up" => this.cycle(-1, cx), - "down" => this.cycle(1, cx), - "escape" => this.close_menu(cx), - "enter" | "space" => { - this.toggle_menu(this.popup_position, cx); - } - _ => {} - } - })) - .child(div().text_color(colors.text).child(label)) - .child(div().child(chevron(colors.border))); + let field = div() + .id(ElementId::named_usize("gpui-widgets-combo-field", control)) + .min_w(px(140.0)) + .h(px(24.0)) + .rounded_md() + .border_1() + .border_color(if open { colors.selected } else { colors.border }) + .bg(colors.background) + .px_2() + .flex() + .items_center() + .justify_between() + .gap(px(6.0)) + .cursor_pointer() + .track_focus(&self.focus_handle) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|this, _event: &gpui::MouseDownEvent, _window, _cx| { + this.was_open_at_down = this.open; + }), + ) + .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { + if this.was_open_at_down { + this.close_menu(cx); + } else { + this.open_menu(event.position(), cx); + } + cx.stop_propagation(); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "up" => this.cycle(-1, cx), + "down" => this.cycle(1, cx), + "escape" => this.close_menu(cx), + "enter" | "space" => { + this.toggle_menu(this.popup_position, cx); + } + _ => {} + } + })) + .child(div().text_color(colors.text).child(label)) + .child(div().child(chevron(colors.border))); - let popup = if open { - // Build the option list inline so each option can carry its own - // view listener. - let mut menu = div() - .id(ElementId::named_usize("gpui-widgets-combo-menu", control)) - .debug_selector(|| "combo-menu".into()) - .min_w(px(140.0)) - .rounded_md() - .border_1() - .border_color(colors.border) - .bg(colors.container) - .py_1() - .flex() - .flex_col() - .overflow_y_scroll() - .max_h(px(220.0)) - .on_mouse_up_out( - gpui::MouseButton::Left, - cx.listener(|this, _event: &gpui::MouseUpEvent, _window, cx| { - this.close_menu(cx); - }), - ); + let popup = if open { + // Build the option list inline so each option can carry its own + // view listener. + let mut menu = div() + .id(ElementId::named_usize("gpui-widgets-combo-menu", control)) + .debug_selector(|| "combo-menu".into()) + .min_w(px(140.0)) + .rounded_md() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .py_1() + .flex() + .flex_col() + .overflow_y_scroll() + .max_h(px(220.0)) + .on_mouse_up_out( + gpui::MouseButton::Left, + cx.listener(|this, _event: &gpui::MouseUpEvent, _window, cx| { + this.close_menu(cx); + }), + ); - for option in &self.options { - let is_selected = self.selected == Some(option.value); - let value = option.value; - let label = option.label.clone(); - menu = menu.child( - div() - .id(ElementId::named_usize( - format!("gpui-widgets-combo-option-{control}"), - value, - )) - .px_2() - .py_1() - .text_color(colors.text) - .hover(|style| style.bg(colors.selected)) - .when(is_selected, |el| el.text_color(colors.selected_text)) - .on_click( - cx.listener(move |this, _event: &ClickEvent, _window, cx| { - this.pick(value, cx); - cx.stop_propagation(); - }), - ) - .child(label), - ); - } + for option in &self.options { + let is_selected = self.selected == Some(option.value); + let value = option.value; + let label = option.label.clone(); + menu = menu.child( + div() + .id(ElementId::named_usize( + format!("gpui-widgets-combo-option-{control}"), + value, + )) + .px_2() + .py_1() + .text_color(colors.text) + .hover(|style| style.bg(colors.selected)) + .when(is_selected, |el| el.text_color(colors.selected_text)) + .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| { + this.pick(value, cx); + cx.stop_propagation(); + })) + .child(label), + ); + } - deferred( - anchored() - .position(self.popup_position) - .anchor(Anchor::TopLeft) - .offset(point(px(0.0), px(26.0))) - .snap_to_window_with_margin(px(8.0)) - .child(menu), - ) - .with_priority(1) - } else { - deferred(div()) - }; + deferred( + anchored() + .position(self.popup_position) + .anchor(Anchor::TopLeft) + .offset(point(px(0.0), px(26.0))) + .snap_to_window_with_margin(px(8.0)) + .child(menu), + ) + .with_priority(1) + } else { + deferred(div()) + }; - div() - .relative() - .child(field) - .child(popup) - } + div().relative().child(field).child(popup) + } } /// A small down-chevron painted with a canvas. fn chevron(color: gpui::Rgba) -> impl IntoElement { - use gpui::{canvas, point, px, Bounds, PathBuilder, Pixels}; + use gpui::{Bounds, PathBuilder, Pixels, canvas, point, px}; - canvas( - move |_bounds, _window, _cx| (), - move |bounds: Bounds, (), window, cx| { - let _ = cx; - let mut path = PathBuilder::fill(); - path.move_to(point(bounds.left() + px(3.0), bounds.top() + px(2.0))); - path.line_to(point(bounds.center().x, bounds.bottom() - px(2.0))); - path.line_to(point(bounds.right() - px(3.0), bounds.top() + px(2.0))); - path.close(); - if let Ok(path) = path.build() { - window.paint_path(path, color); - } - }, - ) + canvas( + move |_bounds, _window, _cx| (), + move |bounds: Bounds, (), window, cx| { + let _ = cx; + let mut path = PathBuilder::fill(); + path.move_to(point(bounds.left() + px(3.0), bounds.top() + px(2.0))); + path.line_to(point(bounds.center().x, bounds.bottom() - px(2.0))); + path.line_to(point(bounds.right() - px(3.0), bounds.top() + px(2.0))); + path.close(); + if let Ok(path) = path.build() { + window.paint_path(path, color); + } + }, + ) } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; + use super::*; + use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; - #[test] - fn cycle_selection_wraps() { - assert_eq!(cycle_selection(None, 3, 1), Some(0)); - assert_eq!(cycle_selection(None, 3, -1), Some(2)); - assert_eq!(cycle_selection(Some(0), 3, -1), Some(2)); - assert_eq!(cycle_selection(Some(2), 3, 1), Some(0)); - assert_eq!(cycle_selection(Some(1), 3, 1), Some(2)); - assert_eq!(cycle_selection(None, 0, 1), None); - } + #[test] + fn cycle_selection_wraps() { + assert_eq!(cycle_selection(None, 3, 1), Some(0)); + assert_eq!(cycle_selection(None, 3, -1), Some(2)); + assert_eq!(cycle_selection(Some(0), 3, -1), Some(2)); + assert_eq!(cycle_selection(Some(2), 3, 1), Some(0)); + assert_eq!(cycle_selection(Some(1), 3, 1), Some(2)); + assert_eq!(cycle_selection(None, 0, 1), None); + } - fn options() -> Vec { - vec![ - ComboBoxOption::new(1, "Frame"), - ComboBoxOption::new(2, "Timecode"), - ComboBoxOption::new(3, "Frames"), - ] - } + fn options() -> Vec { + vec![ + ComboBoxOption::new(1, "Frame"), + ComboBoxOption::new(2, "Timecode"), + ComboBoxOption::new(3, "Frames"), + ] + } - #[gpui::test] - async fn click_opens_menu_and_selects(cx: &mut TestAppContext) { - struct Host { - combo: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.combo.clone()) - } - } + #[gpui::test] + async fn click_opens_menu_and_selects(cx: &mut TestAppContext) { + struct Host { + combo: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.combo.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { - let combo = cx.new(|cx| ComboBox::new(1, options(), window, cx)); - let host = Host { - combo, - events: Vec::new(), - }; - cx.subscribe( - &host.combo, - |host: &mut Host, - _c: Entity, - event: &ComboBoxEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { + let combo = cx.new(|cx| ComboBox::new(1, options(), window, cx)); + let host = Host { + combo, + events: Vec::new(), + }; + cx.subscribe( + &host.combo, + |host: &mut Host, + _c: Entity, + event: &ComboBoxEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Open the menu, then force a redraw so the popup lands in the frame. - cx.simulate_click(point(px(70.0), px(12.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Open the menu, then force a redraw so the popup lands in the frame. + cx.simulate_click(point(px(70.0), px(12.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let menu_visible = cx.debug_bounds("combo-menu").is_some(); - eprintln!("menu bounds: {:?}", cx.debug_bounds("combo-menu")); - assert!(menu_visible, "menu should be open after clicking the field"); + let menu_visible = cx.debug_bounds("combo-menu").is_some(); + eprintln!("menu bounds: {:?}", cx.debug_bounds("combo-menu")); + assert!(menu_visible, "menu should be open after clicking the field"); - // Pick the second option (roughly below the field). - // Pick the second option (menu at x=70, options from x=71; use a - // well-inside point). - cx.simulate_click(point(px(90.0), px(94.0)), Modifiers::none()); - cx.run_until_parked(); - let (selected, emitted, menu_closed) = cx.read(|app| { - let host = host.read(app); - ( - host.combo.read(app).selected(), - host.events.iter().any(|e| { - matches!(e, ComboBoxEvent::Selected { control: 1, value: 2 }) - }), - host.events - .iter() - .any(|e| matches!(e, ComboBoxEvent::MenuClosed { .. })), - ) - }); - assert_eq!(selected, Some(2)); - assert!(emitted); - assert!(menu_closed); - } + // Pick the second option (roughly below the field). + // Pick the second option (menu at x=70, options from x=71; use a + // well-inside point). + cx.simulate_click(point(px(90.0), px(94.0)), Modifiers::none()); + cx.run_until_parked(); + let (selected, emitted, menu_closed) = cx.read(|app| { + let host = host.read(app); + ( + host.combo.read(app).selected(), + host.events.iter().any(|e| { + matches!( + e, + ComboBoxEvent::Selected { + control: 1, + value: 2 + } + ) + }), + host.events + .iter() + .any(|e| matches!(e, ComboBoxEvent::MenuClosed { .. })), + ) + }); + assert_eq!(selected, Some(2)); + assert!(emitted); + assert!(menu_closed); + } - #[gpui::test] - async fn arrow_keys_cycle_selection(cx: &mut TestAppContext) { - struct Host { - combo: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.combo.clone()) - } - } + #[gpui::test] + async fn arrow_keys_cycle_selection(cx: &mut TestAppContext) { + struct Host { + combo: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.combo.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { - let combo = cx.new(|cx| ComboBox::new(1, options(), window, cx)); - let host = Host { - combo, - events: Vec::new(), - }; - cx.subscribe( - &host.combo, - |host: &mut Host, - _c: Entity, - event: &ComboBoxEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { + let combo = cx.new(|cx| ComboBox::new(1, options(), window, cx)); + let host = Host { + combo, + events: Vec::new(), + }; + cx.subscribe( + &host.combo, + |host: &mut Host, + _c: Entity, + event: &ComboBoxEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Focus the field, then press down twice: none -> first -> second. - cx.simulate_click(point(px(70.0), px(12.0)), Modifiers::none()); - cx.run_until_parked(); - cx.simulate_keystrokes("escape"); - cx.run_until_parked(); - cx.simulate_keystrokes("down"); - cx.run_until_parked(); - cx.simulate_keystrokes("down"); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Focus the field, then press down twice: none -> first -> second. + cx.simulate_click(point(px(70.0), px(12.0)), Modifiers::none()); + cx.run_until_parked(); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + cx.simulate_keystrokes("down"); + cx.run_until_parked(); + cx.simulate_keystrokes("down"); + cx.run_until_parked(); - let selected = cx.read(|app| host.read(app).combo.read(app).selected()); - assert_eq!(selected, Some(2)); - } + let selected = cx.read(|app| host.read(app).combo.read(app).selected()); + assert_eq!(selected, Some(2)); + } } diff --git a/crates/gpui_widgets/src/curve_editor/curve.rs b/crates/gpui_widgets/src/curve_editor/curve.rs index 8804ff6748..fb1a57320c 100644 --- a/crates/gpui_widgets/src/curve_editor/curve.rs +++ b/crates/gpui_widgets/src/curve_editor/curve.rs @@ -4,297 +4,318 @@ /// A 2D point in normalized curve space (`x` and `y` in `0..1`). #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct CurveVec2 { - /// Horizontal position. - pub x: f64, - /// Vertical position. - pub y: f64, + /// Horizontal position. + pub x: f64, + /// Vertical position. + pub y: f64, } impl CurveVec2 { - /// Create a vector. - pub const fn new(x: f64, y: f64) -> Self { - Self { x, y } - } + /// Create a vector. + pub const fn new(x: f64, y: f64) -> Self { + Self { x, y } + } } /// Which control handle of a point is being edited. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HandleSide { - /// The handle leading into the point (from the previous point). - In, - /// The handle leaving the point (toward the next point). - Out, + /// The handle leading into the point (from the previous point). + In, + /// The handle leaving the point (toward the next point). + Out, } /// A keyframe point with optional bezier control handles (offsets from the /// point, in normalized units). #[derive(Debug, Clone, Copy, PartialEq)] pub struct CurvePoint { - /// Input position, `0..1`. - pub x: f64, - /// Output value, `0..1`. - pub y: f64, - /// Control point offset for the incoming segment. - pub handle_in: Option, - /// Control point offset for the outgoing segment. - pub handle_out: Option, + /// Input position, `0..1`. + pub x: f64, + /// Output value, `0..1`. + pub y: f64, + /// Control point offset for the incoming segment. + pub handle_in: Option, + /// Control point offset for the outgoing segment. + pub handle_out: Option, } impl CurvePoint { - /// Create a point with no handles (linear segments). - pub const fn new(x: f64, y: f64) -> Self { - Self { - x, - y, - handle_in: None, - handle_out: None, - } - } + /// Create a point with no handles (linear segments). + pub const fn new(x: f64, y: f64) -> Self { + Self { + x, + y, + handle_in: None, + handle_out: None, + } + } - /// Create a point with both handles set to `offset`. - pub const fn with_handles(x: f64, y: f64, offset: CurveVec2) -> Self { - Self { - x, - y, - handle_in: Some(offset), - handle_out: Some(offset), - } - } + /// Create a point with both handles set to `offset`. + pub const fn with_handles(x: f64, y: f64, offset: CurveVec2) -> Self { + Self { + x, + y, + handle_in: Some(offset), + handle_out: Some(offset), + } + } } /// Evaluate a cubic bezier at `t` in `0..1`. -pub fn cubic_bezier(p0: CurveVec2, c1: CurveVec2, c2: CurveVec2, p1: CurveVec2, t: f64) -> CurveVec2 { - let u = 1.0 - t; - CurveVec2::new( - u * u * u * p0.x + 3.0 * u * u * t * c1.x + 3.0 * u * t * t * c2.x + t * t * t * p1.x, - u * u * u * p0.y + 3.0 * u * u * t * c1.y + 3.0 * u * t * t * c2.y + t * t * t * p1.y, - ) +pub fn cubic_bezier( + p0: CurveVec2, + c1: CurveVec2, + c2: CurveVec2, + p1: CurveVec2, + t: f64, +) -> CurveVec2 { + let u = 1.0 - t; + CurveVec2::new( + u * u * u * p0.x + 3.0 * u * u * t * c1.x + 3.0 * u * t * t * c2.x + t * t * t * p1.x, + u * u * u * p0.y + 3.0 * u * u * t * c1.y + 3.0 * u * t * t * c2.y + t * t * t * p1.y, + ) } /// The control points of the segment from `p0` to `p1` (linear when handles /// are absent). pub fn segment_controls(p0: &CurvePoint, p1: &CurvePoint) -> (CurveVec2, CurveVec2) { - let c1 = match p0.handle_out { - Some(h) => CurveVec2::new(p0.x + h.x, p0.y + h.y), - None => CurveVec2::new((p0.x + p1.x) / 2.0, p0.y), - }; - let c2 = match p1.handle_in { - Some(h) => CurveVec2::new(p1.x + h.x, p1.y + h.y), - None => CurveVec2::new((p0.x + p1.x) / 2.0, p1.y), - }; - (c1, c2) + let c1 = match p0.handle_out { + Some(h) => CurveVec2::new(p0.x + h.x, p0.y + h.y), + None => CurveVec2::new((p0.x + p1.x) / 2.0, p0.y), + }; + let c2 = match p1.handle_in { + Some(h) => CurveVec2::new(p1.x + h.x, p1.y + h.y), + None => CurveVec2::new((p0.x + p1.x) / 2.0, p1.y), + }; + (c1, c2) } /// Sample the curve at `x`, returning the output value. `x` is clamped to /// the point range; values before the first (after the last) point clamp to /// the first (last) point's output. pub fn sample_curve(points: &[CurvePoint], x: f64) -> f64 { - if points.is_empty() { - return 0.0; - } - if points.len() == 1 { - return points[0].y; - } - let x = x.clamp(points[0].x, points[points.len() - 1].x); - let index = points - .windows(2) - .position(|w| x >= w[0].x && x <= w[1].x) - .unwrap_or(points.len() - 2); - let p0 = &points[index]; - let p1 = &points[index + 1]; - let (c1, c2) = segment_controls(p0, p1); - sample_segment( - CurveVec2::new(p0.x, p0.y), - c1, - c2, - CurveVec2::new(p1.x, p1.y), - x, - ) + if points.is_empty() { + return 0.0; + } + if points.len() == 1 { + return points[0].y; + } + let x = x.clamp(points[0].x, points[points.len() - 1].x); + let index = points + .windows(2) + .position(|w| x >= w[0].x && x <= w[1].x) + .unwrap_or(points.len() - 2); + let p0 = &points[index]; + let p1 = &points[index + 1]; + let (c1, c2) = segment_controls(p0, p1); + sample_segment( + CurveVec2::new(p0.x, p0.y), + c1, + c2, + CurveVec2::new(p1.x, p1.y), + x, + ) } /// Sample one segment for the `y` at a given `x`, by finding the `t` whose /// bezier x coordinate matches (binary search, since bezier x is monotonic /// for well-formed curves). fn sample_segment(p0: CurveVec2, c1: CurveVec2, c2: CurveVec2, p1: CurveVec2, x: f64) -> f64 { - let mut lo = 0.0; - let mut hi = 1.0; - for _ in 0..24 { - let mid = (lo + hi) / 2.0; - let px = cubic_bezier(p0, c1, c2, p1, mid).x; - if px < x { - lo = mid; - } else { - hi = mid; - } - } - let t = (lo + hi) / 2.0; - cubic_bezier(p0, c1, c2, p1, t).y + let mut lo = 0.0; + let mut hi = 1.0; + for _ in 0..24 { + let mid = (lo + hi) / 2.0; + let px = cubic_bezier(p0, c1, c2, p1, mid).x; + if px < x { + lo = mid; + } else { + hi = mid; + } + } + let t = (lo + hi) / 2.0; + cubic_bezier(p0, c1, c2, p1, t).y } /// Approximate the whole curve as a polyline (for painting). Each segment is /// sampled `samples` times. pub fn polyline(points: &[CurvePoint], samples: usize) -> Vec { - let mut out = Vec::new(); - if points.is_empty() { - return out; - } - out.push(CurveVec2::new(points[0].x, points[0].y)); - for window in points.windows(2) { - let (p0, p1) = (&window[0], &window[1]); - let (c1, c2) = segment_controls(p0, p1); - let p0v = CurveVec2::new(p0.x, p0.y); - let p1v = CurveVec2::new(p1.x, p1.y); - for i in 1..=samples { - let t = i as f64 / samples as f64; - out.push(cubic_bezier(p0v, c1, c2, p1v, t)); - } - } - out + let mut out = Vec::new(); + if points.is_empty() { + return out; + } + out.push(CurveVec2::new(points[0].x, points[0].y)); + for window in points.windows(2) { + let (p0, p1) = (&window[0], &window[1]); + let (c1, c2) = segment_controls(p0, p1); + let p0v = CurveVec2::new(p0.x, p0.y); + let p1v = CurveVec2::new(p1.x, p1.y); + for i in 1..=samples { + let t = i as f64 / samples as f64; + out.push(cubic_bezier(p0v, c1, c2, p1v, t)); + } + } + out } /// Find the point closest to `pos` within `threshold` (normalized units). pub fn hit_test_point(points: &[CurvePoint], pos: CurveVec2, threshold: f64) -> Option { - let mut best = None; - let mut best_dist = threshold; - for (index, point) in points.iter().enumerate() { - let dx = point.x - pos.x; - let dy = point.y - pos.y; - let dist = (dx * dx + dy * dy).sqrt(); - if dist <= best_dist { - best_dist = dist; - best = Some(index); - } - } - best + let mut best = None; + let mut best_dist = threshold; + for (index, point) in points.iter().enumerate() { + let dx = point.x - pos.x; + let dy = point.y - pos.y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist <= best_dist { + best_dist = dist; + best = Some(index); + } + } + best } /// Find the handle closest to `pos` within `threshold`, preferring handles /// over points when both are within reach. pub fn hit_test_handle( - points: &[CurvePoint], - pos: CurveVec2, - threshold: f64, + points: &[CurvePoint], + pos: CurveVec2, + threshold: f64, ) -> Option<(usize, HandleSide)> { - let mut best = None; - let mut best_dist = threshold; - for (index, point) in points.iter().enumerate() { - for (side, handle) in [ - (HandleSide::In, point.handle_in), - (HandleSide::Out, point.handle_out), - ] { - if let Some(h) = handle { - let hp = CurveVec2::new(point.x + h.x, point.y + h.y); - let dx = hp.x - pos.x; - let dy = hp.y - pos.y; - let dist = (dx * dx + dy * dy).sqrt(); - if dist <= best_dist { - best_dist = dist; - best = Some((index, side)); - } - } - } - } - best + let mut best = None; + let mut best_dist = threshold; + for (index, point) in points.iter().enumerate() { + for (side, handle) in [ + (HandleSide::In, point.handle_in), + (HandleSide::Out, point.handle_out), + ] { + if let Some(h) = handle { + let hp = CurveVec2::new(point.x + h.x, point.y + h.y); + let dx = hp.x - pos.x; + let dy = hp.y - pos.y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist <= best_dist { + best_dist = dist; + best = Some((index, side)); + } + } + } + } + best } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn approx(a: f64, b: f64) -> bool { - (a - b).abs() < 1e-6 - } + fn approx(a: f64, b: f64) -> bool { + (a - b).abs() < 1e-6 + } - #[test] - fn bezier_endpoints() { - let p0 = CurveVec2::new(0.0, 0.0); - let p1 = CurveVec2::new(1.0, 1.0); - assert_eq!(cubic_bezier(p0, p0, p1, p1, 0.0), p0); - assert_eq!(cubic_bezier(p0, p0, p1, p1, 1.0), p1); - // A straight-line bezier at t=0.5 is the midpoint. - let mid = cubic_bezier(p0, p0, p1, p1, 0.5); - assert!(approx(mid.x, 0.5) && approx(mid.y, 0.5)); - } + #[test] + fn bezier_endpoints() { + let p0 = CurveVec2::new(0.0, 0.0); + let p1 = CurveVec2::new(1.0, 1.0); + assert_eq!(cubic_bezier(p0, p0, p1, p1, 0.0), p0); + assert_eq!(cubic_bezier(p0, p0, p1, p1, 1.0), p1); + // A straight-line bezier at t=0.5 is the midpoint. + let mid = cubic_bezier(p0, p0, p1, p1, 0.5); + assert!(approx(mid.x, 0.5) && approx(mid.y, 0.5)); + } - #[test] - fn linear_curve_samples_exactly() { - let points = vec![CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)]; - assert!(approx(sample_curve(&points, 0.0), 0.0)); - assert!(approx(sample_curve(&points, 0.5), 0.5)); - assert!(approx(sample_curve(&points, 1.0), 1.0)); - // Clamps outside the range. - assert!(approx(sample_curve(&points, 2.0), 1.0)); - assert!(approx(sample_curve(&points, -1.0), 0.0)); - } + #[test] + fn linear_curve_samples_exactly() { + let points = vec![CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)]; + assert!(approx(sample_curve(&points, 0.0), 0.0)); + assert!(approx(sample_curve(&points, 0.5), 0.5)); + assert!(approx(sample_curve(&points, 1.0), 1.0)); + // Clamps outside the range. + assert!(approx(sample_curve(&points, 2.0), 1.0)); + assert!(approx(sample_curve(&points, -1.0), 0.0)); + } - #[test] - fn stepped_curve_clamps_to_segments() { - let points = vec![ - CurvePoint::new(0.0, 0.0), - CurvePoint::new(0.5, 0.0), - CurvePoint::new(1.0, 1.0), - ]; - assert!(approx(sample_curve(&points, 0.25), 0.0)); - assert!(approx(sample_curve(&points, 0.75), 0.5)); - } + #[test] + fn stepped_curve_clamps_to_segments() { + let points = vec![ + CurvePoint::new(0.0, 0.0), + CurvePoint::new(0.5, 0.0), + CurvePoint::new(1.0, 1.0), + ]; + assert!(approx(sample_curve(&points, 0.25), 0.0)); + assert!(approx(sample_curve(&points, 0.75), 0.5)); + } - #[test] - fn single_point_is_constant() { - let points = vec![CurvePoint::new(0.5, 0.7)]; - assert!(approx(sample_curve(&points, 0.0), 0.7)); - assert!(approx(sample_curve(&points, 0.9), 0.7)); - } + #[test] + fn single_point_is_constant() { + let points = vec![CurvePoint::new(0.5, 0.7)]; + assert!(approx(sample_curve(&points, 0.0), 0.7)); + assert!(approx(sample_curve(&points, 0.9), 0.7)); + } - #[test] - fn bezier_handles_bend_the_curve() { - // A curve whose outgoing handle pushes straight up at the start must - // start with output above the linear interpolation. - let points = vec![ - CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 1.0)), - CurvePoint::new(1.0, 1.0), - ]; - let linear = sample_curve(&[CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)], 0.25); - let bent = sample_curve(&points, 0.25); - assert!(bent > linear, "bent={bent} linear={linear}"); - } + #[test] + fn bezier_handles_bend_the_curve() { + // A curve whose outgoing handle pushes straight up at the start must + // start with output above the linear interpolation. + let points = vec![ + CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 1.0)), + CurvePoint::new(1.0, 1.0), + ]; + let linear = sample_curve( + &[CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)], + 0.25, + ); + let bent = sample_curve(&points, 0.25); + assert!(bent > linear, "bent={bent} linear={linear}"); + } - #[test] - fn polyline_has_expected_length() { - let points = vec![CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)]; - let line = polyline(&points, 8); - assert_eq!(line.len(), 9); - assert_eq!(line.first().unwrap(), &CurveVec2::new(0.0, 0.0)); - assert_eq!(line.last().unwrap(), &CurveVec2::new(1.0, 1.0)); - assert_eq!(polyline(&[], 8).len(), 0); - } + #[test] + fn polyline_has_expected_length() { + let points = vec![CurvePoint::new(0.0, 0.0), CurvePoint::new(1.0, 1.0)]; + let line = polyline(&points, 8); + assert_eq!(line.len(), 9); + assert_eq!(line.first().unwrap(), &CurveVec2::new(0.0, 0.0)); + assert_eq!(line.last().unwrap(), &CurveVec2::new(1.0, 1.0)); + assert_eq!(polyline(&[], 8).len(), 0); + } - #[test] - fn hit_test_finds_nearest_point() { - let points = vec![ - CurvePoint::new(0.1, 0.1), - CurvePoint::new(0.5, 0.5), - CurvePoint::new(0.9, 0.9), - ]; - assert_eq!(hit_test_point(&points, CurveVec2::new(0.52, 0.52), 0.1), Some(1)); - assert_eq!(hit_test_point(&points, CurveVec2::new(0.1, 0.1), 0.1), Some(0)); - // Beyond the threshold. - assert_eq!(hit_test_point(&points, CurveVec2::new(0.3, 0.3), 0.05), None); - } + #[test] + fn hit_test_finds_nearest_point() { + let points = vec![ + CurvePoint::new(0.1, 0.1), + CurvePoint::new(0.5, 0.5), + CurvePoint::new(0.9, 0.9), + ]; + assert_eq!( + hit_test_point(&points, CurveVec2::new(0.52, 0.52), 0.1), + Some(1) + ); + assert_eq!( + hit_test_point(&points, CurveVec2::new(0.1, 0.1), 0.1), + Some(0) + ); + // Beyond the threshold. + assert_eq!( + hit_test_point(&points, CurveVec2::new(0.3, 0.3), 0.05), + None + ); + } - #[test] - fn hit_test_finds_handle() { - let points = vec![CurvePoint { - x: 0.2, - y: 0.5, - handle_in: Some(CurveVec2::new(-0.1, -0.1)), - handle_out: Some(CurveVec2::new(0.1, 0.1)), - }]; - // The outgoing handle endpoint sits at (0.3, 0.6). - let hit = hit_test_handle(&points, CurveVec2::new(0.31, 0.61), 0.05); - assert_eq!(hit, Some((0, HandleSide::Out))); - // The incoming handle endpoint sits at (0.1, 0.4). - let hit = hit_test_handle(&points, CurveVec2::new(0.09, 0.39), 0.05); - assert_eq!(hit, Some((0, HandleSide::In))); - assert_eq!(hit_test_handle(&points, CurveVec2::new(0.9, 0.9), 0.05), None); - } + #[test] + fn hit_test_finds_handle() { + let points = vec![CurvePoint { + x: 0.2, + y: 0.5, + handle_in: Some(CurveVec2::new(-0.1, -0.1)), + handle_out: Some(CurveVec2::new(0.1, 0.1)), + }]; + // The outgoing handle endpoint sits at (0.3, 0.6). + let hit = hit_test_handle(&points, CurveVec2::new(0.31, 0.61), 0.05); + assert_eq!(hit, Some((0, HandleSide::Out))); + // The incoming handle endpoint sits at (0.1, 0.4). + let hit = hit_test_handle(&points, CurveVec2::new(0.09, 0.39), 0.05); + assert_eq!(hit, Some((0, HandleSide::In))); + assert_eq!( + hit_test_handle(&points, CurveVec2::new(0.9, 0.9), 0.05), + None + ); + } } diff --git a/crates/gpui_widgets/src/curve_editor/mod.rs b/crates/gpui_widgets/src/curve_editor/mod.rs index 7c5449b732..f3885af9fa 100644 --- a/crates/gpui_widgets/src/curve_editor/mod.rs +++ b/crates/gpui_widgets/src/curve_editor/mod.rs @@ -12,9 +12,9 @@ mod curve; use std::sync::{Arc, RwLock}; use gpui::{ - App, Bounds, ClickEvent, Context, DragMoveEvent, ElementId, Entity, EventEmitter, FocusHandle, - Focusable, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, Pixels, Point, Render, Window, - canvas, colors::DefaultColors, div, fill, point, prelude::*, px, quad, size, + App, Bounds, ClickEvent, Context, DragMoveEvent, ElementId, Entity, EventEmitter, FocusHandle, + Focusable, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, Pixels, Point, Render, Window, + canvas, colors::DefaultColors, div, fill, point, prelude::*, px, quad, size, }; use gpui::{BorderStyle, Corners, Edges, PathBuilder}; @@ -28,49 +28,49 @@ const HIT_THRESHOLD: f64 = 0.06; /// A request emitted by a curve editor. #[derive(Debug, Clone, PartialEq)] pub enum CurveEditorEvent { - /// A keyframe point was dragged to a new position. - PointMoved { - /// The control's stable id. - control: usize, - /// The point's index in the (sorted) point list. - index: usize, - /// The point's new position/handles. - point: CurvePoint, - }, - /// A bezier control handle was dragged. - HandleMoved { - /// The control's stable id. - control: usize, - /// The owning point's index. - index: usize, - /// Which handle was moved. - side: HandleSide, - /// The handle's new offset from the point (normalized). - handle: CurveVec2, - }, - /// A new keyframe point was added (double-click on the canvas). - PointAdded { - /// The control's stable id. - control: usize, - /// The index the point was inserted at. - index: usize, - /// The new point. - point: CurvePoint, - }, + /// A keyframe point was dragged to a new position. + PointMoved { + /// The control's stable id. + control: usize, + /// The point's index in the (sorted) point list. + index: usize, + /// The point's new position/handles. + point: CurvePoint, + }, + /// A bezier control handle was dragged. + HandleMoved { + /// The control's stable id. + control: usize, + /// The owning point's index. + index: usize, + /// Which handle was moved. + side: HandleSide, + /// The handle's new offset from the point (normalized). + handle: CurveVec2, + }, + /// A new keyframe point was added (double-click on the canvas). + PointAdded { + /// The control's stable id. + control: usize, + /// The index the point was inserted at. + index: usize, + /// The new point. + point: CurvePoint, + }, } /// What a drag gesture is editing. #[derive(Clone, Copy, Debug, PartialEq)] enum DragTarget { - None, - Point(usize), - Handle { index: usize, side: HandleSide }, + None, + Point(usize), + Handle { index: usize, side: HandleSide }, } /// Transient payload carried by an in-flight drag. #[derive(Clone, Copy, Debug)] struct CurveDrag { - target: DragTarget, + target: DragTarget, } /// Invisible ghost view for drags. @@ -78,464 +78,481 @@ struct CurveDrag { struct CurveGhost; impl Render for CurveGhost { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().w(px(0.0)).h(px(0.0)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(0.0)).h(px(0.0)) + } } /// A keyframe curve editor. pub struct CurveEditor { - control: usize, - points: Vec, - focus_handle: FocusHandle, - /// Canvas bounds, refreshed each frame for position conversion. - bounds: Bounds, - /// Hit target recorded on mouse-down, consumed when the drag starts. - pending_drag: Option, + control: usize, + points: Vec, + focus_handle: FocusHandle, + /// Canvas bounds, refreshed each frame for position conversion. + bounds: Bounds, + /// Hit target recorded on mouse-down, consumed when the drag starts. + pending_drag: Option, } impl CurveEditor { - /// Create an editor for `control` over `points` (already sorted by x). - pub fn new( - control: usize, - points: Vec, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - points, - focus_handle: cx.focus_handle(), - bounds: Bounds::default(), - pending_drag: None, - } - } + /// Create an editor for `control` over `points` (already sorted by x). + pub fn new( + control: usize, + points: Vec, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + points, + focus_handle: cx.focus_handle(), + bounds: Bounds::default(), + pending_drag: None, + } + } - /// The current working copy of the curve. - pub fn points(&self) -> &[CurvePoint] { - &self.points - } + /// The current working copy of the curve. + pub fn points(&self) -> &[CurvePoint] { + &self.points + } - /// Apply the host's reconciled curve and repaint. - pub fn set_points(&mut self, points: Vec, cx: &mut Context) { - self.points = points; - cx.notify(); - } + /// Apply the host's reconciled curve and repaint. + pub fn set_points(&mut self, points: Vec, cx: &mut Context) { + self.points = points; + cx.notify(); + } - /// Convert a window position into normalized curve coordinates. - fn normalize(&self, position: Point) -> CurveVec2 { - let w = f32::from(self.bounds.size.width); - let h = f32::from(self.bounds.size.height); - if w <= 0.0 || h <= 0.0 { - return CurveVec2::new(0.5, 0.5); - } - CurveVec2::new( - ((f32::from(position.x) - f32::from(self.bounds.left())) / w).clamp(0.0, 1.0) as f64, - 1.0 - ((f32::from(position.y) - f32::from(self.bounds.top())) / h).clamp(0.0, 1.0) - as f64, - ) - } + /// Convert a window position into normalized curve coordinates. + fn normalize(&self, position: Point) -> CurveVec2 { + let w = f32::from(self.bounds.size.width); + let h = f32::from(self.bounds.size.height); + if w <= 0.0 || h <= 0.0 { + return CurveVec2::new(0.5, 0.5); + } + CurveVec2::new( + ((f32::from(position.x) - f32::from(self.bounds.left())) / w).clamp(0.0, 1.0) as f64, + 1.0 - ((f32::from(position.y) - f32::from(self.bounds.top())) / h).clamp(0.0, 1.0) + as f64, + ) + } - fn hit_test(&self, position: Point) -> DragTarget { - let pos = self.normalize(position); - if let Some((index, side)) = hit_test_handle(&self.points, pos, HIT_THRESHOLD) { - return DragTarget::Handle { index, side }; - } - if let Some(index) = hit_test_point(&self.points, pos, HIT_THRESHOLD) { - return DragTarget::Point(index); - } - DragTarget::None - } + fn hit_test(&self, position: Point) -> DragTarget { + let pos = self.normalize(position); + if let Some((index, side)) = hit_test_handle(&self.points, pos, HIT_THRESHOLD) { + return DragTarget::Handle { index, side }; + } + if let Some(index) = hit_test_point(&self.points, pos, HIT_THRESHOLD) { + return DragTarget::Point(index); + } + DragTarget::None + } - /// Clamp a point's x so the list stays sorted (points cannot pass each - /// other), while y is clamped to the unit range. - fn move_point(&mut self, index: usize, pos: CurveVec2, cx: &mut Context) { - let (prev_x, next_x) = if self.points.len() == 1 { - (0.0, 1.0) - } else if index == 0 { - (0.0, self.points[1].x) - } else if index == self.points.len() - 1 { - (self.points[index - 1].x, 1.0) - } else { - (self.points[index - 1].x, self.points[index + 1].x) - }; - let min_x = (prev_x + 0.001).min(1.0); - let max_x = (next_x - 0.001).max(0.0); - let point = self.points.get_mut(index).expect("point index in range"); - point.x = pos.x.clamp(min_x, max_x); - point.y = pos.y.clamp(0.0, 1.0); - let moved = *point; - cx.emit(CurveEditorEvent::PointMoved { - control: self.control, - index, - point: moved, - }); - cx.notify(); - } + /// Clamp a point's x so the list stays sorted (points cannot pass each + /// other), while y is clamped to the unit range. + fn move_point(&mut self, index: usize, pos: CurveVec2, cx: &mut Context) { + let (prev_x, next_x) = if self.points.len() == 1 { + (0.0, 1.0) + } else if index == 0 { + (0.0, self.points[1].x) + } else if index == self.points.len() - 1 { + (self.points[index - 1].x, 1.0) + } else { + (self.points[index - 1].x, self.points[index + 1].x) + }; + let min_x = (prev_x + 0.001).min(1.0); + let max_x = (next_x - 0.001).max(0.0); + let point = self.points.get_mut(index).expect("point index in range"); + point.x = pos.x.clamp(min_x, max_x); + point.y = pos.y.clamp(0.0, 1.0); + let moved = *point; + cx.emit(CurveEditorEvent::PointMoved { + control: self.control, + index, + point: moved, + }); + cx.notify(); + } - fn move_handle( - &mut self, - index: usize, - side: HandleSide, - pos: CurveVec2, - cx: &mut Context, - ) { - let Some(point) = self.points.get_mut(index) else { - return; - }; - let offset = CurveVec2::new(pos.x - point.x, pos.y - point.y); - match side { - HandleSide::In => point.handle_in = Some(offset), - HandleSide::Out => point.handle_out = Some(offset), - } - cx.emit(CurveEditorEvent::HandleMoved { - control: self.control, - index, - side, - handle: offset, - }); - cx.notify(); - } + fn move_handle( + &mut self, + index: usize, + side: HandleSide, + pos: CurveVec2, + cx: &mut Context, + ) { + let Some(point) = self.points.get_mut(index) else { + return; + }; + let offset = CurveVec2::new(pos.x - point.x, pos.y - point.y); + match side { + HandleSide::In => point.handle_in = Some(offset), + HandleSide::Out => point.handle_out = Some(offset), + } + cx.emit(CurveEditorEvent::HandleMoved { + control: self.control, + index, + side, + handle: offset, + }); + cx.notify(); + } - /// Insert a new point at `pos` (double-click), keeping the list sorted. - fn add_point(&mut self, pos: CurveVec2, cx: &mut Context) { - let x = pos.x.clamp(0.0, 1.0); - let y = pos.y.clamp(0.0, 1.0); - let insert_at = self.points.partition_point(|p| p.x < x); - self.points.insert(insert_at, CurvePoint::new(x, y)); - cx.emit(CurveEditorEvent::PointAdded { - control: self.control, - index: insert_at, - point: CurvePoint::new(x, y), - }); - cx.notify(); - } + /// Insert a new point at `pos` (double-click), keeping the list sorted. + fn add_point(&mut self, pos: CurveVec2, cx: &mut Context) { + let x = pos.x.clamp(0.0, 1.0); + let y = pos.y.clamp(0.0, 1.0); + let insert_at = self.points.partition_point(|p| p.x < x); + self.points.insert(insert_at, CurvePoint::new(x, y)); + cx.emit(CurveEditorEvent::PointAdded { + control: self.control, + index: insert_at, + point: CurvePoint::new(x, y), + }); + cx.notify(); + } } impl EventEmitter for CurveEditor {} impl Focusable for CurveEditor { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for CurveEditor { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let entity = cx.entity(); - let canvas_entity = entity.clone(); - let points = self.points.clone(); - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let entity = cx.entity(); + let canvas_entity = entity.clone(); + let points = self.points.clone(); + let control = self.control; - div() - .id(ElementId::named_usize("gpui-widgets-curve", control)) - .h(px(EDITOR_HEIGHT)) - .rounded_md() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .overflow_hidden() - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, _cx| { - this.pending_drag = Some(this.hit_test(event.position)); - }), - ) - .on_drag( - Arc::new(RwLock::new(CurveDrag { - target: DragTarget::None, - })), - move |drag, offset, window, cx| curve_ghost(drag, offset, window, cx, entity.clone()), - ) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - let drag = event.drag(cx).clone(); - let target = drag.read().unwrap().target; - let pos = this.normalize(event.event.position); - match target { - DragTarget::Point(index) => this.move_point(index, pos, cx), - DragTarget::Handle { index, side } => { - this.move_handle(index, side, pos, cx); - } - DragTarget::None => {} - } - }, - ), - ) - .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { - if event.click_count() >= 2 { - this.add_point(this.normalize(event.position()), cx); - } - })) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - if matches!(event.keystroke.key.as_str(), "escape") { - this.pending_drag = None; - } - cx.notify(); - })) - .child( - canvas( - move |bounds, _window, cx| { - canvas_entity.update(cx, |this, _| this.bounds = bounds); - bounds - }, - move |bounds, content, window, cx| { - paint_curve(bounds, content, &points, &colors, window, cx); - }, - ) - .size_full(), - ) - } + div() + .id(ElementId::named_usize("gpui-widgets-curve", control)) + .h(px(EDITOR_HEIGHT)) + .rounded_md() + .bg(colors.background) + .border_1() + .border_color(colors.border) + .overflow_hidden() + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, _cx| { + this.pending_drag = Some(this.hit_test(event.position)); + }), + ) + .on_drag( + Arc::new(RwLock::new(CurveDrag { + target: DragTarget::None, + })), + move |drag, offset, window, cx| { + curve_ghost(drag, offset, window, cx, entity.clone()) + }, + ) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + let drag = event.drag(cx).clone(); + let target = drag.read().unwrap().target; + let pos = this.normalize(event.event.position); + match target { + DragTarget::Point(index) => this.move_point(index, pos, cx), + DragTarget::Handle { index, side } => { + this.move_handle(index, side, pos, cx); + } + DragTarget::None => {} + } + }, + )) + .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { + if event.click_count() >= 2 { + this.add_point(this.normalize(event.position()), cx); + } + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + if matches!(event.keystroke.key.as_str(), "escape") { + this.pending_drag = None; + } + cx.notify(); + })) + .child( + canvas( + move |bounds, _window, cx| { + canvas_entity.update(cx, |this, _| this.bounds = bounds); + bounds + }, + move |bounds, content, window, cx| { + paint_curve(bounds, content, &points, &colors, window, cx); + }, + ) + .size_full(), + ) + } } /// Initialize an in-flight drag with the hit target recorded at mouse-down. fn curve_ghost( - drag: &Arc>, - _offset: Point, - _window: &mut Window, - cx: &mut App, - entity: Entity, + drag: &Arc>, + _offset: Point, + _window: &mut Window, + cx: &mut App, + entity: Entity, ) -> Entity { - entity.update(cx, |this, _| { - let target = this.pending_drag.take().unwrap_or(DragTarget::None); - if let Ok(mut drag) = drag.write() { - drag.target = target; - } - }); - cx.new(|_| CurveGhost) + entity.update(cx, |this, _| { + let target = this.pending_drag.take().unwrap_or(DragTarget::None); + if let Ok(mut drag) = drag.write() { + drag.target = target; + } + }); + cx.new(|_| CurveGhost) } fn paint_curve( - bounds: Bounds, - _content: Bounds, - points: &[CurvePoint], - colors: &gpui::colors::Colors, - window: &mut Window, - _cx: &mut App, + bounds: Bounds, + _content: Bounds, + points: &[CurvePoint], + colors: &gpui::colors::Colors, + window: &mut Window, + _cx: &mut App, ) { - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - if width <= 0.0 || height <= 0.0 { - return; - } - let to_px = |v: CurveVec2| { - point( - bounds.left() + px((v.x as f32) * width), - bounds.top() + px((1.0 - v.y as f32) * height), - ) - }; + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + if width <= 0.0 || height <= 0.0 { + return; + } + let to_px = |v: CurveVec2| { + point( + bounds.left() + px((v.x as f32) * width), + bounds.top() + px((1.0 - v.y as f32) * height), + ) + }; - // Subtle grid lines. - let grid = Hsla::from(colors.border).opacity(0.35); - for i in 1..4 { - let fx = i as f32 / 4.0; - window.paint_quad(fill( - Bounds::new( - point(bounds.left() + px(fx * width), bounds.top()), - size(px(1.0), bounds.size.height), - ), - grid, - )); - window.paint_quad(fill( - Bounds::new( - point(bounds.left(), bounds.top() + px(fx * height)), - size(bounds.size.width, px(1.0)), - ), - grid, - )); - } + // Subtle grid lines. + let grid = Hsla::from(colors.border).opacity(0.35); + for i in 1..4 { + let fx = i as f32 / 4.0; + window.paint_quad(fill( + Bounds::new( + point(bounds.left() + px(fx * width), bounds.top()), + size(px(1.0), bounds.size.height), + ), + grid, + )); + window.paint_quad(fill( + Bounds::new( + point(bounds.left(), bounds.top() + px(fx * height)), + size(bounds.size.width, px(1.0)), + ), + grid, + )); + } - // The curve polyline. - let line = curve::polyline(points, 16); - if line.len() >= 2 { - let mut path = PathBuilder::stroke(px(2.0)); - let mut iter = line.iter(); - if let Some(first) = iter.next() { - path.move_to(to_px(*first)); - } - for v in iter { - path.line_to(to_px(*v)); - } - if let Ok(path) = path.build() { - window.paint_path(path, Hsla::from(colors.selected)); - } - } + // The curve polyline. + let line = curve::polyline(points, 16); + if line.len() >= 2 { + let mut path = PathBuilder::stroke(px(2.0)); + let mut iter = line.iter(); + if let Some(first) = iter.next() { + path.move_to(to_px(*first)); + } + for v in iter { + path.line_to(to_px(*v)); + } + if let Ok(path) = path.build() { + window.paint_path(path, Hsla::from(colors.selected)); + } + } - // Points and handles. - let point_color = Hsla::from(colors.text); - let handle_color = Hsla::from(colors.disabled); - for pt in points { - let p = to_px(CurveVec2::new(pt.x, pt.y)); - for handle in [pt.handle_in, pt.handle_out] { - if let Some(h) = handle { - let hp = to_px(CurveVec2::new(pt.x + h.x, pt.y + h.y)); - let mut line = PathBuilder::stroke(px(1.0)); - line.move_to(p); - line.line_to(hp); - if let Ok(path) = line.build() { - window.paint_path(path, handle_color); - } - let dot = Bounds::new( - point(hp.x - px(3.0), hp.y - px(3.0)), - size(px(6.0), px(6.0)), - ); - window.paint_quad(quad( - dot, - Corners::all(px(3.0)), - handle_color, - Edges::all(px(0.0)), - handle_color, - BorderStyle::Solid, - )); - } - } - let r = px(5.0); - let circle = Bounds::new(point(p.x - r, p.y - r), size(r * 2.0, r * 2.0)); - window.paint_quad(quad( - circle, - Corners::all(r), - point_color, - Edges::all(px(1.5)), - Hsla::from(colors.background), - BorderStyle::Solid, - )); - } + // Points and handles. + let point_color = Hsla::from(colors.text); + let handle_color = Hsla::from(colors.disabled); + for pt in points { + let p = to_px(CurveVec2::new(pt.x, pt.y)); + for handle in [pt.handle_in, pt.handle_out] { + if let Some(h) = handle { + let hp = to_px(CurveVec2::new(pt.x + h.x, pt.y + h.y)); + let mut line = PathBuilder::stroke(px(1.0)); + line.move_to(p); + line.line_to(hp); + if let Ok(path) = line.build() { + window.paint_path(path, handle_color); + } + let dot = Bounds::new( + point(hp.x - px(3.0), hp.y - px(3.0)), + size(px(6.0), px(6.0)), + ); + window.paint_quad(quad( + dot, + Corners::all(px(3.0)), + handle_color, + Edges::all(px(0.0)), + handle_color, + BorderStyle::Solid, + )); + } + } + let r = px(5.0); + let circle = Bounds::new(point(p.x - r, p.y - r), size(r * 2.0, r * 2.0)); + window.paint_quad(quad( + circle, + Corners::all(r), + point_color, + Edges::all(px(1.5)), + Hsla::from(colors.background), + BorderStyle::Solid, + )); + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Modifiers, TestAppContext, VisualTestContext}; + use super::*; + use gpui::{Modifiers, TestAppContext, VisualTestContext}; - fn demo_points() -> Vec { - vec![ - CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 0.5)), - CurvePoint::with_handles(1.0, 1.0, CurveVec2::new(0.0, -0.5)), - ] - } + fn demo_points() -> Vec { + vec![ + CurvePoint::with_handles(0.0, 0.0, CurveVec2::new(0.0, 0.5)), + CurvePoint::with_handles(1.0, 1.0, CurveVec2::new(0.0, -0.5)), + ] + } - #[gpui::test] - async fn dragging_a_point_emits_point_moved(cx: &mut TestAppContext) { - struct Host { - editor: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.editor.clone()) - } - } + #[gpui::test] + async fn dragging_a_point_emits_point_moved(cx: &mut TestAppContext) { + struct Host { + editor: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.editor.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(400.0), px(200.0)), |window, cx| { - let editor = cx.new(|cx| CurveEditor::new(1, demo_points(), window, cx)); - let host = Host { - editor, - events: Vec::new(), - }; - cx.subscribe( - &host.editor, - |host: &mut Host, - _e: Entity, - event: &CurveEditorEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(400.0), px(200.0)), |window, cx| { + let editor = cx.new(|cx| CurveEditor::new(1, demo_points(), window, cx)); + let host = Host { + editor, + events: Vec::new(), + }; + cx.subscribe( + &host.editor, + |host: &mut Host, + _e: Entity, + event: &CurveEditorEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // The curve editor spans the full window width, 120px tall. The - // first point is at normalized (0,0) -> bottom-left of the canvas. - let start = point(px(5.0), px(115.0)); - let drag_to = point(px(5.0), px(60.0)); - cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_move(point(px(5.0), px(105.0)), MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_move(drag_to, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_up(drag_to, MouseButton::Left, Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // The curve editor spans the full window width, 120px tall. The + // first point is at normalized (0,0) -> bottom-left of the canvas. + let start = point(px(5.0), px(115.0)); + let drag_to = point(px(5.0), px(60.0)); + cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::none()); + cx.simulate_mouse_move( + point(px(5.0), px(105.0)), + MouseButton::Left, + Modifiers::none(), + ); + cx.simulate_mouse_move(drag_to, MouseButton::Left, Modifiers::none()); + cx.simulate_mouse_up(drag_to, MouseButton::Left, Modifiers::none()); + cx.run_until_parked(); - let (points, moved) = cx.read(|app| { - let host = host.read(app); - let points = host.editor.read(app).points().to_vec(); - let moved = host.events.iter().any(|e| { - matches!(e, CurveEditorEvent::PointMoved { index: 0, .. }) - }); - (points, moved) - }); - assert!(moved, "expected a PointMoved event for point 0"); - assert!(points[0].y > 0.1, "point should have moved up: {:?}", points[0]); - } + let (points, moved) = cx.read(|app| { + let host = host.read(app); + let points = host.editor.read(app).points().to_vec(); + let moved = host + .events + .iter() + .any(|e| matches!(e, CurveEditorEvent::PointMoved { index: 0, .. })); + (points, moved) + }); + assert!(moved, "expected a PointMoved event for point 0"); + assert!( + points[0].y > 0.1, + "point should have moved up: {:?}", + points[0] + ); + } - #[gpui::test] - async fn double_click_adds_point(cx: &mut TestAppContext) { - struct Host { - editor: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.editor.clone()) - } - } + #[gpui::test] + async fn double_click_adds_point(cx: &mut TestAppContext) { + struct Host { + editor: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.editor.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(400.0), px(200.0)), |window, cx| { - let editor = cx.new(|cx| CurveEditor::new(1, demo_points(), window, cx)); - let host = Host { - editor, - events: Vec::new(), - }; - cx.subscribe( - &host.editor, - |host: &mut Host, - _e: Entity, - event: &CurveEditorEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(400.0), px(200.0)), |window, cx| { + let editor = cx.new(|cx| CurveEditor::new(1, demo_points(), window, cx)); + let host = Host { + editor, + events: Vec::new(), + }; + cx.subscribe( + &host.editor, + |host: &mut Host, + _e: Entity, + event: &CurveEditorEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Double-click in the middle of the canvas (x=200, y=100). - let pos = point(px(200.0), px(100.0)); - let modifiers = Modifiers::none(); - cx.simulate_event(MouseDownEvent { - position: pos, - modifiers, - button: MouseButton::Left, - click_count: 2, - first_mouse: false, - }); - cx.simulate_event(gpui::MouseUpEvent { - position: pos, - modifiers, - button: MouseButton::Left, - click_count: 2, - }); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Double-click in the middle of the canvas (x=200, y=100). + let pos = point(px(200.0), px(100.0)); + let modifiers = Modifiers::none(); + cx.simulate_event(MouseDownEvent { + position: pos, + modifiers, + button: MouseButton::Left, + click_count: 2, + first_mouse: false, + }); + cx.simulate_event(gpui::MouseUpEvent { + position: pos, + modifiers, + button: MouseButton::Left, + click_count: 2, + }); + cx.run_until_parked(); - let (count, added) = cx.read(|app| { - let host = host.read(app); - ( - host.editor.read(app).points().len(), - host.events - .iter() - .any(|e| matches!(e, CurveEditorEvent::PointAdded { .. })), - ) - }); - assert_eq!(count, 3); - assert!(added); - } + let (count, added) = cx.read(|app| { + let host = host.read(app); + ( + host.editor.read(app).points().len(), + host.events + .iter() + .any(|e| matches!(e, CurveEditorEvent::PointAdded { .. })), + ) + }); + assert_eq!(count, 3); + assert!(added); + } } diff --git a/crates/gpui_widgets/src/dialog/file_dialog.rs b/crates/gpui_widgets/src/dialog/file_dialog.rs index 5bdeecfbdd..b8a1ffb735 100644 --- a/crates/gpui_widgets/src/dialog/file_dialog.rs +++ b/crates/gpui_widgets/src/dialog/file_dialog.rs @@ -8,8 +8,7 @@ //! picker later. use gpui::{ - App, Context, Entity, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, - px, + App, Context, Entity, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, px, }; use gpui_elements::editable_text::{EditableTextState, StringStorage, text_input}; @@ -17,116 +16,124 @@ use super::{DialogButton, Modal, ModalOptions}; /// The content view of a file dialog: a path text field. pub struct FileDialogContent { - editor: Entity, + editor: Entity, } impl FileDialogContent { - /// The path currently entered. - pub fn path(&self, app: &gpui::App) -> SharedString { - self.editor.read(app).as_str().into() - } + /// The path currently entered. + pub fn path(&self, app: &gpui::App) -> SharedString { + self.editor.read(app).as_str().into() + } - /// Set the path shown in the field. - pub fn set_path(&mut self, path: impl Into, cx: &mut Context) { - let path = path.into(); - self.editor.update(cx, |editor, cx| { - editor.emplace(path.as_ref(), cx); - }); - cx.notify(); - } + /// Set the path shown in the field. + pub fn set_path(&mut self, path: impl Into, cx: &mut Context) { + let path = path.into(); + self.editor.update(cx, |editor, cx| { + editor.emplace(path.as_ref(), cx); + }); + cx.notify(); + } } impl Render for FileDialogContent { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let weak = self.editor.downgrade(); - div() - .flex() - .flex_col() - .gap_2() - .child(div().text_color(colors.text).child("Path")) - .child( - div() - .rounded_md() - .border_1() - .border_color(colors.border) - .bg(colors.background) - .px_2() - .py_1() - .child(text_input("gpui-widgets-file-path").state(weak).accepts_input(true)), - ) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let weak = self.editor.downgrade(); + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_color(colors.text).child("Path")) + .child( + div() + .rounded_md() + .border_1() + .border_color(colors.border) + .bg(colors.background) + .px_2() + .py_1() + .child( + text_input("gpui-widgets-file-path") + .state(weak) + .accepts_input(true), + ), + ) + } } /// Build a file dialog. OK is button index `0`, Cancel is index `1`. The host /// reads the chosen path from [`FileDialogContent::path`] when OK is clicked. pub fn file_dialog( - control: usize, - title: impl Into, - window: &mut Window, - cx: &mut App, + control: usize, + title: impl Into, + window: &mut Window, + cx: &mut App, ) -> (Entity, Entity) { - let content = cx.new(|cx| { - let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); - FileDialogContent { editor } - }); - let modal = cx.new(|cx| { - Modal::new( - control, - ModalOptions::new(title, px(420.0)) - .with_button(DialogButton::primary("Open")) - .with_button(DialogButton::cancel("Cancel")), - window, - cx, - ) - .with_content(content.clone()) - }); - (modal, content) + let content = cx.new(|cx| { + let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); + FileDialogContent { editor } + }); + let modal = cx.new(|cx| { + Modal::new( + control, + ModalOptions::new(title, px(420.0)) + .with_button(DialogButton::primary("Open")) + .with_button(DialogButton::cancel("Cancel")), + window, + cx, + ) + .with_content(content.clone()) + }); + (modal, content) } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Render, TestAppContext, VisualTestContext, Window, div, px, size}; + use super::*; + use gpui::{Entity, Render, TestAppContext, VisualTestContext, Window, div, px, size}; - #[gpui::test] - async fn path_round_trips(cx: &mut TestAppContext) { - cx.update(|app| { - let content = app.new(|cx| { - let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); - FileDialogContent { editor } - }); - content.update(app, |content, cx| { - content.set_path("/tmp/movie.mov", cx); - assert_eq!(content.path(cx), "/tmp/movie.mov"); - }); - }); - } + #[gpui::test] + async fn path_round_trips(cx: &mut TestAppContext) { + cx.update(|app| { + let content = app.new(|cx| { + let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); + FileDialogContent { editor } + }); + content.update(app, |content, cx| { + content.set_path("/tmp/movie.mov", cx); + assert_eq!(content.path(cx), "/tmp/movie.mov"); + }); + }); + } - #[gpui::test] - async fn file_dialog_content_renders(cx: &mut TestAppContext) { - struct Host { - content: Entity, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.content.clone()) - } - } + #[gpui::test] + async fn file_dialog_content_renders(cx: &mut TestAppContext) { + struct Host { + content: Entity, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.content.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(400.0), px(120.0)), |_window, cx| { - let content = cx.new(|cx| { - let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); - FileDialogContent { editor } - }); - content.update(cx, |content, cx| content.set_path("/tmp/movie.mov", cx)); - Host { content } - }); - cx.run_until_parked(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); - } + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(400.0), px(120.0)), |_window, cx| { + let content = cx.new(|cx| { + let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); + FileDialogContent { editor } + }); + content.update(cx, |content, cx| content.set_path("/tmp/movie.mov", cx)); + Host { content } + }); + cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); + } } diff --git a/crates/gpui_widgets/src/dialog/message_box.rs b/crates/gpui_widgets/src/dialog/message_box.rs index 1639085a98..ba5f9672ab 100644 --- a/crates/gpui_widgets/src/dialog/message_box.rs +++ b/crates/gpui_widgets/src/dialog/message_box.rs @@ -7,70 +7,75 @@ use super::{DialogButton, Modal, ModalOptions}; /// The severity of a message box. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MessageBoxLevel { - /// Informational. - Info, - /// A warning. - Warning, - /// An error. - Error, + /// Informational. + Info, + /// A warning. + Warning, + /// An error. + Error, } impl MessageBoxLevel { - /// The icon color for this level. - fn color(self) -> gpui::Hsla { - match self { - MessageBoxLevel::Info => gpui::Hsla { - h: 0.6, - s: 0.8, - l: 0.5, - a: 1.0, - }, - MessageBoxLevel::Warning => gpui::Hsla { - h: 0.1, - s: 0.9, - l: 0.5, - a: 1.0, - }, - MessageBoxLevel::Error => gpui::Hsla { - h: 0.0, - s: 0.8, - l: 0.5, - a: 1.0, - }, - } - } + /// The icon color for this level. + fn color(self) -> gpui::Hsla { + match self { + MessageBoxLevel::Info => gpui::Hsla { + h: 0.6, + s: 0.8, + l: 0.5, + a: 1.0, + }, + MessageBoxLevel::Warning => gpui::Hsla { + h: 0.1, + s: 0.9, + l: 0.5, + a: 1.0, + }, + MessageBoxLevel::Error => gpui::Hsla { + h: 0.0, + s: 0.8, + l: 0.5, + a: 1.0, + }, + } + } - /// The glyph shown next to the message. - fn glyph(self) -> &'static str { - match self { - MessageBoxLevel::Info => "ℹ", - MessageBoxLevel::Warning => "⚠", - MessageBoxLevel::Error => "✖", - } - } + /// The glyph shown next to the message. + fn glyph(self) -> &'static str { + match self { + MessageBoxLevel::Info => "ℹ", + MessageBoxLevel::Warning => "⚠", + MessageBoxLevel::Error => "✖", + } + } } /// The content view of a message box. struct MessageContent { - level: MessageBoxLevel, - message: gpui::SharedString, + level: MessageBoxLevel, + message: gpui::SharedString, } impl Render for MessageContent { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - div() - .flex() - .items_start() - .gap_3() - .child( - div() - .w(px(24.0)) - .text_color(self.level.color()) - .child(self.level.glyph()), - ) - .child(div().flex_1().text_color(colors.text).child(self.message.clone())) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .flex() + .items_start() + .gap_3() + .child( + div() + .w(px(24.0)) + .text_color(self.level.color()) + .child(self.level.glyph()), + ) + .child( + div() + .flex_1() + .text_color(colors.text) + .child(self.message.clone()), + ) + } } /// Build a message box modal. @@ -79,95 +84,105 @@ impl Render for MessageContent { /// subscribes to its [`ModalEvent`](super::ModalEvent)s (the OK button is /// button index `0`). pub fn message_box( - control: usize, - level: MessageBoxLevel, - title: impl Into, - message: impl Into, - window: &mut Window, - cx: &mut App, + control: usize, + level: MessageBoxLevel, + title: impl Into, + message: impl Into, + window: &mut Window, + cx: &mut App, ) -> Entity { - let content = cx.new(|_| MessageContent { - level, - message: message.into(), - }); - let modal = cx.new(|cx| { - Modal::new( - control, - ModalOptions::new(title, px(380.0)).with_button(DialogButton::primary("OK")), - window, - cx, - ) - .with_content(content) - }); - modal + let content = cx.new(|_| MessageContent { + level, + message: message.into(), + }); + let modal = cx.new(|cx| { + Modal::new( + control, + ModalOptions::new(title, px(380.0)).with_button(DialogButton::primary("OK")), + window, + cx, + ) + .with_content(content) + }); + modal } #[cfg(test)] mod tests { - use super::*; - use gpui::{Modifiers, TestAppContext, VisualTestContext, px, size}; + use super::*; + use gpui::{Modifiers, TestAppContext, VisualTestContext, px, size}; - #[test] - fn levels_have_distinct_glyphs() { - assert_ne!(MessageBoxLevel::Info.glyph(), MessageBoxLevel::Error.glyph()); - assert_ne!(MessageBoxLevel::Info.color(), MessageBoxLevel::Warning.color()); - } + #[test] + fn levels_have_distinct_glyphs() { + assert_ne!( + MessageBoxLevel::Info.glyph(), + MessageBoxLevel::Error.glyph() + ); + assert_ne!( + MessageBoxLevel::Info.color(), + MessageBoxLevel::Warning.color() + ); + } - #[gpui::test] - async fn ok_button_emits_index_zero(cx: &mut TestAppContext) { - struct Host { - modal: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.modal.clone()) - } - } + #[gpui::test] + async fn ok_button_emits_index_zero(cx: &mut TestAppContext) { + struct Host { + modal: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.modal.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(500.0), px(300.0)), |window, cx| { - let modal = message_box( - 1, - MessageBoxLevel::Error, - "Render failed", - "The export could not be completed.", - window, - cx, - ); - let host = Host { - modal, - events: Vec::new(), - }; - cx.subscribe( - &host.modal, - |host: &mut Host, - _m: Entity, - event: &super::super::ModalEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(500.0), px(300.0)), |window, cx| { + let modal = message_box( + 1, + MessageBoxLevel::Error, + "Render failed", + "The export could not be completed.", + window, + cx, + ); + let host = Host { + modal, + events: Vec::new(), + }; + cx.subscribe( + &host.modal, + |host: &mut Host, + _m: Entity, + event: &super::super::ModalEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Click the OK button (index 0). - let ok = cx.debug_bounds("dialog-button-0").expect("OK button rendered"); - cx.simulate_click(ok.center(), Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Click the OK button (index 0). + let ok = cx + .debug_bounds("dialog-button-0") + .expect("OK button rendered"); + cx.simulate_click(ok.center(), Modifiers::none()); + cx.run_until_parked(); - let routed = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!( - e, - super::super::ModalEvent::ButtonClicked { button: 0, .. } - ) - }) - }); - assert!(routed); - } + let routed = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, super::super::ModalEvent::ButtonClicked { button: 0, .. })) + }); + assert!(routed); + } } diff --git a/crates/gpui_widgets/src/dialog/mod.rs b/crates/gpui_widgets/src/dialog/mod.rs index ad3b3b5663..ee2af83c42 100644 --- a/crates/gpui_widgets/src/dialog/mod.rs +++ b/crates/gpui_widgets/src/dialog/mod.rs @@ -11,343 +11,342 @@ pub mod message_box; pub mod progress; use gpui::{ - AnyView, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Hsla, - KeyDownEvent, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, + AnyView, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Hsla, + KeyDownEvent, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, }; /// How a button behaves in the dialog. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DialogButtonRole { - /// The default action (`enter` triggers it). - Primary, - /// A secondary action. - Secondary, - /// Cancels the dialog (`escape` also triggers it). - Cancel, + /// The default action (`enter` triggers it). + Primary, + /// A secondary action. + Secondary, + /// Cancels the dialog (`escape` also triggers it). + Cancel, } /// A button in a dialog's button row. #[derive(Debug, Clone, PartialEq)] pub struct DialogButton { - /// The button label. - pub label: SharedString, - /// The button's role. - pub role: DialogButtonRole, + /// The button label. + pub label: SharedString, + /// The button's role. + pub role: DialogButtonRole, } impl DialogButton { - /// Create a button. - pub fn new(label: impl Into, role: DialogButtonRole) -> Self { - Self { - label: label.into(), - role, - } - } + /// Create a button. + pub fn new(label: impl Into, role: DialogButtonRole) -> Self { + Self { + label: label.into(), + role, + } + } - /// A primary button. - pub fn primary(label: impl Into) -> Self { - Self::new(label, DialogButtonRole::Primary) - } + /// A primary button. + pub fn primary(label: impl Into) -> Self { + Self::new(label, DialogButtonRole::Primary) + } - /// A cancel button. - pub fn cancel(label: impl Into) -> Self { - Self::new(label, DialogButtonRole::Cancel) - } + /// A cancel button. + pub fn cancel(label: impl Into) -> Self { + Self::new(label, DialogButtonRole::Cancel) + } } /// Configuration for a [`Modal`]. #[derive(Debug, Clone)] pub struct ModalOptions { - /// The title shown in the card's header. - pub title: SharedString, - /// The width of the card. - pub width: gpui::Pixels, - /// The buttons in the footer row. - pub buttons: Vec, + /// The title shown in the card's header. + pub title: SharedString, + /// The width of the card. + pub width: gpui::Pixels, + /// The buttons in the footer row. + pub buttons: Vec, } impl ModalOptions { - /// Create options. - pub fn new(title: impl Into, width: gpui::Pixels) -> Self { - Self { - title: title.into(), - width, - buttons: Vec::new(), - } - } + /// Create options. + pub fn new(title: impl Into, width: gpui::Pixels) -> Self { + Self { + title: title.into(), + width, + buttons: Vec::new(), + } + } - /// Add a button. - pub fn with_button(mut self, button: DialogButton) -> Self { - self.buttons.push(button); - self - } + /// Add a button. + pub fn with_button(mut self, button: DialogButton) -> Self { + self.buttons.push(button); + self + } } /// A request emitted by a modal dialog. #[derive(Debug, Clone, PartialEq)] pub enum ModalEvent { - /// A button was clicked. - ButtonClicked { - /// The modal's stable id. - control: usize, - /// The index of the clicked button in [`ModalOptions::buttons`]. - button: usize, - }, - /// The dialog was dismissed (escape or backdrop). - Dismissed { - /// The modal's stable id. - control: usize, - }, + /// A button was clicked. + ButtonClicked { + /// The modal's stable id. + control: usize, + /// The index of the clicked button in [`ModalOptions::buttons`]. + button: usize, + }, + /// The dialog was dismissed (escape or backdrop). + Dismissed { + /// The modal's stable id. + control: usize, + }, } /// A modal dialog frame: mask, title bar, content slot and button row. pub struct Modal { - control: usize, - options: ModalOptions, - content: Option, - focus_handle: FocusHandle, + control: usize, + options: ModalOptions, + content: Option, + focus_handle: FocusHandle, } impl Modal { - /// Create a modal with `options` (no content yet). - pub fn new( - control: usize, - options: ModalOptions, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - options, - content: None, - focus_handle: cx.focus_handle(), - } - } + /// Create a modal with `options` (no content yet). + pub fn new( + control: usize, + options: ModalOptions, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + options, + content: None, + focus_handle: cx.focus_handle(), + } + } - /// Attach the dialog's content view. - pub fn with_content(mut self, content: impl Into) -> Self { - self.content = Some(content.into()); - self - } + /// Attach the dialog's content view. + pub fn with_content(mut self, content: impl Into) -> Self { + self.content = Some(content.into()); + self + } - /// Replace the content view. - pub fn set_content(&mut self, content: impl Into, cx: &mut Context) { - self.content = Some(content.into()); - cx.notify(); - } + /// Replace the content view. + pub fn set_content(&mut self, content: impl Into, cx: &mut Context) { + self.content = Some(content.into()); + cx.notify(); + } - fn emit_button(&mut self, index: usize, cx: &mut Context) { - cx.emit(ModalEvent::ButtonClicked { - control: self.control, - button: index, - }); - cx.notify(); - } + fn emit_button(&mut self, index: usize, cx: &mut Context) { + cx.emit(ModalEvent::ButtonClicked { + control: self.control, + button: index, + }); + cx.notify(); + } } impl EventEmitter for Modal {} impl Focusable for Modal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for Modal { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let control = self.control; - let buttons = self.options.buttons.clone(); - let primary = buttons - .iter() - .position(|b| b.role == DialogButtonRole::Primary); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let control = self.control; + let buttons = self.options.buttons.clone(); + let primary = buttons + .iter() + .position(|b| b.role == DialogButtonRole::Primary); - // Mask + card, centered. - div() - .id(ElementId::named_usize("gpui-widgets-modal", control)) - .absolute() - .size_full() - .bg(Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 0.4, - }) - .occlude() - .block_mouse_except_scroll() - .flex() - .items_center() - .justify_center() - .track_focus(&self.focus_handle) - .on_key_down(cx.listener(move |this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "escape" => { - cx.emit(ModalEvent::Dismissed { - control: this.control, - }); - cx.notify(); - } - "enter" => { - if let Some(index) = primary { - this.emit_button(index, cx); - } - } - _ => {} - } - })) - .child( - div() - .w(self.options.width) - .rounded_lg() - .border_1() - .border_color(colors.border) - .bg(colors.container) - .debug_selector(|| "dialog-card".into()) - .shadow_lg() - .flex() - .flex_col() - .child( - div() - .px_4() - .py_2() - .border_b_1() - .border_color(colors.border) - .text_color(colors.text) - .child(self.options.title.clone()), - ) - .child( - div() - .p_4() - .flex_1() - .child(if let Some(content) = &self.content { - content.clone().into_any_element() - } else { - div().into_any_element() - }), - ) - .child( - div() - .px_4() - .py_3() - .flex() - .justify_end() - .gap_2() - .children( - buttons - .into_iter() - .enumerate() - .map(|(index, button)| { - let role = button.role; - let label = button.label; - let bg = match role { - DialogButtonRole::Primary => colors.selected, - _ => colors.background, - }; - let text = match role { - DialogButtonRole::Primary => colors.selected_text, - _ => colors.text, - }; - div() - .id(ElementId::named_usize( - format!("gpui-widgets-modal-button-{control}"), - index, - )) - .debug_selector(|| { - format!("dialog-button-{index}").into() - }) - .px_3() - .py_1() - .rounded_md() - .bg(bg) - .text_color(text) - .cursor_pointer() - .on_click(cx.listener( - move |this, _event: &ClickEvent, _window, cx| { - this.emit_button(index, cx); - cx.stop_propagation(); - }, - )) - .child(label) - }), - ), - ), - ) - } + // Mask + card, centered. + div() + .id(ElementId::named_usize("gpui-widgets-modal", control)) + .absolute() + .size_full() + .bg(Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 0.4, + }) + .occlude() + .block_mouse_except_scroll() + .flex() + .items_center() + .justify_center() + .track_focus(&self.focus_handle) + .on_key_down(cx.listener(move |this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "escape" => { + cx.emit(ModalEvent::Dismissed { + control: this.control, + }); + cx.notify(); + } + "enter" => { + if let Some(index) = primary { + this.emit_button(index, cx); + } + } + _ => {} + } + })) + .child( + div() + .w(self.options.width) + .rounded_lg() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .debug_selector(|| "dialog-card".into()) + .shadow_lg() + .flex() + .flex_col() + .child( + div() + .px_4() + .py_2() + .border_b_1() + .border_color(colors.border) + .text_color(colors.text) + .child(self.options.title.clone()), + ) + .child( + div() + .p_4() + .flex_1() + .child(if let Some(content) = &self.content { + content.clone().into_any_element() + } else { + div().into_any_element() + }), + ) + .child(div().px_4().py_3().flex().justify_end().gap_2().children( + buttons.into_iter().enumerate().map(|(index, button)| { + let role = button.role; + let label = button.label; + let bg = match role { + DialogButtonRole::Primary => colors.selected, + _ => colors.background, + }; + let text = match role { + DialogButtonRole::Primary => colors.selected_text, + _ => colors.text, + }; + div() + .id(ElementId::named_usize( + format!("gpui-widgets-modal-button-{control}"), + index, + )) + .debug_selector(|| format!("dialog-button-{index}").into()) + .px_3() + .py_1() + .rounded_md() + .bg(bg) + .text_color(text) + .cursor_pointer() + .on_click(cx.listener( + move |this, _event: &ClickEvent, _window, cx| { + this.emit_button(index, cx); + cx.stop_propagation(); + }, + )) + .child(label) + }), + )), + ) + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, px, size}; + use super::*; + use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, px, size}; - #[test] - fn button_roles_and_primary_index() { - let options = ModalOptions::new("Save", px(400.0)) - .with_button(DialogButton::cancel("Cancel")) - .with_button(DialogButton::primary("Save")); - assert_eq!(options.buttons.len(), 2); - assert_eq!(options.buttons[0].role, DialogButtonRole::Cancel); - assert_eq!(options.buttons[1].role, DialogButtonRole::Primary); - } + #[test] + fn button_roles_and_primary_index() { + let options = ModalOptions::new("Save", px(400.0)) + .with_button(DialogButton::cancel("Cancel")) + .with_button(DialogButton::primary("Save")); + assert_eq!(options.buttons.len(), 2); + assert_eq!(options.buttons[0].role, DialogButtonRole::Cancel); + assert_eq!(options.buttons[1].role, DialogButtonRole::Primary); + } - #[gpui::test] - async fn button_click_emits_routed_event(cx: &mut TestAppContext) { - struct Host { - modal: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.modal.clone()) - } - } + #[gpui::test] + async fn button_click_emits_routed_event(cx: &mut TestAppContext) { + struct Host { + modal: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.modal.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(500.0), px(300.0)), |window, cx| { - let modal = cx.new(|cx| { - Modal::new( - 1, - ModalOptions::new("Prompt", px(360.0)) - .with_button(DialogButton::cancel("Cancel")) - .with_button(DialogButton::primary("Apply")), - window, - cx, - ) - }); - let host = Host { - modal, - events: Vec::new(), - }; - cx.subscribe( - &host.modal, - |host: &mut Host, - _m: Entity, - event: &ModalEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(500.0), px(300.0)), |window, cx| { + let modal = cx.new(|cx| { + Modal::new( + 1, + ModalOptions::new("Prompt", px(360.0)) + .with_button(DialogButton::cancel("Cancel")) + .with_button(DialogButton::primary("Apply")), + window, + cx, + ) + }); + let host = Host { + modal, + events: Vec::new(), + }; + cx.subscribe( + &host.modal, + |host: &mut Host, + _m: Entity, + event: &ModalEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Click the last button in the card's bottom-right corner. - let card = cx.debug_bounds("dialog-card").expect("dialog card rendered"); - eprintln!("card={card:?} btn0={:?} btn1={:?}", - cx.debug_bounds("dialog-button-0"), - cx.debug_bounds("dialog-button-1")); - let btn = cx.debug_bounds("dialog-button-1").expect("button rendered"); - cx.simulate_click(btn.center(), Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Click the last button in the card's bottom-right corner. + let card = cx + .debug_bounds("dialog-card") + .expect("dialog card rendered"); + eprintln!( + "card={card:?} btn0={:?} btn1={:?}", + cx.debug_bounds("dialog-button-0"), + cx.debug_bounds("dialog-button-1") + ); + let btn = cx.debug_bounds("dialog-button-1").expect("button rendered"); + cx.simulate_click(btn.center(), Modifiers::none()); + cx.run_until_parked(); - let routed = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, ModalEvent::ButtonClicked { button: 1, .. }) - }) - }); - assert!(routed, "expected ButtonClicked for the Apply (index 1) button"); - } + let routed = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, ModalEvent::ButtonClicked { button: 1, .. })) + }); + assert!( + routed, + "expected ButtonClicked for the Apply (index 1) button" + ); + } } diff --git a/crates/gpui_widgets/src/dialog/progress.rs b/crates/gpui_widgets/src/dialog/progress.rs index d4827d0541..6855dcceab 100644 --- a/crates/gpui_widgets/src/dialog/progress.rs +++ b/crates/gpui_widgets/src/dialog/progress.rs @@ -9,144 +9,148 @@ use super::{DialogButton, Modal, ModalOptions}; /// The content view of a progress dialog: a labeled progress bar. pub struct ProgressContent { - label: gpui::SharedString, - /// Progress in `0..=1`. - fraction: f32, + label: gpui::SharedString, + /// Progress in `0..=1`. + fraction: f32, } impl ProgressContent { - /// Create a progress content view. - pub fn new(label: impl Into, fraction: f32) -> Self { - Self { - label: label.into(), - fraction: fraction.clamp(0.0, 1.0), - } - } + /// Create a progress content view. + pub fn new(label: impl Into, fraction: f32) -> Self { + Self { + label: label.into(), + fraction: fraction.clamp(0.0, 1.0), + } + } - /// Update the progress and repaint. - pub fn set_progress(&mut self, fraction: f32, cx: &mut Context) { - self.fraction = fraction.clamp(0.0, 1.0); - cx.notify(); - } + /// Update the progress and repaint. + pub fn set_progress(&mut self, fraction: f32, cx: &mut Context) { + self.fraction = fraction.clamp(0.0, 1.0); + cx.notify(); + } - /// The current progress. - pub fn fraction(&self) -> f32 { - self.fraction - } + /// The current progress. + pub fn fraction(&self) -> f32 { + self.fraction + } } impl Render for ProgressContent { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let fraction = self.fraction; - div() - .flex() - .flex_col() - .gap_2() - .child(div().text_color(colors.text).child(self.label.clone())) - .child( - div() - .h(px(10.0)) - .rounded_full() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .overflow_hidden() - .child( - div() - .h_full() - .w(px((fraction * 100.0).clamp(0.0, 100.0))) - .bg(colors.selected), - ), - ) - .child( - div() - .text_color(colors.disabled) - .child(format!("{:.0}%", fraction * 100.0)), - ) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let fraction = self.fraction; + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_color(colors.text).child(self.label.clone())) + .child( + div() + .h(px(10.0)) + .rounded_full() + .bg(colors.background) + .border_1() + .border_color(colors.border) + .overflow_hidden() + .child( + div() + .h_full() + .w(px((fraction * 100.0).clamp(0.0, 100.0))) + .bg(colors.selected), + ), + ) + .child( + div() + .text_color(colors.disabled) + .child(format!("{:.0}%", fraction * 100.0)), + ) + } } /// Build a progress dialog with a Cancel button (index `1`) and an implicit /// primary "Run" button (index `0`). Returns the modal and its content so the /// host can drive the bar. pub fn progress_dialog( - control: usize, - title: impl Into, - label: impl Into, - window: &mut Window, - cx: &mut App, + control: usize, + title: impl Into, + label: impl Into, + window: &mut Window, + cx: &mut App, ) -> (Entity, Entity) { - let content = cx.new(|_| ProgressContent::new(label, 0.0)); - let modal = cx.new(|cx| { - Modal::new( - control, - ModalOptions::new(title, px(360.0)) - .with_button(DialogButton::primary("Run")) - .with_button(DialogButton::cancel("Cancel")), - window, - cx, - ) - .with_content(content.clone()) - }); - (modal, content) + let content = cx.new(|_| ProgressContent::new(label, 0.0)); + let modal = cx.new(|cx| { + Modal::new( + control, + ModalOptions::new(title, px(360.0)) + .with_button(DialogButton::primary("Run")) + .with_button(DialogButton::cancel("Cancel")), + window, + cx, + ) + .with_content(content.clone()) + }); + (modal, content) } #[cfg(test)] mod tests { - use super::*; - use gpui::TestAppContext; + use super::*; + use gpui::TestAppContext; - #[test] - fn progress_clamps_at_construction() { - assert_eq!(ProgressContent::new("Encoding", 2.0).fraction(), 1.0); - assert_eq!(ProgressContent::new("Encoding", -0.5).fraction(), 0.0); - } + #[test] + fn progress_clamps_at_construction() { + assert_eq!(ProgressContent::new("Encoding", 2.0).fraction(), 1.0); + assert_eq!(ProgressContent::new("Encoding", -0.5).fraction(), 0.0); + } - #[gpui::test] - async fn set_progress_clamps(cx: &mut TestAppContext) { - cx.update(|app| { - let content = app.new(|_| ProgressContent::new("Encoding", 0.0)); - content.update(app, |content, cx| { - content.set_progress(1.5, cx); - assert_eq!(content.fraction(), 1.0); - content.set_progress(-1.0, cx); - assert_eq!(content.fraction(), 0.0); - }); - }); - } + #[gpui::test] + async fn set_progress_clamps(cx: &mut TestAppContext) { + cx.update(|app| { + let content = app.new(|_| ProgressContent::new("Encoding", 0.0)); + content.update(app, |content, cx| { + content.set_progress(1.5, cx); + assert_eq!(content.fraction(), 1.0); + content.set_progress(-1.0, cx); + assert_eq!(content.fraction(), 0.0); + }); + }); + } - #[gpui::test] - async fn progress_content_renders(cx: &mut TestAppContext) { - use gpui::{Entity, Render, VisualTestContext, Window, div, prelude::*, px, size}; + #[gpui::test] + async fn progress_content_renders(cx: &mut TestAppContext) { + use gpui::{Entity, Render, VisualTestContext, Window, div, prelude::*, px, size}; - struct Host { - content: Entity, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.content.clone()) - } - } + struct Host { + content: Entity, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.content.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(320.0), px(80.0)), |_window, cx| { - let content = cx.new(|_| ProgressContent::new("Encoding", 0.5)); - Host { content } - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - window - .update(cx, |host, _, cx| { - host.content - .update(cx, |content, cx| content.set_progress(0.75, cx)); - }) - .unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); - let fraction = cx.read(|app| host.read(app).content.read(app).fraction()); - assert_eq!(fraction, 0.75); - } + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(320.0), px(80.0)), |_window, cx| { + let content = cx.new(|_| ProgressContent::new("Encoding", 0.5)); + Host { content } + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + window + .update(cx, |host, _, cx| { + host.content + .update(cx, |content, cx| content.set_progress(0.75, cx)); + }) + .unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); + let fraction = cx.read(|app| host.read(app).content.read(app).fraction()); + assert_eq!(fraction, 0.75); + } } diff --git a/crates/gpui_widgets/src/i18n.rs b/crates/gpui_widgets/src/i18n.rs index 8d155145f5..d97f6882e0 100644 --- a/crates/gpui_widgets/src/i18n.rs +++ b/crates/gpui_widgets/src/i18n.rs @@ -12,20 +12,23 @@ pub use gpui::i18n::*; #[cfg(test)] mod tests { - use super::*; + use super::*; - /// The hook is a thin re-export: installing a table through the widget - /// path is visible to `gpui::i18n::tr` and vice versa. - #[test] - fn widget_path_shares_the_gpui_table() { - clear_table(); - assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); + /// The hook is a thin re-export: installing a table through the widget + /// path is visible to `gpui::i18n::tr` and vice versa. + #[test] + fn widget_path_shares_the_gpui_table() { + clear_table(); + assert_eq!(tr("viewer.safe_frames", "安全框"), "安全框"); - let mut table = StringTable::new(); - table.insert("viewer.safe_frames".into(), "Safe Frames".into()); - set_table(table); + let mut table = StringTable::new(); + table.insert("viewer.safe_frames".into(), "Safe Frames".into()); + set_table(table); - assert_eq!(gpui::i18n::tr("viewer.safe_frames", "安全框"), "Safe Frames"); - clear_table(); - } + assert_eq!( + gpui::i18n::tr("viewer.safe_frames", "安全框"), + "Safe Frames" + ); + clear_table(); + } } diff --git a/crates/gpui_widgets/src/icons.rs b/crates/gpui_widgets/src/icons.rs index 8a4f0e10da..325faf865f 100644 --- a/crates/gpui_widgets/src/icons.rs +++ b/crates/gpui_widgets/src/icons.rs @@ -24,11 +24,11 @@ impl Global for IconResolverGlobal {} /// Registers the host's icon resolver. Call once at startup, before any /// window renders. pub fn set_resolver(resolver: IconResolver, cx: &mut App) { - cx.set_global(IconResolverGlobal(resolver)); + cx.set_global(IconResolverGlobal(resolver)); } /// The file path of the named icon in the current theme, if resolvable. pub fn path(name: &str, cx: &App) -> Option { - cx.try_global::() - .and_then(|global| (global.0)(name, cx)) + cx.try_global::() + .and_then(|global| (global.0)(name, cx)) } diff --git a/crates/gpui_widgets/src/keyable.rs b/crates/gpui_widgets/src/keyable.rs index e6f61196b3..a5f2e9e75d 100644 --- a/crates/gpui_widgets/src/keyable.rs +++ b/crates/gpui_widgets/src/keyable.rs @@ -6,19 +6,19 @@ //! control; the widget itself never touches the engine. use gpui::{ - App, Bounds, Div, ElementId, Hsla, Pixels, Stateful, Window, canvas, colors::DefaultColors, - div, fill, point, prelude::*, px, size, + App, Bounds, Div, ElementId, Hsla, Pixels, Stateful, Window, canvas, colors::DefaultColors, + div, fill, point, prelude::*, px, size, }; /// The keying state of a control at the current playhead frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyingState { - /// The control has no keyframe. - NoKey, - /// The control has a keyframe somewhere on the timeline. - HasKey, - /// The control has a keyframe exactly at the current frame. - AtCurrentFrame, + /// The control has no keyframe. + NoKey, + /// The control has a keyframe somewhere on the timeline. + HasKey, + /// The control has a keyframe exactly at the current frame. + AtCurrentFrame, } /// A request emitted when a keying diamond is clicked. @@ -27,32 +27,32 @@ pub enum KeyingState { /// `SliderEvent::Keying(KeyingRequest)`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeyingRequest { - /// The stable id of the control that owns the diamond. - pub control: usize, - /// The keying state the control reported when the diamond was painted. - pub state: KeyingState, + /// The stable id of the control that owns the diamond. + pub control: usize, + /// The keying state the control reported when the diamond was painted. + pub state: KeyingState, } impl KeyingRequest { - /// Create a keying request for `control`. - pub fn new(control: usize, state: KeyingState) -> Self { - Self { control, state } - } + /// Create a keying request for `control`. + pub fn new(control: usize, state: KeyingState) -> Self { + Self { control, state } + } } /// Accent used for "has a key" diamonds. const KEY_COLOR: Hsla = Hsla { - h: 0.09, - s: 0.75, - l: 0.55, - a: 1.0, + h: 0.09, + s: 0.75, + l: 0.55, + a: 1.0, }; /// Brighter accent for "key at current frame". const KEY_ACTIVE_COLOR: Hsla = Hsla { - h: 0.09, - s: 0.9, - l: 0.68, - a: 1.0, + h: 0.09, + s: 0.9, + l: 0.68, + a: 1.0, }; /// Base opacity of an empty diamond, so missing keys read dimmer. const EMPTY_OPACITY: f32 = 0.45; @@ -64,128 +64,126 @@ const DIAMOND_SIZE: f32 = 14.0; /// The returned element is interactive: chain `.on_click(...)` (or /// `cx.listener(...)`) to receive clicks, then emit a [`KeyingRequest`]. pub fn keying_diamond(control: usize, state: KeyingState) -> Stateful
{ - div() - .size(px(20.0)) - .flex() - .items_center() - .justify_center() - .opacity(if state == KeyingState::NoKey { - EMPTY_OPACITY - } else { - 1.0 - }) - .hover(|style| style.opacity(1.0)) - .child(canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, cx| paint_diamond(bounds, state, window, cx), - )) - .id(ElementId::named_usize("gpui-widgets-keying", control)) + div() + .size(px(20.0)) + .flex() + .items_center() + .justify_center() + .opacity(if state == KeyingState::NoKey { + EMPTY_OPACITY + } else { + 1.0 + }) + .hover(|style| style.opacity(1.0)) + .child(canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, cx| paint_diamond(bounds, state, window, cx), + )) + .id(ElementId::named_usize("gpui-widgets-keying", control)) } /// A ready-made [`KeyingRequest`] for this diamond's state. pub fn keying_request(control: usize, state: KeyingState) -> KeyingRequest { - KeyingRequest::new(control, state) + KeyingRequest::new(control, state) } fn paint_diamond(bounds: Bounds, state: KeyingState, window: &mut Window, cx: &mut App) { - use gpui::PathBuilder; - let colors = cx.default_colors().clone(); - let center = bounds.center(); - let half = px(DIAMOND_SIZE / 2.0 - 2.0); - let (fill_color, border_color) = match state { - KeyingState::NoKey => ( - Hsla::from(colors.disabled), - Hsla::from(colors.disabled), - ), - KeyingState::HasKey => (KEY_COLOR, KEY_COLOR), - KeyingState::AtCurrentFrame => (KEY_ACTIVE_COLOR, KEY_ACTIVE_COLOR), - }; + use gpui::PathBuilder; + let colors = cx.default_colors().clone(); + let center = bounds.center(); + let half = px(DIAMOND_SIZE / 2.0 - 2.0); + let (fill_color, border_color) = match state { + KeyingState::NoKey => (Hsla::from(colors.disabled), Hsla::from(colors.disabled)), + KeyingState::HasKey => (KEY_COLOR, KEY_COLOR), + KeyingState::AtCurrentFrame => (KEY_ACTIVE_COLOR, KEY_ACTIVE_COLOR), + }; - // The diamond body is filled when a key exists, hollow otherwise. - if state != KeyingState::NoKey { - let mut body = PathBuilder::fill(); - body.move_to(point(center.x, center.y - half)); - body.line_to(point(center.x + half, center.y)); - body.line_to(point(center.x, center.y + half)); - body.line_to(point(center.x - half, center.y)); - body.close(); - if let Ok(path) = body.build() { - window.paint_path(path, fill_color); - } - } + // The diamond body is filled when a key exists, hollow otherwise. + if state != KeyingState::NoKey { + let mut body = PathBuilder::fill(); + body.move_to(point(center.x, center.y - half)); + body.line_to(point(center.x + half, center.y)); + body.line_to(point(center.x, center.y + half)); + body.line_to(point(center.x - half, center.y)); + body.close(); + if let Ok(path) = body.build() { + window.paint_path(path, fill_color); + } + } - // The outline is always painted so the diamond is visible when empty. - let mut outline = PathBuilder::stroke(px(1.0)); - outline.move_to(point(center.x, center.y - half)); - outline.line_to(point(center.x + half, center.y)); - outline.line_to(point(center.x, center.y + half)); - outline.line_to(point(center.x - half, center.y)); - outline.close(); - if let Ok(path) = outline.build() { - window.paint_path(path, border_color); - } + // The outline is always painted so the diamond is visible when empty. + let mut outline = PathBuilder::stroke(px(1.0)); + outline.move_to(point(center.x, center.y - half)); + outline.line_to(point(center.x + half, center.y)); + outline.line_to(point(center.x, center.y + half)); + outline.line_to(point(center.x - half, center.y)); + outline.close(); + if let Ok(path) = outline.build() { + window.paint_path(path, border_color); + } - // A center dot marks a key exactly at the current frame. - if state == KeyingState::AtCurrentFrame { - let origin = point(center.x - px(2.0), center.y - px(2.0)); - window.paint_quad(fill( - Bounds::new(origin, size(px(4.0), px(4.0))), - Hsla::from(colors.background), - )); - } + // A center dot marks a key exactly at the current frame. + if state == KeyingState::AtCurrentFrame { + let origin = point(center.x - px(2.0), center.y - px(2.0)); + window.paint_quad(fill( + Bounds::new(origin, size(px(4.0), px(4.0))), + Hsla::from(colors.background), + )); + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Context, Render, TestAppContext, Window, div, px, size}; + use super::*; + use gpui::{Context, Render, TestAppContext, Window, div, px, size}; - struct Host { - control: usize, - state: KeyingState, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(keying_diamond(self.control, self.state)) - } - } + struct Host { + control: usize, + state: KeyingState, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(keying_diamond(self.control, self.state)) + } + } - #[gpui::test] - async fn diamond_renders_in_a_window(cx: &mut TestAppContext) { - - use gpui::VisualTestContext; - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(60.0), px(60.0)), |_window, _cx| Host { - control: 3, - state: KeyingState::AtCurrentFrame, - }); - cx.run_until_parked(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); - } + #[gpui::test] + async fn diamond_renders_in_a_window(cx: &mut TestAppContext) { + use gpui::VisualTestContext; + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(60.0), px(60.0)), |_window, _cx| Host { + control: 3, + state: KeyingState::AtCurrentFrame, + }); + cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); + } - #[test] - fn request_round_trips() { - let request = KeyingRequest::new(7, KeyingState::AtCurrentFrame); - assert_eq!(request.control, 7); - assert_eq!(request.state, KeyingState::AtCurrentFrame); - assert_ne!(request, KeyingRequest::new(7, KeyingState::HasKey)); - } + #[test] + fn request_round_trips() { + let request = KeyingRequest::new(7, KeyingState::AtCurrentFrame); + assert_eq!(request.control, 7); + assert_eq!(request.state, KeyingState::AtCurrentFrame); + assert_ne!(request, KeyingRequest::new(7, KeyingState::HasKey)); + } - #[test] - fn states_are_distinct() { - assert_ne!(KeyingState::NoKey, KeyingState::HasKey); - assert_ne!(KeyingState::HasKey, KeyingState::AtCurrentFrame); - assert_ne!(KeyingState::NoKey, KeyingState::AtCurrentFrame); - } + #[test] + fn states_are_distinct() { + assert_ne!(KeyingState::NoKey, KeyingState::HasKey); + assert_ne!(KeyingState::HasKey, KeyingState::AtCurrentFrame); + assert_ne!(KeyingState::NoKey, KeyingState::AtCurrentFrame); + } - #[test] - fn keying_request_helper_matches() { - assert_eq!( - keying_request(3, KeyingState::NoKey), - KeyingRequest::new(3, KeyingState::NoKey) - ); - } + #[test] + fn keying_request_helper_matches() { + assert_eq!( + keying_request(3, KeyingState::NoKey), + KeyingRequest::new(3, KeyingState::NoKey) + ); + } } diff --git a/crates/gpui_widgets/src/lib.rs b/crates/gpui_widgets/src/lib.rs index 3fa128c43b..46ac41c61b 100644 --- a/crates/gpui_widgets/src/lib.rs +++ b/crates/gpui_widgets/src/lib.rs @@ -39,45 +39,45 @@ pub mod viewer; #[cfg(test)] mod tests { - use gpui::prelude::*; - use gpui::{AbsoluteLength, DefiniteLength, div, rems}; + use gpui::prelude::*; + use gpui::{AbsoluteLength, DefiniteLength, div, rems}; - /// Fractional spacing helpers exist on the [`Styled`](gpui::Styled) trait - /// (via `gpui_macros::padding_style_methods!` / `margin_style_methods!`), - /// named after Tailwind's fractional scale: `0p5`, `1p5`, `2p5`, `3p5` - /// (i.e. 0.5/1.5/2.5/3.5 units of 4px = 2/6/10/14px). - /// - /// We deliberately do **not** add `_0_5`-style aliases (`py_0_5`, - /// `px_1_5`, ...): every fractional value they would name already exists - /// under the established `0p5`/`1p5`/`2p5`/`3p5` convention used - /// throughout gpui and gpui_widgets, and a second naming scheme for the - /// same helpers would only fragment the API surface. This test pins the - /// helpers (and their values) so a future refactor of the style macros - /// cannot silently drop them. - #[test] - fn fractional_spacing_helpers_exist() { - let mut padding = div().py_0p5().px_1p5(); - assert_eq!( - padding.style().padding.top, - Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.125)))) - ); - assert_eq!( - padding.style().padding.left, - Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.375)))) - ); - assert_eq!( - padding.style().padding.right, - Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.375)))) - ); + /// Fractional spacing helpers exist on the [`Styled`](gpui::Styled) trait + /// (via `gpui_macros::padding_style_methods!` / `margin_style_methods!`), + /// named after Tailwind's fractional scale: `0p5`, `1p5`, `2p5`, `3p5` + /// (i.e. 0.5/1.5/2.5/3.5 units of 4px = 2/6/10/14px). + /// + /// We deliberately do **not** add `_0_5`-style aliases (`py_0_5`, + /// `px_1_5`, ...): every fractional value they would name already exists + /// under the established `0p5`/`1p5`/`2p5`/`3p5` convention used + /// throughout gpui and gpui_widgets, and a second naming scheme for the + /// same helpers would only fragment the API surface. This test pins the + /// helpers (and their values) so a future refactor of the style macros + /// cannot silently drop them. + #[test] + fn fractional_spacing_helpers_exist() { + let mut padding = div().py_0p5().px_1p5(); + assert_eq!( + padding.style().padding.top, + Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.125)))) + ); + assert_eq!( + padding.style().padding.left, + Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.375)))) + ); + assert_eq!( + padding.style().padding.right, + Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.375)))) + ); - let mut margin = div().my_2p5().pt_3p5(); - assert_eq!( - margin.style().margin.top, - Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.625))).into()) - ); - assert_eq!( - margin.style().padding.top, - Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.875))).into()) - ); - } + let mut margin = div().my_2p5().pt_3p5(); + assert_eq!( + margin.style().margin.top, + Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.625))).into()) + ); + assert_eq!( + margin.style().padding.top, + Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.875))).into()) + ); + } } diff --git a/crates/gpui_widgets/src/menu/mod.rs b/crates/gpui_widgets/src/menu/mod.rs index 47364edc0a..7289d6882c 100644 --- a/crates/gpui_widgets/src/menu/mod.rs +++ b/crates/gpui_widgets/src/menu/mod.rs @@ -10,9 +10,9 @@ pub mod model; use gpui::{ - Anchor, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, KeyDownEvent, - MouseButton, MouseUpEvent, Pixels, Point, Render, SharedString, Window, anchored, - colors::DefaultColors, deferred, div, point, prelude::*, px, + Anchor, App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, + KeyDownEvent, MouseButton, MouseUpEvent, Pixels, Point, Render, SharedString, Window, anchored, + colors::DefaultColors, deferred, div, point, prelude::*, px, }; pub use model::{Menu, MenuItem}; @@ -22,866 +22,878 @@ const ROW_HEIGHT: f32 = 26.0; /// A fully transparent color (for un-hovered rows). fn transparent() -> gpui::Rgba { - gpui::Rgba { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - } + gpui::Rgba { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + } } /// A request emitted by a menu bar. #[derive(Debug, Clone, PartialEq)] pub enum MenuBarEvent { - /// An item was activated. - Triggered { - /// The menu bar's stable id. - control: usize, - /// The item's id (from [`MenuItem::id`]). - item: usize, - /// The item's label. - label: SharedString, - }, - /// A menu was opened. - MenuOpened { - /// The menu bar's stable id. - control: usize, - /// The menu index. - index: usize, - }, - /// The open menu was closed. - MenuClosed { - /// The menu bar's stable id. - control: usize, - }, + /// An item was activated. + Triggered { + /// The menu bar's stable id. + control: usize, + /// The item's id (from [`MenuItem::id`]). + item: usize, + /// The item's label. + label: SharedString, + }, + /// A menu was opened. + MenuOpened { + /// The menu bar's stable id. + control: usize, + /// The menu index. + index: usize, + }, + /// The open menu was closed. + MenuClosed { + /// The menu bar's stable id. + control: usize, + }, } /// A request emitted by a context menu. #[derive(Debug, Clone, PartialEq)] pub struct ContextMenuEvent { - /// The item's id (from [`MenuItem::id`]). - pub item: usize, - /// The item's label. - pub label: SharedString, + /// The item's id (from [`MenuItem::id`]). + pub item: usize, + /// The item's label. + pub label: SharedString, } /// A titled menu in a menu bar. #[derive(Debug, Clone, PartialEq)] pub struct MenuBarEntry { - /// The title shown in the bar. - pub title: SharedString, - /// The menu opened by the title. - pub menu: Menu, + /// The title shown in the bar. + pub title: SharedString, + /// The menu opened by the title. + pub menu: Menu, } impl MenuBarEntry { - /// Create an entry. - pub fn new(title: impl Into, menu: Menu) -> Self { - Self { - title: title.into(), - menu, - } - } + /// Create an entry. + pub fn new(title: impl Into, menu: Menu) -> Self { + Self { + title: title.into(), + menu, + } + } } /// A horizontal menu bar with drop-down menus. pub struct MenuBar { - control: usize, - entries: Vec, - focus_handle: FocusHandle, - open: Option, - popup_position: Point, - was_open_at_down: bool, - hovered: Option, - submenu: Option, + control: usize, + entries: Vec, + focus_handle: FocusHandle, + open: Option, + popup_position: Point, + was_open_at_down: bool, + hovered: Option, + submenu: Option, } impl MenuBar { - /// Create a menu bar. - pub fn new( - control: usize, - entries: Vec, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - entries, - focus_handle: cx.focus_handle(), - open: None, - popup_position: Point::default(), - was_open_at_down: false, - hovered: None, - submenu: None, - } - } + /// Create a menu bar. + pub fn new( + control: usize, + entries: Vec, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + entries, + focus_handle: cx.focus_handle(), + open: None, + popup_position: Point::default(), + was_open_at_down: false, + hovered: None, + submenu: None, + } + } - /// Whether any menu is open. - pub fn is_open(&self) -> bool { - self.open.is_some() - } + /// Whether any menu is open. + pub fn is_open(&self) -> bool { + self.open.is_some() + } - /// Sets the checked state of the menu item with `id` across all entries - /// (searching submenus recursively), so a host can toggle a checkmark at - /// runtime without rebuilding the [`MenuBar`]. - /// - /// The checkmark appears on the next repaint (the renderer reads - /// `checked` per frame); pair with a `cx.notify()` after the call. - /// Returns whether an item with that id was found. - pub fn set_item_checked(&mut self, id: usize, checked: bool) -> bool { - let mut found = false; - for entry in &mut self.entries { - found |= entry.menu.set_item_checked(id, checked); - } - found - } + /// Sets the checked state of the menu item with `id` across all entries + /// (searching submenus recursively), so a host can toggle a checkmark at + /// runtime without rebuilding the [`MenuBar`]. + /// + /// The checkmark appears on the next repaint (the renderer reads + /// `checked` per frame); pair with a `cx.notify()` after the call. + /// Returns whether an item with that id was found. + pub fn set_item_checked(&mut self, id: usize, checked: bool) -> bool { + let mut found = false; + for entry in &mut self.entries { + found |= entry.menu.set_item_checked(id, checked); + } + found + } - fn open_menu(&mut self, index: usize, position: Point, cx: &mut Context) { - if self.open != Some(index) { - self.open = Some(index); - self.popup_position = position; - self.hovered = None; - self.submenu = None; - cx.emit(MenuBarEvent::MenuOpened { - control: self.control, - index, - }); - cx.notify(); - } - } + fn open_menu(&mut self, index: usize, position: Point, cx: &mut Context) { + if self.open != Some(index) { + self.open = Some(index); + self.popup_position = position; + self.hovered = None; + self.submenu = None; + cx.emit(MenuBarEvent::MenuOpened { + control: self.control, + index, + }); + cx.notify(); + } + } - fn close_menu(&mut self, cx: &mut Context) { - if self.open.take().is_some() { - self.hovered = None; - self.submenu = None; - cx.emit(MenuBarEvent::MenuClosed { - control: self.control, - }); - cx.notify(); - } - } + fn close_menu(&mut self, cx: &mut Context) { + if self.open.take().is_some() { + self.hovered = None; + self.submenu = None; + cx.emit(MenuBarEvent::MenuClosed { + control: self.control, + }); + cx.notify(); + } + } - fn trigger(&mut self, item: usize, cx: &mut Context) { - let label = self - .open - .as_ref() - .and_then(|index| self.entries.get(*index)) - .and_then(|entry| entry.menu.items.iter().find(|i| i.id == item)) - .map(|i| i.label.clone()) - .or_else(|| self.find_label(item)) - .unwrap_or_default(); - cx.emit(MenuBarEvent::Triggered { - control: self.control, - item, - label, - }); - self.close_menu(cx); - } + fn trigger(&mut self, item: usize, cx: &mut Context) { + let label = self + .open + .as_ref() + .and_then(|index| self.entries.get(*index)) + .and_then(|entry| entry.menu.items.iter().find(|i| i.id == item)) + .map(|i| i.label.clone()) + .or_else(|| self.find_label(item)) + .unwrap_or_default(); + cx.emit(MenuBarEvent::Triggered { + control: self.control, + item, + label, + }); + self.close_menu(cx); + } - /// The label of an item with `id` anywhere in the open menu, including - /// nested submenus (the fallback when the item is not a top-level row). - fn find_label(&self, item: usize) -> Option { - let index = self.open?; - fn search(menu: &Menu, item: usize) -> Option { - menu.items - .iter() - .find_map(|i| { - if i.id == item { - Some(i.label.clone()) - } else { - i.submenu.as_deref().and_then(|sub| search(sub, item)) - } - }) - } - search(&self.entries[index].menu, item) - } + /// The label of an item with `id` anywhere in the open menu, including + /// nested submenus (the fallback when the item is not a top-level row). + fn find_label(&self, item: usize) -> Option { + let index = self.open?; + fn search(menu: &Menu, item: usize) -> Option { + menu.items.iter().find_map(|i| { + if i.id == item { + Some(i.label.clone()) + } else { + i.submenu.as_deref().and_then(|sub| search(sub, item)) + } + }) + } + search(&self.entries[index].menu, item) + } - fn navigate(&mut self, delta: i32, cx: &mut Context) { - if let Some(index) = self.open { - let menu = &self.entries[index].menu; - if let Some(next) = menu.navigate(self.hovered, delta) { - self.hovered = Some(next); - self.submenu = None; - cx.notify(); - } - } - } + fn navigate(&mut self, delta: i32, cx: &mut Context) { + if let Some(index) = self.open { + let menu = &self.entries[index].menu; + if let Some(next) = menu.navigate(self.hovered, delta) { + self.hovered = Some(next); + self.submenu = None; + cx.notify(); + } + } + } } impl EventEmitter for MenuBar {} impl Focusable for MenuBar { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for MenuBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let mut bar = div() - .id(ElementId::named_usize("gpui-widgets-menubar", self.control)) - .flex() - .items_center() - .px_2() - .gap_1() - .bg(colors.container); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let mut bar = div() + .id(ElementId::named_usize("gpui-widgets-menubar", self.control)) + .flex() + .items_center() + .px_2() + .gap_1() + .bg(colors.container); - for (index, entry) in self.entries.clone().into_iter().enumerate() { - let is_open = self.open == Some(index); - bar = bar.child( - div() - .id(ElementId::named_usize( - format!("gpui-widgets-menu-title-{}", self.control), - index, - )) - .px_2() - .py_1() - .rounded_md() - .bg(if is_open { colors.selected } else { transparent() }) - .text_color(if is_open { - colors.selected_text - } else { - colors.text - }) - .cursor_pointer() - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _event: &gpui::MouseDownEvent, _window, _cx| { - this.was_open_at_down = this.open.is_some(); - }), - ) - .on_click(cx.listener( - move |this, event: &ClickEvent, _window, cx| { - if this.was_open_at_down { - this.close_menu(cx); - } else { - this.open_menu(index, event.position(), cx); - } - cx.stop_propagation(); - }, - )) - .child(entry.title), - ); - } + for (index, entry) in self.entries.clone().into_iter().enumerate() { + let is_open = self.open == Some(index); + bar = bar.child( + div() + .id(ElementId::named_usize( + format!("gpui-widgets-menu-title-{}", self.control), + index, + )) + .px_2() + .py_1() + .rounded_md() + .bg(if is_open { + colors.selected + } else { + transparent() + }) + .text_color(if is_open { + colors.selected_text + } else { + colors.text + }) + .cursor_pointer() + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _event: &gpui::MouseDownEvent, _window, _cx| { + this.was_open_at_down = this.open.is_some(); + }), + ) + .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + if this.was_open_at_down { + this.close_menu(cx); + } else { + this.open_menu(index, event.position(), cx); + } + cx.stop_propagation(); + })) + .child(entry.title), + ); + } - // The open menu popup. - if let Some(open_index) = self.open { - let entry = self.entries[open_index].clone(); - let hovered = self.hovered; - let menu_popup = menu_popup_element( - self.control, - &entry.menu, - hovered, - "menu-popup", - &colors, - cx.listener(|this, item: &MenuClicked, _window, cx| { - this.trigger(item.id, cx); - }), - cx.listener(|this, item: &MenuHovered, _window, cx| { - // Mouse hover drives both the row highlight and the - // submenu: remember the hovered row so the render pass - // can open the nested menu next to it. - this.hovered = Some(item.index); - this.submenu = if item.submenu { - Some(item.index) - } else { - None - }; - cx.notify(); - }), - ) - .track_focus(&self.focus_handle) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event: &MouseUpEvent, _window, cx| { - this.close_menu(cx); - }), - ) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "up" => this.navigate(-1, cx), - "down" => this.navigate(1, cx), - "enter" | "space" => { - if let Some(hovered) = this.hovered { - if let Some(item) = entry_at(this, hovered) { - if item.enabled && item.submenu.is_none() { - this.trigger(item.id, cx); - } - } - } - } - "escape" | "left" => this.close_menu(cx), - _ => {} - } - })); + // The open menu popup. + if let Some(open_index) = self.open { + let entry = self.entries[open_index].clone(); + let hovered = self.hovered; + let menu_popup = menu_popup_element( + self.control, + &entry.menu, + hovered, + "menu-popup", + &colors, + cx.listener(|this, item: &MenuClicked, _window, cx| { + this.trigger(item.id, cx); + }), + cx.listener(|this, item: &MenuHovered, _window, cx| { + // Mouse hover drives both the row highlight and the + // submenu: remember the hovered row so the render pass + // can open the nested menu next to it. + this.hovered = Some(item.index); + this.submenu = if item.submenu { Some(item.index) } else { None }; + cx.notify(); + }), + ) + .track_focus(&self.focus_handle) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event: &MouseUpEvent, _window, cx| { + this.close_menu(cx); + }), + ) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "up" => this.navigate(-1, cx), + "down" => this.navigate(1, cx), + "enter" | "space" => { + if let Some(hovered) = this.hovered { + if let Some(item) = entry_at(this, hovered) { + if item.enabled && item.submenu.is_none() { + this.trigger(item.id, cx); + } + } + } + } + "escape" | "left" => this.close_menu(cx), + _ => {} + } + })); - bar = bar.child( - deferred( - anchored() - .position(self.popup_position) - .anchor(Anchor::TopLeft) - .offset(point(px(0.0), px(ROW_HEIGHT))) - .snap_to_window_with_margin(px(8.0)) - .child(menu_popup), - ) - .with_priority(1), - ); + bar = bar.child( + deferred( + anchored() + .position(self.popup_position) + .anchor(Anchor::TopLeft) + .offset(point(px(0.0), px(ROW_HEIGHT))) + .snap_to_window_with_margin(px(8.0)) + .child(menu_popup), + ) + .with_priority(1), + ); - // A hovered item's submenu, anchored to the right of the popup. - if let Some(hovered) = self.hovered - && let Some(item) = self.entries[open_index].menu.items.get(hovered) - && let Some(submenu) = item.submenu.clone() - { - let sub_hovered = self.submenu.and_then(|_| None); - let sub_popup = menu_popup_element( - self.control + 1000, - &submenu, - sub_hovered, - "menu-submenu-popup", - &colors, - cx.listener(|this, clicked: &MenuClicked, _window, cx| { - this.trigger(clicked.id, cx); - }), - cx.listener(|_this, _item: &MenuHovered, _window, _cx| {}), - ); - let width = f32::from(menu_width_estimate()); - // Align the submenu with the hovered row (one row height per - // item) so it opens next to the item, not the first row. - let top = ROW_HEIGHT * (hovered as f32 + 1.0); - bar = bar.child( - deferred( - anchored() - .position(self.popup_position) - .anchor(Anchor::TopLeft) - .offset(point(px(width + 2.0), px(top))) - .snap_to_window_with_margin(px(8.0)) - .child(sub_popup), - ) - .with_priority(2), - ); - } + // A hovered item's submenu, anchored to the right of the popup. + if let Some(hovered) = self.hovered + && let Some(item) = self.entries[open_index].menu.items.get(hovered) + && let Some(submenu) = item.submenu.clone() + { + let sub_hovered = self.submenu.and_then(|_| None); + let sub_popup = menu_popup_element( + self.control + 1000, + &submenu, + sub_hovered, + "menu-submenu-popup", + &colors, + cx.listener(|this, clicked: &MenuClicked, _window, cx| { + this.trigger(clicked.id, cx); + }), + cx.listener(|_this, _item: &MenuHovered, _window, _cx| {}), + ); + let width = f32::from(menu_width_estimate()); + // Align the submenu with the hovered row (one row height per + // item) so it opens next to the item, not the first row. + let top = ROW_HEIGHT * (hovered as f32 + 1.0); + bar = bar.child( + deferred( + anchored() + .position(self.popup_position) + .anchor(Anchor::TopLeft) + .offset(point(px(width + 2.0), px(top))) + .snap_to_window_with_margin(px(8.0)) + .child(sub_popup), + ) + .with_priority(2), + ); + } - // Focus the popup so keyboard navigation works. - window.focus(&self.focus_handle, cx); - } + // Focus the popup so keyboard navigation works. + window.focus(&self.focus_handle, cx); + } - bar - } + bar + } } /// A right-click context menu. pub struct ContextMenu { - focus_handle: FocusHandle, - open: Option, + focus_handle: FocusHandle, + open: Option, } struct ContextMenuState { - position: Point, - menu: Menu, - hovered: Option, + position: Point, + menu: Menu, + hovered: Option, } impl ContextMenu { - /// Create a context menu (hidden until [`Self::show`]). - pub fn new(_control: usize, _window: &mut Window, cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - open: None, - } - } + /// Create a context menu (hidden until [`Self::show`]). + pub fn new(_control: usize, _window: &mut Window, cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + open: None, + } + } - /// Whether the menu is visible. - pub fn is_open(&self) -> bool { - self.open.is_some() - } + /// Whether the menu is visible. + pub fn is_open(&self) -> bool { + self.open.is_some() + } - /// Show the menu at `position` (window coordinates). - pub fn show(&mut self, position: Point, menu: Menu, cx: &mut Context) { - self.open = Some(ContextMenuState { - position, - menu, - hovered: None, - }); - cx.notify(); - } + /// Show the menu at `position` (window coordinates). + pub fn show(&mut self, position: Point, menu: Menu, cx: &mut Context) { + self.open = Some(ContextMenuState { + position, + menu, + hovered: None, + }); + cx.notify(); + } - /// Hide the menu. - pub fn hide(&mut self, cx: &mut Context) { - if self.open.take().is_some() { - cx.notify(); - } - } + /// Hide the menu. + pub fn hide(&mut self, cx: &mut Context) { + if self.open.take().is_some() { + cx.notify(); + } + } } impl EventEmitter for ContextMenu {} impl Focusable for ContextMenu { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for ContextMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let mut root = div(); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let mut root = div(); - if let Some(state) = self.open.take() { - let menu = state.menu.clone(); - let hovered = state.hovered; - let position = state.position; - let popup = menu_popup_element( - 0, - &menu, - hovered, - "menu-popup", - &colors, - cx.listener(|this, clicked: &MenuClicked, _window, cx| { - cx.emit(ContextMenuEvent { - item: clicked.id, - label: clicked.label.clone(), - }); - this.hide(cx); - }), - cx.listener(|_this, _item: &MenuHovered, _window, _cx| {}), - ) - .track_focus(&self.focus_handle) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event: &MouseUpEvent, _window, cx| { - this.hide(cx); - }), - ) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "up" | "down" => { - if let Some(state) = this.open.as_mut() { - let delta = if event.keystroke.key == "up" { -1 } else { 1 }; - if let Some(next) = state.menu.navigate(state.hovered, delta) { - state.hovered = Some(next); - cx.notify(); - } - } - } - "enter" | "space" => { - if let Some(state) = this.open.as_ref() - && let Some(hovered) = state.hovered - && let Some(item) = state.menu.items.get(hovered) - && item.enabled - { - cx.emit(ContextMenuEvent { - item: item.id, - label: item.label.clone(), - }); - this.hide(cx); - } - } - "escape" => this.hide(cx), - _ => {} - } - })); + if let Some(state) = self.open.take() { + let menu = state.menu.clone(); + let hovered = state.hovered; + let position = state.position; + let popup = menu_popup_element( + 0, + &menu, + hovered, + "menu-popup", + &colors, + cx.listener(|this, clicked: &MenuClicked, _window, cx| { + cx.emit(ContextMenuEvent { + item: clicked.id, + label: clicked.label.clone(), + }); + this.hide(cx); + }), + cx.listener(|_this, _item: &MenuHovered, _window, _cx| {}), + ) + .track_focus(&self.focus_handle) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event: &MouseUpEvent, _window, cx| { + this.hide(cx); + }), + ) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "up" | "down" => { + if let Some(state) = this.open.as_mut() { + let delta = if event.keystroke.key == "up" { -1 } else { 1 }; + if let Some(next) = state.menu.navigate(state.hovered, delta) { + state.hovered = Some(next); + cx.notify(); + } + } + } + "enter" | "space" => { + if let Some(state) = this.open.as_ref() + && let Some(hovered) = state.hovered + && let Some(item) = state.menu.items.get(hovered) + && item.enabled + { + cx.emit(ContextMenuEvent { + item: item.id, + label: item.label.clone(), + }); + this.hide(cx); + } + } + "escape" => this.hide(cx), + _ => {} + } + })); - root = root.child( - deferred(anchored().position(position).child(popup)).with_priority(1), - ); - window.focus(&self.focus_handle, cx); - self.open = Some(ContextMenuState { - position, - menu, - hovered, - }); - } + root = + root.child(deferred(anchored().position(position).child(popup)).with_priority(1)); + window.focus(&self.focus_handle, cx); + self.open = Some(ContextMenuState { + position, + menu, + hovered, + }); + } - root - } + root + } } /// Marker event types passed to the shared popup builder. struct MenuClicked { - id: usize, - label: SharedString, + id: usize, + label: SharedString, } struct MenuHovered { - index: usize, - submenu: bool, + index: usize, + submenu: bool, } /// Build a menu popup list. `on_click` receives the clicked item, `on_hover` /// receives hovered-item info (used to open submenus). `debug_key` is the /// test selector registered for the popup's bounds. fn menu_popup_element( - control: usize, - menu: &Menu, - hovered: Option, - debug_key: &'static str, - colors: &gpui::colors::Colors, - on_click: impl Fn(&MenuClicked, &mut Window, &mut App) + 'static, - on_hover: impl Fn(&MenuHovered, &mut Window, &mut App) + 'static, + control: usize, + menu: &Menu, + hovered: Option, + debug_key: &'static str, + colors: &gpui::colors::Colors, + on_click: impl Fn(&MenuClicked, &mut Window, &mut App) + 'static, + on_hover: impl Fn(&MenuHovered, &mut Window, &mut App) + 'static, ) -> gpui::Stateful { - use std::sync::Arc; - let on_click = Arc::new(on_click); - let on_hover = Arc::new(on_hover); - let mut column = div() - .id(ElementId::named_usize("gpui-widgets-menu-popup", control)) - .debug_selector(move || debug_key.into()) - .min_w(px(180.0)) - .rounded_md() - .border_1() - .border_color(colors.border) - .bg(colors.container) - .py_1() - .flex() - .flex_col(); + use std::sync::Arc; + let on_click = Arc::new(on_click); + let on_hover = Arc::new(on_hover); + let mut column = div() + .id(ElementId::named_usize("gpui-widgets-menu-popup", control)) + .debug_selector(move || debug_key.into()) + .min_w(px(180.0)) + .rounded_md() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .py_1() + .flex() + .flex_col(); - for (index, item) in menu.items.iter().enumerate() { - let id = item.id; - let label = item.label.clone(); - let shortcut = item.shortcut.clone(); - let checked = item.checked; - let enabled = item.enabled; - let has_submenu = item.submenu.is_some(); - let is_hovered = hovered == Some(index); - let is_separator = Menu::is_separator(item); + for (index, item) in menu.items.iter().enumerate() { + let id = item.id; + let label = item.label.clone(); + let shortcut = item.shortcut.clone(); + let checked = item.checked; + let enabled = item.enabled; + let has_submenu = item.submenu.is_some(); + let is_hovered = hovered == Some(index); + let is_separator = Menu::is_separator(item); - if is_separator { - column = column.child(div().h(px(1.0)).my_1().bg(colors.separator)); - continue; - } + if is_separator { + column = column.child(div().h(px(1.0)).my_1().bg(colors.separator)); + continue; + } - let row = div() - .id(ElementId::named_usize( - format!("gpui-widgets-menu-item-{control}"), - id, - )) - .px_2() - .h(px(ROW_HEIGHT)) - .flex() - .items_center() - .gap_2() - .bg(if is_hovered { colors.selected } else { transparent() }) - .text_color(if enabled { - colors.text - } else { - colors.disabled - }) - .cursor_pointer() - .child( - div() - .w(px(16.0)) - .child(if checked == Some(true) { "✓" } else { "" }), - ) - .child(div().flex_1().child(label.clone())) - .child(if has_submenu { "›" } else { "" }) - .child( - div() - .text_color(colors.disabled) - .child(shortcut.unwrap_or_default()), - ); + let row = div() + .id(ElementId::named_usize( + format!("gpui-widgets-menu-item-{control}"), + id, + )) + .px_2() + .h(px(ROW_HEIGHT)) + .flex() + .items_center() + .gap_2() + .bg(if is_hovered { + colors.selected + } else { + transparent() + }) + .text_color(if enabled { + colors.text + } else { + colors.disabled + }) + .cursor_pointer() + .child( + div() + .w(px(16.0)) + .child(if checked == Some(true) { "✓" } else { "" }), + ) + .child(div().flex_1().child(label.clone())) + .child(if has_submenu { "›" } else { "" }) + .child( + div() + .text_color(colors.disabled) + .child(shortcut.unwrap_or_default()), + ); - let row = if enabled { - let on_click = on_click.clone(); - let on_hover = on_hover.clone(); - row.on_click(move |_event: &ClickEvent, window, cx| { - on_click( - &MenuClicked { - id, - label: label.clone(), - }, - window, - cx, - ); - }) - .on_hover(move |hovered: &bool, window, cx| { - if *hovered { - on_hover( - &MenuHovered { - index, - submenu: has_submenu, - }, - window, - cx, - ); - } - }) - } else { - row - }; + let row = if enabled { + let on_click = on_click.clone(); + let on_hover = on_hover.clone(); + row.on_click(move |_event: &ClickEvent, window, cx| { + on_click( + &MenuClicked { + id, + label: label.clone(), + }, + window, + cx, + ); + }) + .on_hover(move |hovered: &bool, window, cx| { + if *hovered { + on_hover( + &MenuHovered { + index, + submenu: has_submenu, + }, + window, + cx, + ); + } + }) + } else { + row + }; - column = column.child(row); - } - column + column = column.child(row); + } + column } /// A rough menu width estimate for submenu placement (matches `min_w`). fn menu_width_estimate() -> Pixels { - px(180.0) + px(180.0) } /// Find the menu item at a raw index in the currently open menu. fn entry_at(bar: &MenuBar, index: usize) -> Option<&MenuItem> { - bar.entries - .get(bar.open?) - .and_then(|entry| entry.menu.items.get(index)) + bar.entries + .get(bar.open?) + .and_then(|entry| entry.menu.items.get(index)) } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; + use super::*; + use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; - #[test] - fn menu_bar_open_close_round_trip() { - // Pure state checks are in model tests; here we just ensure the - // model types are wired through the view API. - let menu = Menu::new(vec![MenuItem::new(1, "Save").with_shortcut("⌘S")]); - let entry = MenuBarEntry::new("File", menu); - assert_eq!(entry.title, "File"); - assert_eq!(entry.menu.items[0].id, 1); - } + #[test] + fn menu_bar_open_close_round_trip() { + // Pure state checks are in model tests; here we just ensure the + // model types are wired through the view API. + let menu = Menu::new(vec![MenuItem::new(1, "Save").with_shortcut("⌘S")]); + let entry = MenuBarEntry::new("File", menu); + assert_eq!(entry.title, "File"); + assert_eq!(entry.menu.items[0].id, 1); + } - #[test] - fn context_menu_event_carries_item() { - let event = ContextMenuEvent { - item: 7, - label: "Paste".into(), - }; - assert_eq!(event.item, 7); - assert_eq!(event.label, "Paste"); - } + #[test] + fn context_menu_event_carries_item() { + let event = ContextMenuEvent { + item: 7, + label: "Paste".into(), + }; + assert_eq!(event.item, 7); + assert_eq!(event.label, "Paste"); + } - // --- interaction tests for the menu views --- + // --- interaction tests for the menu views --- - fn demo_entries() -> Vec { - vec![MenuBarEntry::new( - "File", - Menu::new(vec![ - MenuItem::new(10, "Open…").with_shortcut("⌘O"), - MenuItem::new(11, "Save").with_shortcut("⌘S"), - MenuItem::new(12, "Quit").separated(), - ]), - )] - } + fn demo_entries() -> Vec { + vec![MenuBarEntry::new( + "File", + Menu::new(vec![ + MenuItem::new(10, "Open…").with_shortcut("⌘O"), + MenuItem::new(11, "Save").with_shortcut("⌘S"), + MenuItem::new(12, "Quit").separated(), + ]), + )] + } - /// An entry whose first item carries a nested submenu (like the app's - /// 视图 → 语言 / 主题). - fn demo_entries_with_submenu() -> Vec { - let sub = Menu::new(vec![ - MenuItem::new(21, "简体中文"), - MenuItem::new(22, "English"), - ]); - vec![MenuBarEntry::new( - "View", - Menu::new(vec![ - MenuItem::new(20, "Language").with_submenu(sub), - MenuItem::new(23, "Preferences…"), - ]), - )] - } + /// An entry whose first item carries a nested submenu (like the app's + /// 视图 → 语言 / 主题). + fn demo_entries_with_submenu() -> Vec { + let sub = Menu::new(vec![ + MenuItem::new(21, "简体中文"), + MenuItem::new(22, "English"), + ]); + vec![MenuBarEntry::new( + "View", + Menu::new(vec![ + MenuItem::new(20, "Language").with_submenu(sub), + MenuItem::new(23, "Preferences…"), + ]), + )] + } - struct Host { - menu_bar: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.menu_bar.clone()) - } - } + struct Host { + menu_bar: Entity, + events: Vec, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.menu_bar.clone()) + } + } - fn make_bar(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { - make_bar_with(cx, demo_entries()) - } + fn make_bar(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { + make_bar_with(cx, demo_entries()) + } - fn make_bar_with( - cx: &mut TestAppContext, - entries: Vec, - ) -> (&'static mut VisualTestContext, Entity) { - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(400.0), px(120.0)), |window, cx| { - let menu_bar = cx.new(|cx| MenuBar::new(1, entries, window, cx)); - let host = Host { - menu_bar, - events: Vec::new(), - }; - cx.subscribe( - &host.menu_bar, - |host: &mut Host, - _m: Entity, - event: &MenuBarEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - (cx, host) - } + fn make_bar_with( + cx: &mut TestAppContext, + entries: Vec, + ) -> (&'static mut VisualTestContext, Entity) { + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(400.0), px(120.0)), |window, cx| { + let menu_bar = cx.new(|cx| MenuBar::new(1, entries, window, cx)); + let host = Host { + menu_bar, + events: Vec::new(), + }; + cx.subscribe( + &host.menu_bar, + |host: &mut Host, + _m: Entity, + event: &MenuBarEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + (cx, host) + } - #[gpui::test] - async fn clicking_a_menu_title_opens_the_popup(cx: &mut TestAppContext) { - let (cx, _host) = make_bar(cx); - // Click the "File" title (top-left of the bar). - cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn clicking_a_menu_title_opens_the_popup(cx: &mut TestAppContext) { + let (cx, _host) = make_bar(cx); + // Click the "File" title (top-left of the bar). + cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered"); - assert!(popup.size.height > px(60.0), "popup should list the items"); - } + let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered"); + assert!(popup.size.height > px(60.0), "popup should list the items"); + } - #[gpui::test] - async fn clicking_a_menu_item_emits_triggered(cx: &mut TestAppContext) { - let (cx, host) = make_bar(cx); - cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn clicking_a_menu_item_emits_triggered(cx: &mut TestAppContext) { + let (cx, host) = make_bar(cx); + cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered"); - // The first item row is the first ~26px of the popup. - cx.simulate_click( - point(popup.left() + px(40.0), popup.top() + px(16.0)), - Modifiers::none(), - ); - cx.run_until_parked(); + let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered"); + // The first item row is the first ~26px of the popup. + cx.simulate_click( + point(popup.left() + px(40.0), popup.top() + px(16.0)), + Modifiers::none(), + ); + cx.run_until_parked(); - let triggered = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, MenuBarEvent::Triggered { item: 10, .. }) - }) - }); - assert!(triggered, "expected Triggered for the first item"); - } + let triggered = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, MenuBarEvent::Triggered { item: 10, .. })) + }); + assert!(triggered, "expected Triggered for the first item"); + } - #[gpui::test] - async fn keyboard_navigation_triggers_the_hovered_item(cx: &mut TestAppContext) { - let (cx, host) = make_bar(cx); - cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn keyboard_navigation_triggers_the_hovered_item(cx: &mut TestAppContext) { + let (cx, host) = make_bar(cx); + cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - // Down from no selection lands on the first item; the second down - // moves to Save. Enter triggers it. - cx.simulate_keystrokes("down"); - cx.run_until_parked(); - cx.simulate_keystrokes("down"); - cx.run_until_parked(); - cx.simulate_keystrokes("enter"); - cx.run_until_parked(); + // Down from no selection lands on the first item; the second down + // moves to Save. Enter triggers it. + cx.simulate_keystrokes("down"); + cx.run_until_parked(); + cx.simulate_keystrokes("down"); + cx.run_until_parked(); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); - let triggered = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, MenuBarEvent::Triggered { item: 11, .. }) - }) - }); - assert!(triggered, "expected Triggered for the second item via keyboard"); - } + let triggered = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, MenuBarEvent::Triggered { item: 11, .. })) + }); + assert!( + triggered, + "expected Triggered for the second item via keyboard" + ); + } - #[gpui::test] - async fn escape_closes_the_menu(cx: &mut TestAppContext) { - let (cx, host) = make_bar(cx); - cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); - assert!(cx.debug_bounds("menu-popup").is_some()); + #[gpui::test] + async fn escape_closes_the_menu(cx: &mut TestAppContext) { + let (cx, host) = make_bar(cx); + cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); + assert!(cx.debug_bounds("menu-popup").is_some()); - cx.simulate_keystrokes("escape"); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - assert!(cx.debug_bounds("menu-popup").is_none(), "menu should close on escape"); - let closed = cx.read(|app| { - host.read(app).events.iter().any(|e| matches!(e, MenuBarEvent::MenuClosed { .. })) - }); - assert!(closed); - } + assert!( + cx.debug_bounds("menu-popup").is_none(), + "menu should close on escape" + ); + let closed = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, MenuBarEvent::MenuClosed { .. })) + }); + assert!(closed); + } - #[gpui::test] - async fn runtime_set_item_checked_flips_the_checkmark(cx: &mut TestAppContext) { - let (cx, host) = make_bar(cx); - // Toggle item 11 ("Save") at runtime, by id. - let changed = cx.update(|_window, app| { - let bar = host.read(app).menu_bar.clone(); - bar.update(app, |bar, _cx| bar.set_item_checked(11, true)) - }); - assert!(changed, "item 11 exists and should be updated"); - let checked = cx.update(|_window, app| { - host.read(app).menu_bar.read(app).entries[0].menu.items[1].checked - }); - assert_eq!(checked, Some(true)); + #[gpui::test] + async fn runtime_set_item_checked_flips_the_checkmark(cx: &mut TestAppContext) { + let (cx, host) = make_bar(cx); + // Toggle item 11 ("Save") at runtime, by id. + let changed = cx.update(|_window, app| { + let bar = host.read(app).menu_bar.clone(); + bar.update(app, |bar, _cx| bar.set_item_checked(11, true)) + }); + assert!(changed, "item 11 exists and should be updated"); + let checked = cx.update(|_window, app| { + host.read(app).menu_bar.read(app).entries[0].menu.items[1].checked + }); + assert_eq!(checked, Some(true)); - // Unknown ids are reported as not found and change nothing. - let changed = cx.update(|_window, app| { - let bar = host.read(app).menu_bar.clone(); - bar.update(app, |bar, _cx| bar.set_item_checked(12345, true)) - }); - assert!(!changed); - let checked = cx.update(|_window, app| { - host.read(app).menu_bar.read(app).entries[0].menu.items[0].checked - }); - assert_eq!(checked, None); - } + // Unknown ids are reported as not found and change nothing. + let changed = cx.update(|_window, app| { + let bar = host.read(app).menu_bar.clone(); + bar.update(app, |bar, _cx| bar.set_item_checked(12345, true)) + }); + assert!(!changed); + let checked = cx.update(|_window, app| { + host.read(app).menu_bar.read(app).entries[0].menu.items[0].checked + }); + assert_eq!(checked, None); + } - /// Hovering a menu item that carries a nested menu must open the second - /// level to the right of the popup (the app's 视图 → 语言 / 主题 flow). - #[gpui::test] - async fn hovering_a_submenu_item_opens_the_submenu(cx: &mut TestAppContext) { - let (cx, host) = make_bar_with(cx, demo_entries_with_submenu()); - // Click the "View" title to open the menu. - cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + /// Hovering a menu item that carries a nested menu must open the second + /// level to the right of the popup (the app's 视图 → 语言 / 主题 flow). + #[gpui::test] + async fn hovering_a_submenu_item_opens_the_submenu(cx: &mut TestAppContext) { + let (cx, host) = make_bar_with(cx, demo_entries_with_submenu()); + // Click the "View" title to open the menu. + cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let popup = cx - .debug_bounds("menu-popup") - .expect("menu popup rendered"); - // Hover the first row ("Language", which has the submenu). - cx.simulate_mouse_move( - point(popup.left() + px(40.0), popup.top() + px(13.0)), - None, - Modifiers::none(), - ); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered"); + // Hover the first row ("Language", which has the submenu). + cx.simulate_mouse_move( + point(popup.left() + px(40.0), popup.top() + px(13.0)), + None, + Modifiers::none(), + ); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let submenu = cx - .debug_bounds("menu-submenu-popup") - .expect("submenu popup opens next to the hovered item"); - assert!( - submenu.left() > popup.left(), - "the submenu opens to the right of the parent popup" - ); + let submenu = cx + .debug_bounds("menu-submenu-popup") + .expect("submenu popup opens next to the hovered item"); + assert!( + submenu.left() > popup.left(), + "the submenu opens to the right of the parent popup" + ); - // Clicking a submenu row triggers it (and closes the menus). - cx.simulate_click( - point(submenu.left() + px(40.0), submenu.top() + px(13.0)), - Modifiers::none(), - ); - cx.run_until_parked(); - let triggered = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, MenuBarEvent::Triggered { item: 21, .. }) - }) - }); - assert!(triggered, "expected Triggered for the submenu item"); - assert!(cx.debug_bounds("menu-popup").is_none(), "menu closed after triggering"); - } + // Clicking a submenu row triggers it (and closes the menus). + cx.simulate_click( + point(submenu.left() + px(40.0), submenu.top() + px(13.0)), + Modifiers::none(), + ); + cx.run_until_parked(); + let triggered = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, MenuBarEvent::Triggered { item: 21, .. })) + }); + assert!(triggered, "expected Triggered for the submenu item"); + assert!( + cx.debug_bounds("menu-popup").is_none(), + "menu closed after triggering" + ); + } } diff --git a/crates/gpui_widgets/src/menu/model.rs b/crates/gpui_widgets/src/menu/model.rs index 52abfbd138..f9a99d04a6 100644 --- a/crates/gpui_widgets/src/menu/model.rs +++ b/crates/gpui_widgets/src/menu/model.rs @@ -6,240 +6,243 @@ use gpui::SharedString; /// A menu item. #[derive(Debug, Clone, PartialEq)] pub struct MenuItem { - /// The item's stable id (used in [`MenuEvent`](super::MenuEvent)). - pub id: usize, - /// The label shown in the menu. - pub label: SharedString, - /// A shortcut to display on the right (e.g. `"⌘S"`). - pub shortcut: Option, - /// Whether the item can be activated. - pub enabled: bool, - /// `None` = no checkmark; `Some(checked)` = a check/tick state. - pub checked: Option, - /// A nested submenu, opened on hover/click. - pub submenu: Option>, - /// Whether a separator line follows this item. - pub separator_after: bool, + /// The item's stable id (used in [`MenuEvent`](super::MenuEvent)). + pub id: usize, + /// The label shown in the menu. + pub label: SharedString, + /// A shortcut to display on the right (e.g. `"⌘S"`). + pub shortcut: Option, + /// Whether the item can be activated. + pub enabled: bool, + /// `None` = no checkmark; `Some(checked)` = a check/tick state. + pub checked: Option, + /// A nested submenu, opened on hover/click. + pub submenu: Option>, + /// Whether a separator line follows this item. + pub separator_after: bool, } impl MenuItem { - /// Create a plain enabled item. - pub fn new(id: usize, label: impl Into) -> Self { - Self { - id, - label: label.into(), - shortcut: None, - enabled: true, - checked: None, - submenu: None, - separator_after: false, - } - } + /// Create a plain enabled item. + pub fn new(id: usize, label: impl Into) -> Self { + Self { + id, + label: label.into(), + shortcut: None, + enabled: true, + checked: None, + submenu: None, + separator_after: false, + } + } - /// Mark the item disabled. - pub fn disabled(mut self) -> Self { - self.enabled = false; - self - } + /// Mark the item disabled. + pub fn disabled(mut self) -> Self { + self.enabled = false; + self + } - /// Attach a shortcut label. - pub fn with_shortcut(mut self, shortcut: impl Into) -> Self { - self.shortcut = Some(shortcut.into()); - self - } + /// Attach a shortcut label. + pub fn with_shortcut(mut self, shortcut: impl Into) -> Self { + self.shortcut = Some(shortcut.into()); + self + } - /// Set a checked state. - pub fn with_checked(mut self, checked: bool) -> Self { - self.checked = Some(checked); - self - } + /// Set a checked state. + pub fn with_checked(mut self, checked: bool) -> Self { + self.checked = Some(checked); + self + } - /// Set the checked state of an already-built item (runtime mutation). - /// - /// Unlike the construction-only [`with_checked`](Self::with_checked), this - /// lets a host flip a menu checkmark after the [`Menu`] has been handed - /// to a view — e.g. through [`Menu::set_item_checked`] on the menu held - /// by a [`MenuBar`](super::MenuBar) — without rebuilding the menu. The - /// renderers read `checked` on every frame, so the change shows up on the - /// next repaint. - pub fn set_checked(&mut self, checked: bool) -> &mut Self { - self.checked = Some(checked); - self - } + /// Set the checked state of an already-built item (runtime mutation). + /// + /// Unlike the construction-only [`with_checked`](Self::with_checked), this + /// lets a host flip a menu checkmark after the [`Menu`] has been handed + /// to a view — e.g. through [`Menu::set_item_checked`] on the menu held + /// by a [`MenuBar`](super::MenuBar) — without rebuilding the menu. The + /// renderers read `checked` on every frame, so the change shows up on the + /// next repaint. + pub fn set_checked(&mut self, checked: bool) -> &mut Self { + self.checked = Some(checked); + self + } - /// Remove the checkmark from an already-built item (runtime mutation). - pub fn clear_checked(&mut self) -> &mut Self { - self.checked = None; - self - } + /// Remove the checkmark from an already-built item (runtime mutation). + pub fn clear_checked(&mut self) -> &mut Self { + self.checked = None; + self + } - /// Attach a submenu. - pub fn with_submenu(mut self, submenu: Menu) -> Self { - self.submenu = Some(Box::new(submenu)); - self - } + /// Attach a submenu. + pub fn with_submenu(mut self, submenu: Menu) -> Self { + self.submenu = Some(Box::new(submenu)); + self + } - /// Draw a separator line after this item. - pub fn separated(mut self) -> Self { - self.separator_after = true; - self - } + /// Draw a separator line after this item. + pub fn separated(mut self) -> Self { + self.separator_after = true; + self + } } /// A menu: an ordered list of items. #[derive(Debug, Clone, PartialEq, Default)] pub struct Menu { - /// The items of this menu. - pub items: Vec, + /// The items of this menu. + pub items: Vec, } impl Menu { - /// Create a menu. - pub fn new(items: Vec) -> Self { - Self { items } - } + /// Create a menu. + pub fn new(items: Vec) -> Self { + Self { items } + } - /// Whether the item at `index` is a visual separator (an empty item). - pub fn is_separator(item: &MenuItem) -> bool { - item.label.is_empty() - } + /// Whether the item at `index` is a visual separator (an empty item). + pub fn is_separator(item: &MenuItem) -> bool { + item.label.is_empty() + } - /// Sets the checked state of the item with `id`, searching top-level - /// items and their submenus recursively. Returns whether an item with - /// that id was found and updated. - /// - /// Runtime counterpart to [`MenuItem::with_checked`]: hosts that hold a - /// live [`Menu`] (e.g. in a [`MenuBar`](super::MenuBar)) can toggle a - /// checkmark without rebuilding the menu. - pub fn set_item_checked(&mut self, id: usize, checked: bool) -> bool { - for item in &mut self.items { - if item.id == id { - item.set_checked(checked); - return true; - } - if let Some(submenu) = item.submenu.as_mut() { - if submenu.set_item_checked(id, checked) { - return true; - } - } - } - false - } + /// Sets the checked state of the item with `id`, searching top-level + /// items and their submenus recursively. Returns whether an item with + /// that id was found and updated. + /// + /// Runtime counterpart to [`MenuItem::with_checked`]: hosts that hold a + /// live [`Menu`] (e.g. in a [`MenuBar`](super::MenuBar)) can toggle a + /// checkmark without rebuilding the menu. + pub fn set_item_checked(&mut self, id: usize, checked: bool) -> bool { + for item in &mut self.items { + if item.id == id { + item.set_checked(checked); + return true; + } + if let Some(submenu) = item.submenu.as_mut() { + if submenu.set_item_checked(id, checked) { + return true; + } + } + } + false + } - /// The next selectable index from `current`, moving `delta` steps - /// (skipping separators and disabled items). `None` returns the first - /// (or last) selectable item. Returns `None` if nothing is selectable. - pub fn navigate(&self, current: Option, delta: i32) -> Option { - let selectable: Vec = self - .items - .iter() - .enumerate() - .filter(|(_, item)| item.enabled && !Self::is_separator(item)) - .map(|(index, _)| index) - .collect(); - if selectable.is_empty() { - return None; - } - let position = current.and_then(|c| selectable.iter().position(|&i| i == c)); - let next = match position { - Some(pos) => { - ((pos as i64 + delta as i64).rem_euclid(selectable.len() as i64)) as usize - } - None if delta > 0 => 0, - None => selectable.len() - 1, - }; - Some(selectable[next]) - } + /// The next selectable index from `current`, moving `delta` steps + /// (skipping separators and disabled items). `None` returns the first + /// (or last) selectable item. Returns `None` if nothing is selectable. + pub fn navigate(&self, current: Option, delta: i32) -> Option { + let selectable: Vec = self + .items + .iter() + .enumerate() + .filter(|(_, item)| item.enabled && !Self::is_separator(item)) + .map(|(index, _)| index) + .collect(); + if selectable.is_empty() { + return None; + } + let position = current.and_then(|c| selectable.iter().position(|&i| i == c)); + let next = match position { + Some(pos) => ((pos as i64 + delta as i64).rem_euclid(selectable.len() as i64)) as usize, + None if delta > 0 => 0, + None => selectable.len() - 1, + }; + Some(selectable[next]) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn sample_menu() -> Menu { - Menu::new(vec![ - MenuItem::new(1, "Open…").with_shortcut("⌘O"), - MenuItem::new(2, "Save").with_shortcut("⌘S"), - MenuItem::new(3, "Save As…").with_shortcut("⇧⌘S").separated(), - MenuItem::new(4, "Export").disabled(), - MenuItem::new(5, "Export Again").with_checked(true), - ]) - } + fn sample_menu() -> Menu { + Menu::new(vec![ + MenuItem::new(1, "Open…").with_shortcut("⌘O"), + MenuItem::new(2, "Save").with_shortcut("⌘S"), + MenuItem::new(3, "Save As…") + .with_shortcut("⇧⌘S") + .separated(), + MenuItem::new(4, "Export").disabled(), + MenuItem::new(5, "Export Again").with_checked(true), + ]) + } - #[test] - fn navigate_skips_disabled_and_separators() { - let menu = sample_menu(); - // From none, down lands on the first selectable (Open). - assert_eq!(menu.navigate(None, 1), Some(0)); - // From none, up lands on the last selectable (Export Again, idx 4). - assert_eq!(menu.navigate(None, -1), Some(4)); - // From Open (0), down skips nothing until Save (1). - assert_eq!(menu.navigate(Some(0), 1), Some(1)); - // From Save As (2), down skips the disabled Export (3) to Export Again (4). - assert_eq!(menu.navigate(Some(2), 1), Some(4)); - // Wrap around. - assert_eq!(menu.navigate(Some(4), 1), Some(0)); - assert_eq!(menu.navigate(Some(0), -1), Some(4)); - } + #[test] + fn navigate_skips_disabled_and_separators() { + let menu = sample_menu(); + // From none, down lands on the first selectable (Open). + assert_eq!(menu.navigate(None, 1), Some(0)); + // From none, up lands on the last selectable (Export Again, idx 4). + assert_eq!(menu.navigate(None, -1), Some(4)); + // From Open (0), down skips nothing until Save (1). + assert_eq!(menu.navigate(Some(0), 1), Some(1)); + // From Save As (2), down skips the disabled Export (3) to Export Again (4). + assert_eq!(menu.navigate(Some(2), 1), Some(4)); + // Wrap around. + assert_eq!(menu.navigate(Some(4), 1), Some(0)); + assert_eq!(menu.navigate(Some(0), -1), Some(4)); + } - #[test] - fn navigate_returns_none_when_nothing_selectable() { - let menu = Menu::new(vec![ - MenuItem::new(1, "Only").disabled(), - MenuItem::new(2, ""), - ]); - assert_eq!(menu.navigate(None, 1), None); - assert_eq!(menu.navigate(Some(1), 1), None); - } + #[test] + fn navigate_returns_none_when_nothing_selectable() { + let menu = Menu::new(vec![ + MenuItem::new(1, "Only").disabled(), + MenuItem::new(2, ""), + ]); + assert_eq!(menu.navigate(None, 1), None); + assert_eq!(menu.navigate(Some(1), 1), None); + } - #[test] - fn checked_state_is_optional() { - let menu = sample_menu(); - assert_eq!(menu.items[0].checked, None); - assert_eq!(menu.items[4].checked, Some(true)); - } + #[test] + fn checked_state_is_optional() { + let menu = sample_menu(); + assert_eq!(menu.items[0].checked, None); + assert_eq!(menu.items[4].checked, Some(true)); + } - #[test] - fn set_item_checked_mutates_in_place() { - let mut menu = sample_menu(); - // Runtime toggle on a built item, found by id. - assert!(menu.set_item_checked(5, false)); - assert_eq!(menu.items[4].checked, Some(false)); - assert!(menu.set_item_checked(5, true)); - assert_eq!(menu.items[4].checked, Some(true)); - // Clear the checkmark entirely. - assert!(menu.items[4].clear_checked().checked.is_none()); - // Unknown ids report failure and change nothing. - assert!(!menu.set_item_checked(999, true)); - assert_eq!(menu.items[0].checked, None); - } + #[test] + fn set_item_checked_mutates_in_place() { + let mut menu = sample_menu(); + // Runtime toggle on a built item, found by id. + assert!(menu.set_item_checked(5, false)); + assert_eq!(menu.items[4].checked, Some(false)); + assert!(menu.set_item_checked(5, true)); + assert_eq!(menu.items[4].checked, Some(true)); + // Clear the checkmark entirely. + assert!(menu.items[4].clear_checked().checked.is_none()); + // Unknown ids report failure and change nothing. + assert!(!menu.set_item_checked(999, true)); + assert_eq!(menu.items[0].checked, None); + } - #[test] - fn set_item_checked_reaches_nested_submenus() { - let sub = Menu::new(vec![MenuItem::new(10, "A"), MenuItem::new(11, "B")]); - let mut menu = Menu::new(vec![ - MenuItem::new(5, "Nested").with_submenu(sub), - MenuItem::new(6, "Top"), - ]); - assert!(menu.set_item_checked(11, true)); - assert_eq!(menu.items[0].submenu.as_ref().unwrap().items[1].checked, Some(true)); - // The top-level item is untouched. - assert_eq!(menu.items[1].checked, None); - } + #[test] + fn set_item_checked_reaches_nested_submenus() { + let sub = Menu::new(vec![MenuItem::new(10, "A"), MenuItem::new(11, "B")]); + let mut menu = Menu::new(vec![ + MenuItem::new(5, "Nested").with_submenu(sub), + MenuItem::new(6, "Top"), + ]); + assert!(menu.set_item_checked(11, true)); + assert_eq!( + menu.items[0].submenu.as_ref().unwrap().items[1].checked, + Some(true) + ); + // The top-level item is untouched. + assert_eq!(menu.items[1].checked, None); + } - #[test] - fn cascade_nesting() { - let sub = Menu::new(vec![MenuItem::new(10, "A"), MenuItem::new(11, "B")]); - let item = MenuItem::new(5, "Nested").with_submenu(sub.clone()); - assert_eq!(item.submenu.as_ref().unwrap().items.len(), 2); - assert_eq!(item.submenu.as_ref().unwrap().items[1].id, 11); - // The nested menu itself navigates independently. - assert_eq!(item.submenu.as_ref().unwrap().navigate(None, -1), Some(1)); - } + #[test] + fn cascade_nesting() { + let sub = Menu::new(vec![MenuItem::new(10, "A"), MenuItem::new(11, "B")]); + let item = MenuItem::new(5, "Nested").with_submenu(sub.clone()); + assert_eq!(item.submenu.as_ref().unwrap().items.len(), 2); + assert_eq!(item.submenu.as_ref().unwrap().items[1].id, 11); + // The nested menu itself navigates independently. + assert_eq!(item.submenu.as_ref().unwrap().navigate(None, -1), Some(1)); + } - #[test] - fn separator_detection() { - assert!(Menu::is_separator(&MenuItem::new(0, ""))); - assert!(!Menu::is_separator(&MenuItem::new(0, "Save"))); - } + #[test] + fn separator_detection() { + assert!(Menu::is_separator(&MenuItem::new(0, ""))); + assert!(!Menu::is_separator(&MenuItem::new(0, "Save"))); + } } diff --git a/crates/gpui_widgets/src/project_explorer.rs b/crates/gpui_widgets/src/project_explorer.rs index aba4ca26ee..bf27fb1c92 100644 --- a/crates/gpui_widgets/src/project_explorer.rs +++ b/crates/gpui_widgets/src/project_explorer.rs @@ -8,9 +8,8 @@ //! placeholder swatch. use gpui::{ - App, ClickEvent, Context, ElementId, Entity, EventEmitter, ExternalPaths, FocusHandle, - Focusable, Hsla, Render, SharedString, Window, colors::DefaultColors, div, img, prelude::*, - px, + App, ClickEvent, Context, ElementId, Entity, EventEmitter, ExternalPaths, FocusHandle, + Focusable, Hsla, Render, SharedString, Window, colors::DefaultColors, div, img, prelude::*, px, }; use std::collections::HashSet; use std::path::PathBuf; @@ -18,590 +17,611 @@ use std::sync::Arc; /// A fully transparent color (for un-hovered rows). fn transparent() -> gpui::Rgba { - gpui::Rgba { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - } + gpui::Rgba { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + } } /// A single entry in the project tree. #[derive(Debug, Clone, PartialEq)] pub struct ProjectEntry { - /// The entry's stable id (host-side key). - pub id: u64, - /// The display name. - pub name: SharedString, - /// Whether the entry is a folder (expandable). - pub is_dir: bool, - /// An asset path for a thumbnail, if any. - pub thumbnail: Option, + /// The entry's stable id (host-side key). + pub id: u64, + /// The display name. + pub name: SharedString, + /// Whether the entry is a folder (expandable). + pub is_dir: bool, + /// An asset path for a thumbnail, if any. + pub thumbnail: Option, } impl ProjectEntry { - /// Create an entry. - pub fn new(id: u64, name: impl Into, is_dir: bool) -> Self { - Self { - id, - name: name.into(), - is_dir, - thumbnail: None, - } - } + /// Create an entry. + pub fn new(id: u64, name: impl Into, is_dir: bool) -> Self { + Self { + id, + name: name.into(), + is_dir, + thumbnail: None, + } + } - /// Attach a thumbnail asset path. - pub fn with_thumbnail(mut self, thumbnail: impl Into) -> Self { - self.thumbnail = Some(thumbnail.into()); - self - } + /// Attach a thumbnail asset path. + pub fn with_thumbnail(mut self, thumbnail: impl Into) -> Self { + self.thumbnail = Some(thumbnail.into()); + self + } } /// The host's project model, read through this trait. pub trait ProjectDataSource: 'static { - /// The top-level entries. - fn roots(&self) -> Vec; - /// The children of a folder entry. - fn children(&self, parent_id: u64) -> Vec; + /// The top-level entries. + fn roots(&self) -> Vec; + /// The children of a folder entry. + fn children(&self, parent_id: u64) -> Vec; } /// The material bin's display mode. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExplorerView { - /// A hierarchical tree. - Tree, - /// A flat grid of thumbnails. - Icons, + /// A hierarchical tree. + Tree, + /// A flat grid of thumbnails. + Icons, } /// A request emitted by the explorer. #[derive(Debug, Clone, PartialEq)] pub enum ProjectExplorerEvent { - /// Open an entry (double-click on a file, or an explicit open request). - OpenRequested { - /// The explorer's stable id. - control: usize, - /// The entry's id. - id: u64, - /// The entry's name. - name: SharedString, - }, - /// Files were dropped onto the bin. - FileDropRequested { - /// The explorer's stable id. - control: usize, - /// The dropped paths. - paths: Vec, - }, - /// The view mode changed. - ViewChanged { - /// The explorer's stable id. - control: usize, - /// The new view mode. - view: ExplorerView, - }, + /// Open an entry (double-click on a file, or an explicit open request). + OpenRequested { + /// The explorer's stable id. + control: usize, + /// The entry's id. + id: u64, + /// The entry's name. + name: SharedString, + }, + /// Files were dropped onto the bin. + FileDropRequested { + /// The explorer's stable id. + control: usize, + /// The dropped paths. + paths: Vec, + }, + /// The view mode changed. + ViewChanged { + /// The explorer's stable id. + control: usize, + /// The new view mode. + view: ExplorerView, + }, } /// Flatten a tree into visible rows, honoring the `expanded` set. /// /// Pure and unit-tested; `children` resolves a folder's entries. pub fn flatten_tree( - roots: &[ProjectEntry], - expanded: &HashSet, - children: F, + roots: &[ProjectEntry], + expanded: &HashSet, + children: F, ) -> Vec<(ProjectEntry, usize)> where - F: Fn(u64) -> Vec, + F: Fn(u64) -> Vec, { - fn walk(entries: &[ProjectEntry], depth: usize, expanded: &HashSet, children: &F, out: &mut Vec<(ProjectEntry, usize)>) - where - F: Fn(u64) -> Vec, - { - for entry in entries { - out.push((entry.clone(), depth)); - if entry.is_dir && expanded.contains(&entry.id) { - let kids = children(entry.id); - walk(&kids, depth + 1, expanded, children, out); - } - } - } + fn walk( + entries: &[ProjectEntry], + depth: usize, + expanded: &HashSet, + children: &F, + out: &mut Vec<(ProjectEntry, usize)>, + ) where + F: Fn(u64) -> Vec, + { + for entry in entries { + out.push((entry.clone(), depth)); + if entry.is_dir && expanded.contains(&entry.id) { + let kids = children(entry.id); + walk(&kids, depth + 1, expanded, children, out); + } + } + } - let mut out = Vec::new(); - walk(roots, 0, expanded, &children, &mut out); - out + let mut out = Vec::new(); + walk(roots, 0, expanded, &children, &mut out); + out } /// A material bin with tree and icon views. pub struct ProjectExplorer { - control: usize, - data: Entity, - expanded: HashSet, - selected: Option, - view: ExplorerView, - focus_handle: FocusHandle, + control: usize, + data: Entity, + expanded: HashSet, + selected: Option, + view: ExplorerView, + focus_handle: FocusHandle, } impl ProjectExplorer { - /// Create an explorer over `data`. - pub fn new( - control: usize, - data: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - data, - expanded: HashSet::new(), - selected: None, - view: ExplorerView::Tree, - focus_handle: cx.focus_handle(), - } - } + /// Create an explorer over `data`. + pub fn new( + control: usize, + data: Entity, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + data, + expanded: HashSet::new(), + selected: None, + view: ExplorerView::Tree, + focus_handle: cx.focus_handle(), + } + } - /// The current view mode. - pub fn view(&self) -> ExplorerView { - self.view - } + /// The current view mode. + pub fn view(&self) -> ExplorerView { + self.view + } - /// Set the view mode from the host. - pub fn set_view(&mut self, view: ExplorerView, cx: &mut Context) { - if self.view != view { - self.view = view; - cx.notify(); - } - } + /// Set the view mode from the host. + pub fn set_view(&mut self, view: ExplorerView, cx: &mut Context) { + if self.view != view { + self.view = view; + cx.notify(); + } + } - fn toggle(&mut self, id: u64, cx: &mut Context) { - if !self.expanded.remove(&id) { - self.expanded.insert(id); - } - cx.notify(); - } + fn toggle(&mut self, id: u64, cx: &mut Context) { + if !self.expanded.remove(&id) { + self.expanded.insert(id); + } + cx.notify(); + } - fn open(&mut self, entry: &ProjectEntry, cx: &mut Context) { - cx.emit(ProjectExplorerEvent::OpenRequested { - control: self.control, - id: entry.id, - name: entry.name.clone(), - }); - cx.notify(); - } + fn open(&mut self, entry: &ProjectEntry, cx: &mut Context) { + cx.emit(ProjectExplorerEvent::OpenRequested { + control: self.control, + id: entry.id, + name: entry.name.clone(), + }); + cx.notify(); + } - fn set_view_mode(&mut self, view: ExplorerView, cx: &mut Context) { - if self.view != view { - self.view = view; - cx.emit(ProjectExplorerEvent::ViewChanged { - control: self.control, - view, - }); - cx.notify(); - } - } + fn set_view_mode(&mut self, view: ExplorerView, cx: &mut Context) { + if self.view != view { + self.view = view; + cx.emit(ProjectExplorerEvent::ViewChanged { + control: self.control, + view, + }); + cx.notify(); + } + } } impl EventEmitter for ProjectExplorer {} impl Focusable for ProjectExplorer { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for ProjectExplorer { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let roots = self.data.read(cx).roots(); - let selected = self.selected; - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let roots = self.data.read(cx).roots(); + let selected = self.selected; + let control = self.control; - // Toolbar: view toggle. - let toolbar = div() - .flex() - .items_center() - .gap_1() - .px_2() - .py_1() - .bg(colors.container) - .child(toggle_button( - "gpui-widgets-explorer-tree", - "树", - self.view == ExplorerView::Tree, - &colors, - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.set_view_mode(ExplorerView::Tree, cx); - }), - )) - .child(toggle_button( - "gpui-widgets-explorer-icons", - "图标", - self.view == ExplorerView::Icons, - &colors, - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.set_view_mode(ExplorerView::Icons, cx); - }), - )); + // Toolbar: view toggle. + let toolbar = div() + .flex() + .items_center() + .gap_1() + .px_2() + .py_1() + .bg(colors.container) + .child(toggle_button( + "gpui-widgets-explorer-tree", + "树", + self.view == ExplorerView::Tree, + &colors, + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.set_view_mode(ExplorerView::Tree, cx); + }), + )) + .child(toggle_button( + "gpui-widgets-explorer-icons", + "图标", + self.view == ExplorerView::Icons, + &colors, + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.set_view_mode(ExplorerView::Icons, cx); + }), + )); - let content = match self.view { - ExplorerView::Tree => { - let expanded = self.expanded.clone(); - let rows = flatten_tree(&roots, &expanded, |id| self.data.read(cx).children(id)); - let mut column = div() - .id(ElementId::named_usize("gpui-widgets-explorer-tree", control)) - .flex() - .flex_col() - .py_1() - .overflow_y_scroll(); - for (entry, depth) in rows { - let is_selected = selected == Some(entry.id); - let click_entry = entry.clone(); - let entry_id = entry.id; - let mut row = div() - .id(ElementId::named_usize("gpui-widgets-explorer-entry", entry.id as usize)) - .debug_selector(move || { - format!("gpui-widgets-explorer-entry-{entry_id}").into() - }) - .h(px(24.0)) - .flex() - .items_center() - .gap_1() - .pl(px(8.0 + depth as f32 * 14.0)) - .pr_2() - .cursor_pointer() - .bg(if is_selected { colors.selected } else { transparent() }) - .text_color(if is_selected { - colors.selected_text - } else { - colors.text - }) - .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { - this.selected = Some(click_entry.id); - if click_entry.is_dir { - if event.click_count() >= 2 { - this.open(&click_entry, cx); - } else { - this.toggle(click_entry.id, cx); - } - } else if event.click_count() >= 2 { - this.open(&click_entry, cx); - } - cx.notify(); - })) - .child( - div() - .w(px(14.0)) - .child(if entry.is_dir { - if this_expanded(&expanded, entry.id) { "▾" } else { "▸" } - } else { - "" - }), - ) - .child(div().child(entry.name.clone())); - if !is_selected { - row = row.hover(|style| style.bg(Hsla::from(colors.selected).opacity(0.3))); - } - column = column.child(row); - } - column - } - ExplorerView::Icons => { - // A flat grid of the roots' children with thumbnails. - let mut grid = div() - .id(ElementId::named_usize("gpui-widgets-explorer-icons", control)) - .flex() - .flex_wrap() - .gap_2() - .p_2() - .overflow_y_scroll(); - for entry in roots.iter().flat_map(|root| self.data.read(cx).children(root.id)) { - let is_selected = selected == Some(entry.id); - let click_entry = entry.clone(); - grid = grid.child( - div() - .id(ElementId::named_usize( - "gpui-widgets-explorer-icon", - entry.id as usize, - )) - .w(px(96.0)) - .p_1() - .rounded_md() - .flex() - .flex_col() - .items_center() - .gap_1() - .cursor_pointer() - .bg(if is_selected { colors.selected } else { transparent() }) - .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { - this.selected = Some(click_entry.id); - if event.click_count() >= 2 { - this.open(&click_entry, cx); - } - cx.notify(); - })) - .child( - if let Some(thumbnail) = entry.thumbnail.clone() { - img(thumbnail).w(px(72.0)).h(px(48.0)).into_any_element() - } else { - div() - .w(px(72.0)) - .h(px(48.0)) - .rounded_md() - .bg(Hsla::from(colors.selected).opacity(0.4)) - .flex() - .items_center() - .justify_center() - .text_color(colors.text) - .child( - entry - .name - .chars() - .next() - .unwrap_or(' ') - .to_string(), - ) - .into_any_element() - }, - ) - .child( - div() - .w_full() - .text_color(colors.text) - .child(entry.name.clone()), - ), - ); - } - grid - } - }; + let content = match self.view { + ExplorerView::Tree => { + let expanded = self.expanded.clone(); + let rows = flatten_tree(&roots, &expanded, |id| self.data.read(cx).children(id)); + let mut column = div() + .id(ElementId::named_usize( + "gpui-widgets-explorer-tree", + control, + )) + .flex() + .flex_col() + .py_1() + .overflow_y_scroll(); + for (entry, depth) in rows { + let is_selected = selected == Some(entry.id); + let click_entry = entry.clone(); + let entry_id = entry.id; + let mut row = div() + .id(ElementId::named_usize( + "gpui-widgets-explorer-entry", + entry.id as usize, + )) + .debug_selector(move || { + format!("gpui-widgets-explorer-entry-{entry_id}").into() + }) + .h(px(24.0)) + .flex() + .items_center() + .gap_1() + .pl(px(8.0 + depth as f32 * 14.0)) + .pr_2() + .cursor_pointer() + .bg(if is_selected { + colors.selected + } else { + transparent() + }) + .text_color(if is_selected { + colors.selected_text + } else { + colors.text + }) + .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + this.selected = Some(click_entry.id); + if click_entry.is_dir { + if event.click_count() >= 2 { + this.open(&click_entry, cx); + } else { + this.toggle(click_entry.id, cx); + } + } else if event.click_count() >= 2 { + this.open(&click_entry, cx); + } + cx.notify(); + })) + .child(div().w(px(14.0)).child(if entry.is_dir { + if this_expanded(&expanded, entry.id) { + "▾" + } else { + "▸" + } + } else { + "" + })) + .child(div().child(entry.name.clone())); + if !is_selected { + row = row.hover(|style| style.bg(Hsla::from(colors.selected).opacity(0.3))); + } + column = column.child(row); + } + column + } + ExplorerView::Icons => { + // A flat grid of the roots' children with thumbnails. + let mut grid = div() + .id(ElementId::named_usize( + "gpui-widgets-explorer-icons", + control, + )) + .flex() + .flex_wrap() + .gap_2() + .p_2() + .overflow_y_scroll(); + for entry in roots + .iter() + .flat_map(|root| self.data.read(cx).children(root.id)) + { + let is_selected = selected == Some(entry.id); + let click_entry = entry.clone(); + grid = grid.child( + div() + .id(ElementId::named_usize( + "gpui-widgets-explorer-icon", + entry.id as usize, + )) + .w(px(96.0)) + .p_1() + .rounded_md() + .flex() + .flex_col() + .items_center() + .gap_1() + .cursor_pointer() + .bg(if is_selected { + colors.selected + } else { + transparent() + }) + .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + this.selected = Some(click_entry.id); + if event.click_count() >= 2 { + this.open(&click_entry, cx); + } + cx.notify(); + })) + .child(if let Some(thumbnail) = entry.thumbnail.clone() { + img(thumbnail).w(px(72.0)).h(px(48.0)).into_any_element() + } else { + div() + .w(px(72.0)) + .h(px(48.0)) + .rounded_md() + .bg(Hsla::from(colors.selected).opacity(0.4)) + .flex() + .items_center() + .justify_center() + .text_color(colors.text) + .child(entry.name.chars().next().unwrap_or(' ').to_string()) + .into_any_element() + }) + .child( + div() + .w_full() + .text_color(colors.text) + .child(entry.name.clone()), + ), + ); + } + grid + } + }; - div() - .id(ElementId::named_usize("gpui-widgets-explorer", control)) - .size_full() - .flex() - .flex_col() - .on_drop( - cx.listener( - |this, paths: &Arc, _window, cx| { - cx.emit(ProjectExplorerEvent::FileDropRequested { - control: this.control, - paths: paths.0.iter().cloned().collect(), - }); - cx.notify(); - }, - ), - ) - .child(toolbar) - .child(content.flex_1()) - } + div() + .id(ElementId::named_usize("gpui-widgets-explorer", control)) + .size_full() + .flex() + .flex_col() + .on_drop( + cx.listener(|this, paths: &Arc, _window, cx| { + cx.emit(ProjectExplorerEvent::FileDropRequested { + control: this.control, + paths: paths.0.iter().cloned().collect(), + }); + cx.notify(); + }), + ) + .child(toolbar) + .child(content.flex_1()) + } } fn this_expanded(expanded: &HashSet, id: u64) -> bool { - expanded.contains(&id) + expanded.contains(&id) } /// A small toggle button for the view switcher. fn toggle_button( - id: &'static str, - label: &'static str, - active: bool, - colors: &gpui::colors::Colors, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + id: &'static str, + label: &'static str, + active: bool, + colors: &gpui::colors::Colors, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> impl IntoElement { - div() - .id(id) - .debug_selector(move || id.into()) - .px_2() - .py_1() - .rounded_md() - .bg(if active { colors.selected } else { transparent() }) - .text_color(if active { - colors.selected_text - } else { - colors.text - }) - .cursor_pointer() - .on_click(on_click) - .child(label) + div() + .id(id) + .debug_selector(move || id.into()) + .px_2() + .py_1() + .rounded_md() + .bg(if active { + colors.selected + } else { + transparent() + }) + .text_color(if active { + colors.selected_text + } else { + colors.text + }) + .cursor_pointer() + .on_click(on_click) + .child(label) } #[cfg(test)] mod tests { - use super::*; - use gpui::{ - Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext, VisualTestContext, - px, size, - }; + use super::*; + use gpui::{ + Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext, VisualTestContext, px, size, + }; - #[test] - fn flatten_tree_honors_expansion() { - let roots = vec![ - ProjectEntry::new(1, "Footage", true), - ProjectEntry::new(2, "Notes.md", false), - ]; - let expanded = HashSet::from([1u64]); - let children = |id: u64| -> Vec { - if id == 1 { - vec![ - ProjectEntry::new(10, "a.mov", false), - ProjectEntry::new(11, "b.mov", false), - ] - } else { - Vec::new() - } - }; - let rows = flatten_tree(&roots, &expanded, children); - assert_eq!( - rows.iter().map(|(e, d)| (e.id, *d)).collect::>(), - vec![(1, 0), (10, 1), (11, 1), (2, 0)] - ); + #[test] + fn flatten_tree_honors_expansion() { + let roots = vec![ + ProjectEntry::new(1, "Footage", true), + ProjectEntry::new(2, "Notes.md", false), + ]; + let expanded = HashSet::from([1u64]); + let children = |id: u64| -> Vec { + if id == 1 { + vec![ + ProjectEntry::new(10, "a.mov", false), + ProjectEntry::new(11, "b.mov", false), + ] + } else { + Vec::new() + } + }; + let rows = flatten_tree(&roots, &expanded, children); + assert_eq!( + rows.iter().map(|(e, d)| (e.id, *d)).collect::>(), + vec![(1, 0), (10, 1), (11, 1), (2, 0)] + ); - let collapsed = flatten_tree(&roots, &HashSet::new(), children); - assert_eq!( - collapsed.iter().map(|(e, _)| e.id).collect::>(), - vec![1, 2] - ); - } + let collapsed = flatten_tree(&roots, &HashSet::new(), children); + assert_eq!( + collapsed.iter().map(|(e, _)| e.id).collect::>(), + vec![1, 2] + ); + } - #[test] - fn entry_thumbnail_optional() { - let plain = ProjectEntry::new(1, "x.mov", false); - assert_eq!(plain.thumbnail, None); - let with = plain.clone().with_thumbnail("thumbs/x.png"); - assert_eq!(with.thumbnail.as_deref(), Some("thumbs/x.png")); - } + #[test] + fn entry_thumbnail_optional() { + let plain = ProjectEntry::new(1, "x.mov", false); + assert_eq!(plain.thumbnail, None); + let with = plain.clone().with_thumbnail("thumbs/x.png"); + assert_eq!(with.thumbnail.as_deref(), Some("thumbs/x.png")); + } - // --- view interaction tests --- + // --- view interaction tests --- - struct MockData; - impl ProjectDataSource for MockData { - fn roots(&self) -> Vec { - vec![ - ProjectEntry::new(1, "Footage", true), - ProjectEntry::new(2, "Notes.md", false), - ] - } - fn children(&self, parent_id: u64) -> Vec { - if parent_id == 1 { - vec![ProjectEntry::new(10, "a.mov", false)] - } else { - Vec::new() - } - } - } + struct MockData; + impl ProjectDataSource for MockData { + fn roots(&self) -> Vec { + vec![ + ProjectEntry::new(1, "Footage", true), + ProjectEntry::new(2, "Notes.md", false), + ] + } + fn children(&self, parent_id: u64) -> Vec { + if parent_id == 1 { + vec![ProjectEntry::new(10, "a.mov", false)] + } else { + Vec::new() + } + } + } - struct Host { - explorer: Entity>, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.explorer.clone()) - } - } + struct Host { + explorer: Entity>, + events: Vec, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.explorer.clone()) + } + } - fn make_explorer(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(300.0), px(240.0)), |window, cx| { - let data = cx.new(|_| MockData); - let explorer = cx.new(|cx| ProjectExplorer::new(1, data, window, cx)); - let host = Host { - explorer, - events: Vec::new(), - }; - cx.subscribe( - &host.explorer, - |host: &mut Host, - _e: Entity>, - event: &ProjectExplorerEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - (cx, host) - } + fn make_explorer(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(300.0), px(240.0)), |window, cx| { + let data = cx.new(|_| MockData); + let explorer = cx.new(|cx| ProjectExplorer::new(1, data, window, cx)); + let host = Host { + explorer, + events: Vec::new(), + }; + cx.subscribe( + &host.explorer, + |host: &mut Host, + _e: Entity>, + event: &ProjectExplorerEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + (cx, host) + } - #[gpui::test] - async fn clicking_a_folder_expands_it(cx: &mut TestAppContext) { - let (cx, _host) = make_explorer(cx); - // The first row (Footage) is at the top of the content area. - let first = cx - .debug_bounds("gpui-widgets-explorer-entry-1") - .expect("first row rendered"); - cx.simulate_click(first.center(), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn clicking_a_folder_expands_it(cx: &mut TestAppContext) { + let (cx, _host) = make_explorer(cx); + // The first row (Footage) is at the top of the content area. + let first = cx + .debug_bounds("gpui-widgets-explorer-entry-1") + .expect("first row rendered"); + cx.simulate_click(first.center(), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - // The child a.mov should now be visible. - let child = cx.debug_bounds("gpui-widgets-explorer-entry-10"); - assert!(child.is_some(), "expanded folder should reveal its child"); - } + // The child a.mov should now be visible. + let child = cx.debug_bounds("gpui-widgets-explorer-entry-10"); + assert!(child.is_some(), "expanded folder should reveal its child"); + } - #[gpui::test] - async fn double_clicking_a_file_emits_open_request(cx: &mut TestAppContext) { - let (cx, host) = make_explorer(cx); - // Expand Footage first. - let first = cx - .debug_bounds("gpui-widgets-explorer-entry-1") - .expect("first row rendered"); - cx.simulate_click(first.center(), Modifiers::none()); - cx.run_until_parked(); - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn double_clicking_a_file_emits_open_request(cx: &mut TestAppContext) { + let (cx, host) = make_explorer(cx); + // Expand Footage first. + let first = cx + .debug_bounds("gpui-widgets-explorer-entry-1") + .expect("first row rendered"); + cx.simulate_click(first.center(), Modifiers::none()); + cx.run_until_parked(); + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - // Double-click a.mov. - let child = cx - .debug_bounds("gpui-widgets-explorer-entry-10") - .expect("child row rendered"); - let modifiers = Modifiers::none(); - cx.simulate_event(MouseDownEvent { - position: child.center(), - modifiers, - button: MouseButton::Left, - click_count: 2, - first_mouse: false, - }); - cx.simulate_event(gpui::MouseUpEvent { - position: child.center(), - modifiers, - button: MouseButton::Left, - click_count: 2, - }); - cx.run_until_parked(); + // Double-click a.mov. + let child = cx + .debug_bounds("gpui-widgets-explorer-entry-10") + .expect("child row rendered"); + let modifiers = Modifiers::none(); + cx.simulate_event(MouseDownEvent { + position: child.center(), + modifiers, + button: MouseButton::Left, + click_count: 2, + first_mouse: false, + }); + cx.simulate_event(gpui::MouseUpEvent { + position: child.center(), + modifiers, + button: MouseButton::Left, + click_count: 2, + }); + cx.run_until_parked(); - let opened = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, ProjectExplorerEvent::OpenRequested { id: 10, .. }) - }) - }); - assert!(opened, "expected an OpenRequested for a.mov"); - } + let opened = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, ProjectExplorerEvent::OpenRequested { id: 10, .. })) + }); + assert!(opened, "expected an OpenRequested for a.mov"); + } - #[gpui::test] - async fn switching_to_icons_emits_view_changed(cx: &mut TestAppContext) { - let (cx, host) = make_explorer(cx); - let toggle = cx - .debug_bounds("gpui-widgets-explorer-icons") - .expect("icons toggle rendered"); - cx.simulate_click(toggle.center(), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn switching_to_icons_emits_view_changed(cx: &mut TestAppContext) { + let (cx, host) = make_explorer(cx); + let toggle = cx + .debug_bounds("gpui-widgets-explorer-icons") + .expect("icons toggle rendered"); + cx.simulate_click(toggle.center(), Modifiers::none()); + cx.run_until_parked(); - let changed = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!( - e, - ProjectExplorerEvent::ViewChanged { view: ExplorerView::Icons, .. } - ) - }) - }); - assert!(changed, "expected a ViewChanged(Icons) event"); - } + let changed = cx.read(|app| { + host.read(app).events.iter().any(|e| { + matches!( + e, + ProjectExplorerEvent::ViewChanged { + view: ExplorerView::Icons, + .. + } + ) + }) + }); + assert!(changed, "expected a ViewChanged(Icons) event"); + } } diff --git a/crates/gpui_widgets/src/radio_group.rs b/crates/gpui_widgets/src/radio_group.rs index e4fb1bef94..271b8534a7 100644 --- a/crates/gpui_widgets/src/radio_group.rs +++ b/crates/gpui_widgets/src/radio_group.rs @@ -5,239 +5,249 @@ //! the widget via [`RadioGroup::set_selected`]. use gpui::{ - App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Render, - SharedString, Window, colors::DefaultColors, div, prelude::*, px, + App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Render, + SharedString, Window, colors::DefaultColors, div, prelude::*, px, }; /// A single selectable option. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RadioOption { - /// The option's value (stable id). - pub value: usize, - /// The label shown next to the radio circle. - pub label: SharedString, + /// The option's value (stable id). + pub value: usize, + /// The label shown next to the radio circle. + pub label: SharedString, } impl RadioOption { - /// Create an option. - pub fn new(value: usize, label: impl Into) -> Self { - Self { - value, - label: label.into(), - } - } + /// Create an option. + pub fn new(value: usize, label: impl Into) -> Self { + Self { + value, + label: label.into(), + } + } } /// A request emitted when an option is selected. #[derive(Debug, Clone, PartialEq)] pub enum RadioGroupEvent { - /// The user clicked an option. - Selected { - /// The group's stable id. - control: usize, - /// The chosen option value. - value: usize, - }, + /// The user clicked an option. + Selected { + /// The group's stable id. + control: usize, + /// The chosen option value. + value: usize, + }, } /// A group of mutually-exclusive radio options. pub struct RadioGroup { - control: usize, - options: Vec, - selected: Option, - enabled: bool, - focus_handle: FocusHandle, + control: usize, + options: Vec, + selected: Option, + enabled: bool, + focus_handle: FocusHandle, } impl RadioGroup { - /// Create a radio group for `control` over `options`. - pub fn new( - control: usize, - options: Vec, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - options, - selected: None, - enabled: true, - focus_handle: cx.focus_handle(), - } - } + /// Create a radio group for `control` over `options`. + pub fn new( + control: usize, + options: Vec, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + options, + selected: None, + enabled: true, + focus_handle: cx.focus_handle(), + } + } - /// The currently selected value, if any. - pub fn selected(&self) -> Option { - self.selected - } + /// The currently selected value, if any. + pub fn selected(&self) -> Option { + self.selected + } - /// Apply the host's selection and repaint. - pub fn set_selected(&mut self, selected: Option, cx: &mut Context) { - if self.selected != selected { - self.selected = selected; - cx.notify(); - } - } + /// Apply the host's selection and repaint. + pub fn set_selected(&mut self, selected: Option, cx: &mut Context) { + if self.selected != selected { + self.selected = selected; + cx.notify(); + } + } - /// Enable or disable the whole group. - pub fn with_enabled(mut self, enabled: bool) -> Self { - self.enabled = enabled; - self - } + /// Enable or disable the whole group. + pub fn with_enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } - fn emit_select(&self, value: usize, cx: &mut Context) { - if !self.enabled { - return; - } - cx.emit(RadioGroupEvent::Selected { - control: self.control, - value, - }); - cx.notify(); - } + fn emit_select(&self, value: usize, cx: &mut Context) { + if !self.enabled { + return; + } + cx.emit(RadioGroupEvent::Selected { + control: self.control, + value, + }); + cx.notify(); + } } impl EventEmitter for RadioGroup {} impl Focusable for RadioGroup { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for RadioGroup { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let selected = self.selected; - let enabled = self.enabled; - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let selected = self.selected; + let enabled = self.enabled; + let control = self.control; - let mut column = div().flex().flex_col().gap(px(4.0)); - for option in self.options.clone() { - let is_selected = selected == Some(option.value); - let accent = if is_selected { - colors.selected - } else { - colors.background - }; - let border = if enabled { - colors.border - } else { - colors.disabled - }; - let text_color = if enabled { - colors.text - } else { - colors.disabled - }; - let value = option.value; - let label = option.label; + let mut column = div().flex().flex_col().gap(px(4.0)); + for option in self.options.clone() { + let is_selected = selected == Some(option.value); + let accent = if is_selected { + colors.selected + } else { + colors.background + }; + let border = if enabled { + colors.border + } else { + colors.disabled + }; + let text_color = if enabled { + colors.text + } else { + colors.disabled + }; + let value = option.value; + let label = option.label; - column = column.child( - div() - .id(ElementId::named_usize( - format!("gpui-widgets-radio-{control}"), - value, - )) - .flex() - .items_center() - .gap(px(6.0)) - .cursor_pointer() - .track_focus(&self.focus_handle) - .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| { - this.emit_select(value, cx); - cx.stop_propagation(); - })) - .child( - div() - .size(px(16.0)) - .rounded_full() - .border_1() - .border_color(border) - .bg(accent) - .flex() - .items_center() - .justify_center() - .child(if is_selected { - div().size(px(6.0)).rounded_full().bg(colors.selected_text) - } else { - div().size(px(0.0)) - }), - ) - .child(div().text_color(text_color).child(label)), - ); - } - column - } + column = column.child( + div() + .id(ElementId::named_usize( + format!("gpui-widgets-radio-{control}"), + value, + )) + .flex() + .items_center() + .gap(px(6.0)) + .cursor_pointer() + .track_focus(&self.focus_handle) + .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| { + this.emit_select(value, cx); + cx.stop_propagation(); + })) + .child( + div() + .size(px(16.0)) + .rounded_full() + .border_1() + .border_color(border) + .bg(accent) + .flex() + .items_center() + .justify_center() + .child(if is_selected { + div().size(px(6.0)).rounded_full().bg(colors.selected_text) + } else { + div().size(px(0.0)) + }), + ) + .child(div().text_color(text_color).child(label)), + ); + } + column + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; + use super::*; + use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size}; - #[test] - fn option_construction() { - let option = RadioOption::new(3, "1080p"); - assert_eq!(option.value, 3); - assert_eq!(option.label, "1080p"); - } + #[test] + fn option_construction() { + let option = RadioOption::new(3, "1080p"); + assert_eq!(option.value, 3); + assert_eq!(option.label, "1080p"); + } - #[gpui::test] - async fn click_emits_selection(cx: &mut TestAppContext) { - struct Host { - group: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.group.clone()) - } - } + #[gpui::test] + async fn click_emits_selection(cx: &mut TestAppContext) { + struct Host { + group: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.group.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(200.0), px(120.0)), |window, cx| { - let group = cx.new(|cx| { - RadioGroup::new( - 1, - vec![ - RadioOption::new(1, "1080p"), - RadioOption::new(2, "4K"), - RadioOption::new(3, "8K"), - ], - window, - cx, - ) - }); - let host = Host { - group, - events: Vec::new(), - }; - cx.subscribe( - &host.group, - |host: &mut Host, - _g: Entity, - event: &RadioGroupEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(200.0), px(120.0)), |window, cx| { + let group = cx.new(|cx| { + RadioGroup::new( + 1, + vec![ + RadioOption::new(1, "1080p"), + RadioOption::new(2, "4K"), + RadioOption::new(3, "8K"), + ], + window, + cx, + ) + }); + let host = Host { + group, + events: Vec::new(), + }; + cx.subscribe( + &host.group, + |host: &mut Host, + _g: Entity, + event: &RadioGroupEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Second option (4K) is around y = 30 + 4 + 16/2. - cx.simulate_click(point(px(30.0), px(42.0)), Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Second option (4K) is around y = 30 + 4 + 16/2. + cx.simulate_click(point(px(30.0), px(42.0)), Modifiers::none()); + cx.run_until_parked(); - let emitted = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, RadioGroupEvent::Selected { control: 1, value: 2 }) - }) - }); - assert!(emitted); - } + let emitted = cx.read(|app| { + host.read(app).events.iter().any(|e| { + matches!( + e, + RadioGroupEvent::Selected { + control: 1, + value: 2 + } + ) + }) + }); + assert!(emitted); + } } diff --git a/crates/gpui_widgets/src/scopes/math.rs b/crates/gpui_widgets/src/scopes/math.rs index 47d6954d32..5239c3e644 100644 --- a/crates/gpui_widgets/src/scopes/math.rs +++ b/crates/gpui_widgets/src/scopes/math.rs @@ -4,43 +4,45 @@ /// Bin luma samples (`0..1`) into `bins` histogram buckets, returning the /// count per bucket. Samples outside `0..1` clamp to the edges. pub fn histogram_bins(samples: &[f32], bins: usize) -> Vec { - if bins == 0 { - return Vec::new(); - } - let mut out = vec![0u32; bins]; - if samples.is_empty() { - return out; - } - for &sample in samples { - let clamped = sample.clamp(0.0, 1.0); - let index = ((clamped * bins as f32) as usize).min(bins - 1); - out[index] += 1; - } - out + if bins == 0 { + return Vec::new(); + } + let mut out = vec![0u32; bins]; + if samples.is_empty() { + return out; + } + for &sample in samples { + let clamped = sample.clamp(0.0, 1.0); + let index = ((clamped * bins as f32) as usize).min(bins - 1); + out[index] += 1; + } + out } /// Compute the min/max envelope of `samples` over `columns` vertical slices. /// Each column covers a contiguous slice of the input; empty columns report /// `(0.0, 0.0)`. pub fn waveform_envelope(samples: &[f32], columns: usize) -> Vec<(f32, f32)> { - let mut out = vec![(0.0f32, 0.0f32); columns.max(1)]; - if columns == 0 || samples.is_empty() { - return out; - } - for column in 0..columns { - let start = column * samples.len() / columns; - let end = ((column + 1) * samples.len() / columns).max(start + 1).min(samples.len()); - let mut min = f32::MAX; - let mut max = f32::MIN; - for &sample in &samples[start..end] { - min = min.min(sample); - max = max.max(sample); - } - if end > start { - out[column] = (min, max); - } - } - out + let mut out = vec![(0.0f32, 0.0f32); columns.max(1)]; + if columns == 0 || samples.is_empty() { + return out; + } + for column in 0..columns { + let start = column * samples.len() / columns; + let end = ((column + 1) * samples.len() / columns) + .max(start + 1) + .min(samples.len()); + let mut min = f32::MAX; + let mut max = f32::MIN; + for &sample in &samples[start..end] { + min = min.min(sample); + max = max.max(sample); + } + if end > start { + out[column] = (min, max); + } + } + out } /// A chroma sample pair (e.g. `u`, `v` centered on `0.5`). @@ -50,91 +52,86 @@ pub type ChromaSample = (f32, f32); /// (`u - 0.5`, `v - 0.5`, normalized to `-0.5..0.5`). Out-of-range values /// clamp. pub fn vectorscope_points(samples: &[ChromaSample]) -> Vec<(f32, f32)> { - samples - .iter() - .map(|&(u, v)| { - ( - (u - 0.5).clamp(-0.5, 0.5), - (v - 0.5).clamp(-0.5, 0.5), - ) - }) - .collect() + samples + .iter() + .map(|&(u, v)| ((u - 0.5).clamp(-0.5, 0.5), (v - 0.5).clamp(-0.5, 0.5))) + .collect() } /// Map a normalized `0..1` level to a meter segment's lit count: `segments` /// segments, the lit portion is proportional to the level. pub fn meter_lit_segments(level: f32, segments: usize) -> usize { - let lit = (level.clamp(0.0, 1.0) * segments as f32).round() as usize; - lit.min(segments) + let lit = (level.clamp(0.0, 1.0) * segments as f32).round() as usize; + lit.min(segments) } /// Peak-hold decay: `peak` decays toward `level` at `decay_per_frame`. pub fn decay_peak(peak: f32, level: f32, decay_per_frame: f32) -> f32 { - if level >= peak { - level - } else { - (peak - decay_per_frame).max(level) - } + if level >= peak { + level + } else { + (peak - decay_per_frame).max(level) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn histogram_bins_correctly() { - let samples = [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0]; - let bins = histogram_bins(&samples, 4); - // Buckets: [0,0.25): 0.0, 0.1; [0.25,0.5): 0.25; [0.5,0.75): 0.5; - // [0.75,1]: 0.75, 0.9, 1.0 (1.0 clamps into the last bucket). - assert_eq!(bins, vec![2, 1, 1, 3]); - } + #[test] + fn histogram_bins_correctly() { + let samples = [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0]; + let bins = histogram_bins(&samples, 4); + // Buckets: [0,0.25): 0.0, 0.1; [0.25,0.5): 0.25; [0.5,0.75): 0.5; + // [0.75,1]: 0.75, 0.9, 1.0 (1.0 clamps into the last bucket). + assert_eq!(bins, vec![2, 1, 1, 3]); + } - #[test] - fn histogram_clamps_out_of_range() { - let bins = histogram_bins(&[-1.0, 0.5, 2.0], 2); - assert_eq!(bins, vec![1, 2]); - } + #[test] + fn histogram_clamps_out_of_range() { + let bins = histogram_bins(&[-1.0, 0.5, 2.0], 2); + assert_eq!(bins, vec![1, 2]); + } - #[test] - fn histogram_handles_empty_and_zero_bins() { - assert_eq!(histogram_bins(&[], 4), vec![0u32; 4]); - assert_eq!(histogram_bins(&[0.5], 0), Vec::::new()); - } + #[test] + fn histogram_handles_empty_and_zero_bins() { + assert_eq!(histogram_bins(&[], 4), vec![0u32; 4]); + assert_eq!(histogram_bins(&[0.5], 0), Vec::::new()); + } - #[test] - fn waveform_envelope_slices() { - let samples: Vec = (0..100).map(|i| i as f32 / 100.0).collect(); - let env = waveform_envelope(&samples, 10); - assert_eq!(env.len(), 10); - // First column covers [0, 10): min 0.0, max 0.09. - assert!((env[0].0 - 0.0).abs() < 0.001); - assert!((env[0].1 - 0.09).abs() < 0.001); - // Last column covers [90, 100): min 0.9, max 0.99. - assert!((env[9].0 - 0.9).abs() < 0.001); - assert!((env[9].1 - 0.99).abs() < 0.001); - } + #[test] + fn waveform_envelope_slices() { + let samples: Vec = (0..100).map(|i| i as f32 / 100.0).collect(); + let env = waveform_envelope(&samples, 10); + assert_eq!(env.len(), 10); + // First column covers [0, 10): min 0.0, max 0.09. + assert!((env[0].0 - 0.0).abs() < 0.001); + assert!((env[0].1 - 0.09).abs() < 0.001); + // Last column covers [90, 100): min 0.9, max 0.99. + assert!((env[9].0 - 0.9).abs() < 0.001); + assert!((env[9].1 - 0.99).abs() < 0.001); + } - #[test] - fn vectorscope_projection_centers() { - let points = vectorscope_points(&[(0.5, 0.5), (1.0, 0.0), (0.0, 1.0)]); - assert_eq!(points[0], (0.0, 0.0)); - assert_eq!(points[1], (0.5, -0.5)); - assert_eq!(points[2], (-0.5, 0.5)); - // Out-of-range clamps. - let clamped = vectorscope_points(&[(2.0, -1.0)]); - assert_eq!(clamped[0], (0.5, -0.5)); - } + #[test] + fn vectorscope_projection_centers() { + let points = vectorscope_points(&[(0.5, 0.5), (1.0, 0.0), (0.0, 1.0)]); + assert_eq!(points[0], (0.0, 0.0)); + assert_eq!(points[1], (0.5, -0.5)); + assert_eq!(points[2], (-0.5, 0.5)); + // Out-of-range clamps. + let clamped = vectorscope_points(&[(2.0, -1.0)]); + assert_eq!(clamped[0], (0.5, -0.5)); + } - #[test] - fn meter_lit_and_peak_decay() { - assert_eq!(meter_lit_segments(0.0, 8), 0); - assert_eq!(meter_lit_segments(0.5, 8), 4); - assert_eq!(meter_lit_segments(1.0, 8), 8); - assert_eq!(meter_lit_segments(1.5, 8), 8); + #[test] + fn meter_lit_and_peak_decay() { + assert_eq!(meter_lit_segments(0.0, 8), 0); + assert_eq!(meter_lit_segments(0.5, 8), 4); + assert_eq!(meter_lit_segments(1.0, 8), 8); + assert_eq!(meter_lit_segments(1.5, 8), 8); - assert_eq!(decay_peak(0.8, 0.5, 0.1), 0.7); - assert_eq!(decay_peak(0.8, 0.9, 0.1), 0.9); - assert_eq!(decay_peak(0.8, 0.78, 0.1), 0.78); - } + assert_eq!(decay_peak(0.8, 0.5, 0.1), 0.7); + assert_eq!(decay_peak(0.8, 0.9, 0.1), 0.9); + assert_eq!(decay_peak(0.8, 0.78, 0.1), 0.78); + } } diff --git a/crates/gpui_widgets/src/scopes/mod.rs b/crates/gpui_widgets/src/scopes/mod.rs index e0ebb07a76..b7ee62e569 100644 --- a/crates/gpui_widgets/src/scopes/mod.rs +++ b/crates/gpui_widgets/src/scopes/mod.rs @@ -10,280 +10,288 @@ mod math; pub use math::*; use gpui::{ - App, Bounds, Context, Entity, FocusHandle, Focusable, Hsla, Render, Window, canvas, - colors::DefaultColors, fill, point, prelude::*, px, size, + App, Bounds, Context, Entity, FocusHandle, Focusable, Hsla, Render, Window, canvas, + colors::DefaultColors, fill, point, prelude::*, px, size, }; /// Provides luma samples (`0..1`) for the histogram and waveform scopes. pub trait LumaDataSource: 'static { - /// Luma samples for the current frame. - fn luma_samples(&self) -> Vec; + /// Luma samples for the current frame. + fn luma_samples(&self) -> Vec; } /// Provides chroma samples for the vectorscope. pub trait ChromaDataSource: 'static { - /// `(u, v)` samples in `0..1` (centered on `0.5`). - fn chroma_samples(&self) -> Vec<(f32, f32)>; + /// `(u, v)` samples in `0..1` (centered on `0.5`). + fn chroma_samples(&self) -> Vec<(f32, f32)>; } /// A luminance histogram. pub struct Histogram { - data: Entity, - focus_handle: FocusHandle, + data: Entity, + focus_handle: FocusHandle, } impl Histogram { - /// Create a histogram over `data`. - pub fn new( - _control: usize, - data: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - data, - focus_handle: cx.focus_handle(), - } - } + /// Create a histogram over `data`. + pub fn new( + _control: usize, + data: Entity, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + data, + focus_handle: cx.focus_handle(), + } + } - /// The current histogram bins (for tests and hosts). - pub fn bins(&self, cx: &App) -> Vec { - histogram_bins(&self.data.read(cx).luma_samples(), 64) - } + /// The current histogram bins (for tests and hosts). + pub fn bins(&self, cx: &App) -> Vec { + histogram_bins(&self.data.read(cx).luma_samples(), 64) + } } impl Focusable for Histogram { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for Histogram { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let bins = self.bins(cx); - let height = bins.iter().copied().max().unwrap_or(1).max(1) as f32; - canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, _cx| { - let width = f32::from(bounds.size.width); - let bar_w = width / bins.len() as f32; - let bar_color = Hsla::from(colors.selected); - for (index, &count) in bins.iter().enumerate() { - let h = (count as f32 / height) * f32::from(bounds.size.height); - let bar = Bounds::new( - point(bounds.left() + px(index as f32 * bar_w), bounds.bottom() - px(h)), - size(px((bar_w - 1.0).max(1.0)), px(h)), - ); - window.paint_quad(fill(bar, bar_color)); - } - }, - ) - .size_full() - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let bins = self.bins(cx); + let height = bins.iter().copied().max().unwrap_or(1).max(1) as f32; + canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, _cx| { + let width = f32::from(bounds.size.width); + let bar_w = width / bins.len() as f32; + let bar_color = Hsla::from(colors.selected); + for (index, &count) in bins.iter().enumerate() { + let h = (count as f32 / height) * f32::from(bounds.size.height); + let bar = Bounds::new( + point( + bounds.left() + px(index as f32 * bar_w), + bounds.bottom() - px(h), + ), + size(px((bar_w - 1.0).max(1.0)), px(h)), + ); + window.paint_quad(fill(bar, bar_color)); + } + }, + ) + .size_full() + } } /// A waveform scope (min/max envelope over the frame's luma). pub struct Waveform { - data: Entity, - focus_handle: FocusHandle, + data: Entity, + focus_handle: FocusHandle, } impl Waveform { - /// Create a waveform scope. - pub fn new( - _control: usize, - data: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - data, - focus_handle: cx.focus_handle(), - } - } + /// Create a waveform scope. + pub fn new( + _control: usize, + data: Entity, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + data, + focus_handle: cx.focus_handle(), + } + } - /// The current envelope columns. - pub fn envelope(&self, cx: &App) -> Vec<(f32, f32)> { - waveform_envelope(&self.data.read(cx).luma_samples(), 128) - } + /// The current envelope columns. + pub fn envelope(&self, cx: &App) -> Vec<(f32, f32)> { + waveform_envelope(&self.data.read(cx).luma_samples(), 128) + } } impl Focusable for Waveform { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for Waveform { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let envelope = self.envelope(cx); - canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, _cx| { - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - let col_w = width / envelope.len() as f32; - let line = Hsla::from(colors.selected); - for (column, &(min, max)) in envelope.iter().enumerate() { - let y_min = (1.0 - min.clamp(0.0, 1.0)) * height; - let y_max = (1.0 - max.clamp(0.0, 1.0)) * height; - let band = Bounds::new( - point(bounds.left() + px(column as f32 * col_w), bounds.top() + px(y_max)), - size(px((col_w - 0.5).max(0.5)), px((y_min - y_max).max(1.0))), - ); - window.paint_quad(fill(band, line)); - } - }, - ) - .size_full() - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let envelope = self.envelope(cx); + canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, _cx| { + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + let col_w = width / envelope.len() as f32; + let line = Hsla::from(colors.selected); + for (column, &(min, max)) in envelope.iter().enumerate() { + let y_min = (1.0 - min.clamp(0.0, 1.0)) * height; + let y_max = (1.0 - max.clamp(0.0, 1.0)) * height; + let band = Bounds::new( + point( + bounds.left() + px(column as f32 * col_w), + bounds.top() + px(y_max), + ), + size(px((col_w - 0.5).max(0.5)), px((y_min - y_max).max(1.0))), + ); + window.paint_quad(fill(band, line)); + } + }, + ) + .size_full() + } } /// A vectorscope (chroma projection with a graticule). pub struct Vectorscope { - data: Entity, - focus_handle: FocusHandle, + data: Entity, + focus_handle: FocusHandle, } impl Vectorscope { - /// Create a vectorscope. - pub fn new( - _control: usize, - data: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - data, - focus_handle: cx.focus_handle(), - } - } + /// Create a vectorscope. + pub fn new( + _control: usize, + data: Entity, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + data, + focus_handle: cx.focus_handle(), + } + } - /// The projected chroma points. - pub fn points(&self, cx: &App) -> Vec<(f32, f32)> { - vectorscope_points(&self.data.read(cx).chroma_samples()) - } + /// The projected chroma points. + pub fn points(&self, cx: &App) -> Vec<(f32, f32)> { + vectorscope_points(&self.data.read(cx).chroma_samples()) + } } impl Focusable for Vectorscope { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for Vectorscope { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let points = self.points(cx); - canvas( - move |_bounds, _window, _cx| (), - move |bounds, (), window, _cx| { - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - let center = point(bounds.left() + px(width / 2.0), bounds.top() + px(height / 2.0)); - let grid = Hsla::from(colors.border); - // Graticule: crosshair + box. - window.paint_quad(fill( - Bounds::new( - point(bounds.left(), center.y - px(0.5)), - size(bounds.size.width, px(1.0)), - ), - grid, - )); - window.paint_quad(fill( - Bounds::new( - point(center.x - px(0.5), bounds.top()), - size(px(1.0), bounds.size.height), - ), - grid, - )); - // Points: u -> x, v -> y (inverted). - let point_color = Hsla::from(colors.selected); - for &(u, v) in points.iter().take(4096) { - let x = center.x + px(u / 0.5 * width / 2.0); - let y = center.y - px(v / 0.5 * height / 2.0); - let dot = Bounds::new(point(x - px(1.0), y - px(1.0)), size(px(2.0), px(2.0))); - window.paint_quad(fill(dot, point_color)); - } - }, - ) - .size_full() - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let points = self.points(cx); + canvas( + move |_bounds, _window, _cx| (), + move |bounds, (), window, _cx| { + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + let center = point( + bounds.left() + px(width / 2.0), + bounds.top() + px(height / 2.0), + ); + let grid = Hsla::from(colors.border); + // Graticule: crosshair + box. + window.paint_quad(fill( + Bounds::new( + point(bounds.left(), center.y - px(0.5)), + size(bounds.size.width, px(1.0)), + ), + grid, + )); + window.paint_quad(fill( + Bounds::new( + point(center.x - px(0.5), bounds.top()), + size(px(1.0), bounds.size.height), + ), + grid, + )); + // Points: u -> x, v -> y (inverted). + let point_color = Hsla::from(colors.selected); + for &(u, v) in points.iter().take(4096) { + let x = center.x + px(u / 0.5 * width / 2.0); + let y = center.y - px(v / 0.5 * height / 2.0); + let dot = Bounds::new(point(x - px(1.0), y - px(1.0)), size(px(2.0), px(2.0))); + window.paint_quad(fill(dot, point_color)); + } + }, + ) + .size_full() + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{Entity, Render, TestAppContext, Window, div, px, size}; + use super::*; + use gpui::{Entity, Render, TestAppContext, Window, div, px, size}; - struct MockLuma(Vec); - impl LumaDataSource for MockLuma { - fn luma_samples(&self) -> Vec { - self.0.clone() - } - } - struct MockChroma(Vec<(f32, f32)>); - impl ChromaDataSource for MockChroma { - fn chroma_samples(&self) -> Vec<(f32, f32)> { - self.0.clone() - } - } + struct MockLuma(Vec); + impl LumaDataSource for MockLuma { + fn luma_samples(&self) -> Vec { + self.0.clone() + } + } + struct MockChroma(Vec<(f32, f32)>); + impl ChromaDataSource for MockChroma { + fn chroma_samples(&self) -> Vec<(f32, f32)> { + self.0.clone() + } + } - struct Host { - histogram: Entity>, - waveform: Entity>, - vectorscope: Entity>, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .child(self.histogram.clone()) - .child(self.waveform.clone()) - .child(self.vectorscope.clone()) - } - } + struct Host { + histogram: Entity>, + waveform: Entity>, + vectorscope: Entity>, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(self.histogram.clone()) + .child(self.waveform.clone()) + .child(self.vectorscope.clone()) + } + } - #[gpui::test] - async fn scopes_render_from_mock_data(cx: &mut TestAppContext) { - - use gpui::VisualTestContext; - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { - let luma = cx.new(|_| MockLuma((0..100).map(|i| i as f32 / 100.0).collect())); - let chroma = cx.new(|_| MockChroma(vec![(0.5, 0.5), (0.75, 0.25), (0.25, 0.75)])); - let histogram = cx.new(|cx| Histogram::new(1, luma.clone(), window, cx)); - let waveform = cx.new(|cx| Waveform::new(2, luma.clone(), window, cx)); - let vectorscope = cx.new(|cx| Vectorscope::new(3, chroma.clone(), window, cx)); - Host { - histogram, - waveform, - vectorscope, - } - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Force a draw so the canvas paint closures run (no double-lease: - // VisualTestContext::update goes through App::update_window). - cx.update(|window, cx| { - window.draw(cx).clear(); - }); + #[gpui::test] + async fn scopes_render_from_mock_data(cx: &mut TestAppContext) { + use gpui::VisualTestContext; + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| { + let luma = cx.new(|_| MockLuma((0..100).map(|i| i as f32 / 100.0).collect())); + let chroma = cx.new(|_| MockChroma(vec![(0.5, 0.5), (0.75, 0.25), (0.25, 0.75)])); + let histogram = cx.new(|cx| Histogram::new(1, luma.clone(), window, cx)); + let waveform = cx.new(|cx| Waveform::new(2, luma.clone(), window, cx)); + let vectorscope = cx.new(|cx| Vectorscope::new(3, chroma.clone(), window, cx)); + Host { + histogram, + waveform, + vectorscope, + } + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Force a draw so the canvas paint closures run (no double-lease: + // VisualTestContext::update goes through App::update_window). + cx.update(|window, cx| { + window.draw(cx).clear(); + }); - let (bins, envelope, points) = cx.read(|app| { - let host = host.read(app); - ( - host.histogram.read(app).bins(app), - host.waveform.read(app).envelope(app), - host.vectorscope.read(app).points(app), - ) - }); - assert_eq!(bins.len(), 64); - let total: u32 = bins.iter().sum(); - assert_eq!(total, 100); - assert_eq!(envelope.len(), 128); - assert_eq!(points.len(), 3); - } + let (bins, envelope, points) = cx.read(|app| { + let host = host.read(app); + ( + host.histogram.read(app).bins(app), + host.waveform.read(app).envelope(app), + host.vectorscope.read(app).points(app), + ) + }); + assert_eq!(bins.len(), 64); + let total: u32 = bins.iter().sum(); + assert_eq!(total, 100); + assert_eq!(envelope.len(), 128); + assert_eq!(points.len(), 3); + } } diff --git a/crates/gpui_widgets/src/slider/mod.rs b/crates/gpui_widgets/src/slider/mod.rs index e86b895aa2..258ac953aa 100644 --- a/crates/gpui_widgets/src/slider/mod.rs +++ b/crates/gpui_widgets/src/slider/mod.rs @@ -22,10 +22,10 @@ mod model; use std::sync::{Arc, RwLock}; use gpui::{ - App, BorderStyle, Bounds, ClickEvent, Context, Corners, DragMoveEvent, Edges, ElementId, - Entity, EventEmitter, FocusHandle, Focusable, KeyDownEvent, MouseButton, MouseDownEvent, - Pixels, Point, Render, ScrollWheelEvent, Subscription, Window, canvas, colors::DefaultColors, - div, fill, point, prelude::*, px, quad, size, + App, BorderStyle, Bounds, ClickEvent, Context, Corners, DragMoveEvent, Edges, ElementId, + Entity, EventEmitter, FocusHandle, Focusable, KeyDownEvent, MouseButton, MouseDownEvent, + Pixels, Point, Render, ScrollWheelEvent, Subscription, Window, canvas, colors::DefaultColors, + div, fill, point, prelude::*, px, quad, size, }; use gpui_elements::editable_text::{EditableTextState, StringStorage, text_input}; @@ -41,44 +41,44 @@ const VERTICAL_DRAG_RANGE_PX: f32 = 100.0; /// A request emitted by a slider. #[derive(Debug, Clone, PartialEq)] pub enum SliderEvent { - /// The value changed (drag, wheel, reset, typed commit). - ValueChanged { - /// The control's stable id. - control: usize, - /// The new value. - value: SliderValue, - }, - /// A drag gesture started. - DragStarted { - /// The control's stable id. - control: usize, - }, - /// A drag gesture ended. - DragFinished { - /// The control's stable id. - control: usize, - }, - /// Direct text entry was committed (or the editor lost focus). - EditCommitted { - /// The control's stable id. - control: usize, - /// The accepted value. - value: SliderValue, - }, - /// Direct text entry was cancelled (escape or rejected input). - EditCancelled { - /// The control's stable id. - control: usize, - }, - /// The keying diamond was clicked. - Keying(KeyingRequest), + /// The value changed (drag, wheel, reset, typed commit). + ValueChanged { + /// The control's stable id. + control: usize, + /// The new value. + value: SliderValue, + }, + /// A drag gesture started. + DragStarted { + /// The control's stable id. + control: usize, + }, + /// A drag gesture ended. + DragFinished { + /// The control's stable id. + control: usize, + }, + /// Direct text entry was committed (or the editor lost focus). + EditCommitted { + /// The control's stable id. + control: usize, + /// The accepted value. + value: SliderValue, + }, + /// Direct text entry was cancelled (escape or rejected input). + EditCancelled { + /// The control's stable id. + control: usize, + }, + /// The keying diamond was clicked. + Keying(KeyingRequest), } /// Transient payload carried by an in-flight drag gesture. #[derive(Clone, Copy, Debug)] struct SliderDrag { - /// Cursor Y at drag start (window coords, px). - start_y: f32, + /// Cursor Y at drag start (window coords, px). + start_y: f32, } /// Invisible ghost view that follows the cursor during a drag. @@ -86,549 +86,544 @@ struct SliderDrag { struct SliderDragGhost; impl Render for SliderDragGhost { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().w(px(0.0)).h(px(0.0)) - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(0.0)).h(px(0.0)) + } } /// A single form-control slider. pub struct Slider { - /// Stable id used for element ids, events and keying requests. - control: usize, - /// The pure state machine. - model: SliderModel, - /// Injectable formatting/parsing. - formatter: Box, - /// The keying state reported at the current frame. - keying: KeyingState, - /// For arrow-key navigation. - focus_handle: FocusHandle, - /// Active direct-entry editor, if any. - editing: Option>, - /// Focus-out subscription of the active editor, kept alive while editing. - edit_subscription: Option, - /// Set by the escape path so a later focus-lost does not commit. - edit_cancelled: bool, - /// True while the user is dragging (so we can report drag start/finish). - dragging: bool, + /// Stable id used for element ids, events and keying requests. + control: usize, + /// The pure state machine. + model: SliderModel, + /// Injectable formatting/parsing. + formatter: Box, + /// The keying state reported at the current frame. + keying: KeyingState, + /// For arrow-key navigation. + focus_handle: FocusHandle, + /// Active direct-entry editor, if any. + editing: Option>, + /// Focus-out subscription of the active editor, kept alive while editing. + edit_subscription: Option, + /// Set by the escape path so a later focus-lost does not commit. + edit_cancelled: bool, + /// True while the user is dragging (so we can report drag start/finish). + dragging: bool, } impl Slider { - /// Create a slider for `control` over `model`. - pub fn new( - control: usize, - model: SliderModel, - _window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - control, - model, - formatter: Box::new(crate::value::DefaultFormatter), - keying: KeyingState::NoKey, - focus_handle: cx.focus_handle(), - editing: None, - edit_subscription: None, - edit_cancelled: false, - dragging: false, - } - } + /// Create a slider for `control` over `model`. + pub fn new( + control: usize, + model: SliderModel, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + control, + model, + formatter: Box::new(crate::value::DefaultFormatter), + keying: KeyingState::NoKey, + focus_handle: cx.focus_handle(), + editing: None, + edit_subscription: None, + edit_cancelled: false, + dragging: false, + } + } - /// Inject a custom formatter/parser for display and direct entry. - pub fn with_formatter(mut self, formatter: impl ValueFormatter) -> Self { - self.formatter = Box::new(formatter); - self - } + /// Inject a custom formatter/parser for display and direct entry. + pub fn with_formatter(mut self, formatter: impl ValueFormatter) -> Self { + self.formatter = Box::new(formatter); + self + } - /// Report the control's keying state for the current frame. - pub fn with_keying(mut self, state: KeyingState) -> Self { - self.keying = state; - self - } + /// Report the control's keying state for the current frame. + pub fn with_keying(mut self, state: KeyingState) -> Self { + self.keying = state; + self + } - /// The current value. - pub fn value(&self) -> SliderValue { - self.model.value() - } + /// The current value. + pub fn value(&self) -> SliderValue { + self.model.value() + } - /// A copy of the pure model state. - pub fn model(&self) -> SliderModel { - self.model - } + /// A copy of the pure model state. + pub fn model(&self) -> SliderModel { + self.model + } - /// Whether direct entry is currently active. - pub fn is_editing(&self) -> bool { - self.editing.is_some() - } + /// Whether direct entry is currently active. + pub fn is_editing(&self) -> bool { + self.editing.is_some() + } - /// The active editor entity, if direct entry is open. - /// - /// Hosts and tests can use this to seed or inspect the text being - /// edited (e.g. `editor.update(cx, |editor, cx| editor.emplace("0.7", cx))`). - pub fn editor(&self) -> Option> { - self.editing.clone() - } + /// The active editor entity, if direct entry is open. + /// + /// Hosts and tests can use this to seed or inspect the text being + /// edited (e.g. `editor.update(cx, |editor, cx| editor.emplace("0.7", cx))`). + pub fn editor(&self) -> Option> { + self.editing.clone() + } - /// Set the value directly (clamped and snapped by the model). - pub fn set_value(&mut self, value: SliderValue) { - self.model.set_value(value); - } + /// Set the value directly (clamped and snapped by the model). + pub fn set_value(&mut self, value: SliderValue) { + self.model.set_value(value); + } - /// Emit `ValueChanged` if `changed` is true and repaint. - fn apply_and_notify(&mut self, changed: bool, cx: &mut Context) { - if changed { - cx.emit(SliderEvent::ValueChanged { - control: self.control, - value: self.model.value(), - }); - cx.notify(); - } - } + /// Emit `ValueChanged` if `changed` is true and repaint. + fn apply_and_notify(&mut self, changed: bool, cx: &mut Context) { + if changed { + cx.emit(SliderEvent::ValueChanged { + control: self.control, + value: self.model.value(), + }); + cx.notify(); + } + } - /// Begin double-click direct entry. - fn begin_edit(&mut self, window: &mut Window, cx: &mut Context) { - if self.editing.is_some() { - return; - } - let text = self.formatter.format(self.model.value()); - let editor = cx.new(|cx| EditableTextState::new(StringStorage::from(text.to_string()), cx)); - let focus_handle = editor.read(cx).focus_handle(cx); - let subscription = cx.on_focus_out(&focus_handle, window, |this, _event, _window, cx| { - // Clicking away commits what was typed, unless escape already - // cancelled the session. - if this.editing.is_some() { - if this.edit_cancelled { - this.cancel_edit(cx); - } else { - this.commit_edit(cx); - } - } - }); - self.edit_cancelled = false; - self.edit_subscription = Some(subscription); - self.editing = Some(editor.clone()); - window.focus(&focus_handle, cx); - cx.notify(); - } + /// Begin double-click direct entry. + fn begin_edit(&mut self, window: &mut Window, cx: &mut Context) { + if self.editing.is_some() { + return; + } + let text = self.formatter.format(self.model.value()); + let editor = cx.new(|cx| EditableTextState::new(StringStorage::from(text.to_string()), cx)); + let focus_handle = editor.read(cx).focus_handle(cx); + let subscription = cx.on_focus_out(&focus_handle, window, |this, _event, _window, cx| { + // Clicking away commits what was typed, unless escape already + // cancelled the session. + if this.editing.is_some() { + if this.edit_cancelled { + this.cancel_edit(cx); + } else { + this.commit_edit(cx); + } + } + }); + self.edit_cancelled = false; + self.edit_subscription = Some(subscription); + self.editing = Some(editor.clone()); + window.focus(&focus_handle, cx); + cx.notify(); + } - /// Commit the current editor text. Invalid text is rejected (the - /// previous value is kept) and the editor closes either way. - fn commit_edit(&mut self, cx: &mut Context) { - if let Some(editor) = self.editing.take() { - self.edit_subscription = None; - let text = editor.read(cx).as_str().to_string(); - match self.formatter.parse(&text) { - Ok(value) => { - let changed = self.model.set_value(value); - self.apply_and_notify(changed, cx); - cx.emit(SliderEvent::EditCommitted { - control: self.control, - value: self.model.value(), - }); - } - Err(_) => { - cx.emit(SliderEvent::EditCancelled { - control: self.control, - }); - } - } - cx.notify(); - } - } + /// Commit the current editor text. Invalid text is rejected (the + /// previous value is kept) and the editor closes either way. + fn commit_edit(&mut self, cx: &mut Context) { + if let Some(editor) = self.editing.take() { + self.edit_subscription = None; + let text = editor.read(cx).as_str().to_string(); + match self.formatter.parse(&text) { + Ok(value) => { + let changed = self.model.set_value(value); + self.apply_and_notify(changed, cx); + cx.emit(SliderEvent::EditCommitted { + control: self.control, + value: self.model.value(), + }); + } + Err(_) => { + cx.emit(SliderEvent::EditCancelled { + control: self.control, + }); + } + } + cx.notify(); + } + } - /// Cancel direct entry, keeping the previous value. - fn cancel_edit(&mut self, cx: &mut Context) { - if self.editing.take().is_some() { - self.edit_subscription = None; - cx.emit(SliderEvent::EditCancelled { - control: self.control, - }); - cx.notify(); - } - } + /// Cancel direct entry, keeping the previous value. + fn cancel_edit(&mut self, cx: &mut Context) { + if self.editing.take().is_some() { + self.edit_subscription = None; + cx.emit(SliderEvent::EditCancelled { + control: self.control, + }); + cx.notify(); + } + } - /// Apply a wheel step. - fn apply_scroll(&mut self, event: &ScrollWheelEvent, cx: &mut Context) { - let delta = event.delta.pixel_delta(px(16.0)); - let steps = (f32::from(delta.y) / 16.0).round() as i32; - if steps == 0 { - return; - } - let fine = event.modifiers.shift; - let changed = self.model.apply_step(steps, fine); - self.apply_and_notify(changed, cx); - } + /// Apply a wheel step. + fn apply_scroll(&mut self, event: &ScrollWheelEvent, cx: &mut Context) { + let delta = event.delta.pixel_delta(px(16.0)); + let steps = (f32::from(delta.y) / 16.0).round() as i32; + if steps == 0 { + return; + } + let fine = event.modifiers.shift; + let changed = self.model.apply_step(steps, fine); + self.apply_and_notify(changed, cx); + } } impl EventEmitter for Slider {} impl Focusable for Slider { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for Slider { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let fraction = self.model.fraction(); - let drag_payload = Arc::new(RwLock::new(SliderDrag { start_y: 0.0 })); - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let fraction = self.model.fraction(); + let drag_payload = Arc::new(RwLock::new(SliderDrag { start_y: 0.0 })); + let control = self.control; - let mut track = div() - .id(ElementId::named_usize("gpui-widgets-slider-track", control)) - .flex_1() - .h(px(18.0)) - .relative() - .rounded_md() - .bg(colors.container) - .track_focus(&self.focus_handle) - .cursor_pointer() - .debug_selector(|| "slider-track".into()) - .on_drag(drag_payload.clone(), slider_drag_ghost) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - let drag = event.drag(cx).clone(); - let drag = drag.read().unwrap(); - let dy = f32::from(event.event.position.y) - drag.start_y; - let fine = event.event.modifiers.shift; - let changed = this.model.drag_delta(dy, VERTICAL_DRAG_RANGE_PX, fine); - this.apply_and_notify(changed, cx); - }, - ), - ) - .on_drag_move( - cx.listener( - |this, event: &DragMoveEvent>>, _window, cx| { - if !this.dragging { - this.dragging = true; - cx.emit(SliderEvent::DragStarted { - control: this.control, - }); - } - let _ = event; - cx.notify(); - }, - ), - ) - .on_drop(cx.listener(|this, _drag: &Arc>, _window, cx| { - if this.dragging { - this.dragging = false; - cx.emit(SliderEvent::DragFinished { - control: this.control, - }); - } - })) - .on_click(cx.listener(|this, event: &ClickEvent, window, cx| { - if event.click_count() >= 2 { - this.begin_edit(window, cx); - } - })) - .on_mouse_down( - MouseButton::Middle, - cx.listener(|this, _event: &MouseDownEvent, _window, cx| { - let changed = this.model.reset(); - this.apply_and_notify(changed, cx); - }), - ) - .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { - this.apply_scroll(event, cx); - })) - .on_key_down( - cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "left" | "down" => { - let changed = - this.model.apply_step(-1, event.keystroke.modifiers.shift); - this.apply_and_notify(changed, cx); - } - "right" | "up" => { - let changed = this.model.apply_step(1, event.keystroke.modifiers.shift); - this.apply_and_notify(changed, cx); - } - _ => {} - } - }), - ) - .child(canvas( - move |bounds, _window, _cx| bounds, - move |bounds, content, window, cx| { - paint_slider(bounds, content, fraction, window, cx); - }, - )); + let mut track = div() + .id(ElementId::named_usize("gpui-widgets-slider-track", control)) + .flex_1() + .h(px(18.0)) + .relative() + .rounded_md() + .bg(colors.container) + .track_focus(&self.focus_handle) + .cursor_pointer() + .debug_selector(|| "slider-track".into()) + .on_drag(drag_payload.clone(), slider_drag_ghost) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + let drag = event.drag(cx).clone(); + let drag = drag.read().unwrap(); + let dy = f32::from(event.event.position.y) - drag.start_y; + let fine = event.event.modifiers.shift; + let changed = this.model.drag_delta(dy, VERTICAL_DRAG_RANGE_PX, fine); + this.apply_and_notify(changed, cx); + }, + )) + .on_drag_move(cx.listener( + |this, event: &DragMoveEvent>>, _window, cx| { + if !this.dragging { + this.dragging = true; + cx.emit(SliderEvent::DragStarted { + control: this.control, + }); + } + let _ = event; + cx.notify(); + }, + )) + .on_drop( + cx.listener(|this, _drag: &Arc>, _window, cx| { + if this.dragging { + this.dragging = false; + cx.emit(SliderEvent::DragFinished { + control: this.control, + }); + } + }), + ) + .on_click(cx.listener(|this, event: &ClickEvent, window, cx| { + if event.click_count() >= 2 { + this.begin_edit(window, cx); + } + })) + .on_mouse_down( + MouseButton::Middle, + cx.listener(|this, _event: &MouseDownEvent, _window, cx| { + let changed = this.model.reset(); + this.apply_and_notify(changed, cx); + }), + ) + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { + this.apply_scroll(event, cx); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "left" | "down" => { + let changed = this.model.apply_step(-1, event.keystroke.modifiers.shift); + this.apply_and_notify(changed, cx); + } + "right" | "up" => { + let changed = this.model.apply_step(1, event.keystroke.modifiers.shift); + this.apply_and_notify(changed, cx); + } + _ => {} + } + })) + .child(canvas( + move |bounds, _window, _cx| bounds, + move |bounds, content, window, cx| { + paint_slider(bounds, content, fraction, window, cx); + }, + )); - // Direct-entry editor overlay while editing. - if let Some(editor) = self.editing.clone() { - let weak = editor.downgrade(); - track = track - .child( - div() - .absolute() - .left_0() - .right_0() - .top_0() - .bottom_0() - .flex() - .items_center() - .px_1() - .bg(colors.background) - .on_mouse_down_out( - cx.listener(|this, _event: &MouseDownEvent, _window, cx| { - // Clicking outside the editor commits. - this.commit_edit(cx); - }), - ) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "enter" => this.commit_edit(cx), - "escape" => { - this.edit_cancelled = true; - this.cancel_edit(cx); - } - _ => {} - } - })) - .child( - text_input(ElementId::named_usize( - "gpui-widgets-slider-edit-input", - control, - )) - .state(weak) - .accepts_input(true), - ), - ); - } + // Direct-entry editor overlay while editing. + if let Some(editor) = self.editing.clone() { + let weak = editor.downgrade(); + track = track.child( + div() + .absolute() + .left_0() + .right_0() + .top_0() + .bottom_0() + .flex() + .items_center() + .px_1() + .bg(colors.background) + .on_mouse_down_out(cx.listener(|this, _event: &MouseDownEvent, _window, cx| { + // Clicking outside the editor commits. + this.commit_edit(cx); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "enter" => this.commit_edit(cx), + "escape" => { + this.edit_cancelled = true; + this.cancel_edit(cx); + } + _ => {} + } + })) + .child( + text_input(ElementId::named_usize( + "gpui-widgets-slider-edit-input", + control, + )) + .state(weak) + .accepts_input(true), + ), + ); + } - div() - .w_full() - .flex() - .items_center() - .gap(px(6.0)) - .child(track) - .child( - keying_diamond(self.control, self.keying).on_click( - cx.listener(|this, _event: &ClickEvent, _window, cx| { - cx.emit(SliderEvent::Keying(keying_request( - this.control, - this.keying, - ))); - cx.stop_propagation(); - }), - ), - ) - } + div() + .w_full() + .flex() + .items_center() + .gap(px(6.0)) + .child(track) + .child( + keying_diamond(self.control, self.keying).on_click(cx.listener( + |this, _event: &ClickEvent, _window, cx| { + cx.emit(SliderEvent::Keying(keying_request( + this.control, + this.keying, + ))); + cx.stop_propagation(); + }, + )), + ) + } } /// Build the (invisible) ghost view that accompanies a slider drag. fn slider_drag_ghost( - drag: &Arc>, - _offset: Point, - window: &mut Window, - cx: &mut App, + drag: &Arc>, + _offset: Point, + window: &mut Window, + cx: &mut App, ) -> Entity { - if let Ok(mut drag) = drag.write() { - drag.start_y = f32::from(window.mouse_position().y); - } - cx.new(|_| SliderDragGhost) + if let Ok(mut drag) = drag.write() { + drag.start_y = f32::from(window.mouse_position().y); + } + cx.new(|_| SliderDragGhost) } fn paint_slider( - bounds: Bounds, - _content: Bounds, - fraction: f64, - window: &mut Window, - cx: &mut App, + bounds: Bounds, + _content: Bounds, + fraction: f64, + window: &mut Window, + cx: &mut App, ) { - use gpui::Hsla; - let colors = cx.default_colors().clone(); - let center_y = px(f32::from(bounds.top()) + f32::from(bounds.size.height) / 2.0); + use gpui::Hsla; + let colors = cx.default_colors().clone(); + let center_y = px(f32::from(bounds.top()) + f32::from(bounds.size.height) / 2.0); - // Track. - let track_bounds = Bounds::new( - point(bounds.left(), center_y - px(2.0)), - size(bounds.size.width, px(4.0)), - ); - window.paint_quad(fill(track_bounds, Hsla::from(colors.border))); + // Track. + let track_bounds = Bounds::new( + point(bounds.left(), center_y - px(2.0)), + size(bounds.size.width, px(4.0)), + ); + window.paint_quad(fill(track_bounds, Hsla::from(colors.border))); - // Active fill up to the fraction. - let width = f32::from(bounds.size.width); - let fill_width = (width * fraction as f32).clamp(0.0, width); - if fill_width > 0.0 { - let fill_bounds = Bounds::new( - point(bounds.left(), track_bounds.top()), - size(px(fill_width), track_bounds.size.height), - ); - window.paint_quad(fill(fill_bounds, Hsla::from(colors.selected))); - } + // Active fill up to the fraction. + let width = f32::from(bounds.size.width); + let fill_width = (width * fraction as f32).clamp(0.0, width); + if fill_width > 0.0 { + let fill_bounds = Bounds::new( + point(bounds.left(), track_bounds.top()), + size(px(fill_width), track_bounds.size.height), + ); + window.paint_quad(fill(fill_bounds, Hsla::from(colors.selected))); + } - // Handle: a small circle at the fraction position. - let handle_radius = 5.0_f32; - let handle_bounds = Bounds::new( - point(bounds.left() + px(fill_width - handle_radius), center_y - px(handle_radius)), - size(px(handle_radius * 2.0), px(handle_radius * 2.0)), - ); - window.paint_quad(quad( - handle_bounds, - Corners::all(px(handle_radius)), - Hsla::from(colors.text), - Edges::all(px(1.0)), - Hsla::from(colors.border), - BorderStyle::Solid, - )); + // Handle: a small circle at the fraction position. + let handle_radius = 5.0_f32; + let handle_bounds = Bounds::new( + point( + bounds.left() + px(fill_width - handle_radius), + center_y - px(handle_radius), + ), + size(px(handle_radius * 2.0), px(handle_radius * 2.0)), + ); + window.paint_quad(quad( + handle_bounds, + Corners::all(px(handle_radius)), + Hsla::from(colors.text), + Edges::all(px(1.0)), + Hsla::from(colors.border), + BorderStyle::Solid, + )); } #[cfg(test)] mod tests { - use super::*; - use crate::value::ValueKind; - use gpui::{Modifiers, MouseUpEvent, ScrollDelta, TestAppContext, VisualTestContext}; + use super::*; + use crate::value::ValueKind; + use gpui::{Modifiers, MouseUpEvent, ScrollDelta, TestAppContext, VisualTestContext}; - #[derive(Clone)] - struct Host { - slider: Entity, - events: Vec, - } + #[derive(Clone)] + struct Host { + slider: Entity, + events: Vec, + } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .flex_col() - .justify_center() - .child(self.slider.clone()) - } - } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .flex_col() + .justify_center() + .child(self.slider.clone()) + } + } - fn make_host(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { - // `default_colors()` (used by the widgets) requires the global to be - // initialized; the test platform does not do it automatically. - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(400.0), px(100.0)), |window, cx| { - let slider = cx.new(|cx| { - Slider::new( - 1, - SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 0.5), - window, - cx, - ) - }); - let host = Host { - slider, - events: Vec::new(), - }; - cx.subscribe( - &host.slider, - |host: &mut Host, - _slider: Entity, - event: &SliderEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - (cx, host) - } + fn make_host(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { + // `default_colors()` (used by the widgets) requires the global to be + // initialized; the test platform does not do it automatically. + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(400.0), px(100.0)), |window, cx| { + let slider = cx.new(|cx| { + Slider::new( + 1, + SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 0.5), + window, + cx, + ) + }); + let host = Host { + slider, + events: Vec::new(), + }; + cx.subscribe( + &host.slider, + |host: &mut Host, + _slider: Entity, + event: &SliderEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + (cx, host) + } - fn value_events(events: &[SliderEvent]) -> Vec { - events - .iter() - .filter_map(|event| match event { - SliderEvent::ValueChanged { value, .. } => Some(value.to_f64()), - _ => None, - }) - .collect() - } + fn value_events(events: &[SliderEvent]) -> Vec { + events + .iter() + .filter_map(|event| match event { + SliderEvent::ValueChanged { value, .. } => Some(value.to_f64()), + _ => None, + }) + .collect() + } - fn double_click(cx: &mut VisualTestContext, position: Point) { - let modifiers = Modifiers::none(); - cx.simulate_event(MouseDownEvent { - position, - modifiers, - button: MouseButton::Left, - click_count: 2, - first_mouse: false, - }); - cx.simulate_event(MouseUpEvent { - position, - modifiers, - button: MouseButton::Left, - click_count: 2, - }); - cx.run_until_parked(); - // Render the editor overlay so its input handler is registered. - redraw(cx); - } + fn double_click(cx: &mut VisualTestContext, position: Point) { + let modifiers = Modifiers::none(); + cx.simulate_event(MouseDownEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 2, + first_mouse: false, + }); + cx.simulate_event(MouseUpEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 2, + }); + cx.run_until_parked(); + // Render the editor overlay so its input handler is registered. + redraw(cx); + } - /// A point comfortably inside the track (row is vertically centered). - fn track_point() -> Point { - point(px(200.0), px(50.0)) - } + /// A point comfortably inside the track (row is vertically centered). + fn track_point() -> Point { + point(px(200.0), px(50.0)) + } - /// The test platform does not redraw dirty windows on its own, so state - /// changes (which only call `notify`) do not repaint until we force a - /// draw. This is required for freshly-created elements (e.g. the editor - /// overlay) to register their listeners and input handlers. - fn redraw(cx: &mut VisualTestContext) { - cx.update(|window, cx| { - window.draw(cx).clear(); - }); - } + /// The test platform does not redraw dirty windows on its own, so state + /// changes (which only call `notify`) do not repaint until we force a + /// draw. This is required for freshly-created elements (e.g. the editor + /// overlay) to register their listeners and input handlers. + fn redraw(cx: &mut VisualTestContext) { + cx.update(|window, cx| { + window.draw(cx).clear(); + }); + } - /// Compare a slider value to an expected float with tolerance (stepping - /// through non-binary steps like 0.1 accumulates representation error). - fn assert_value_close(actual: SliderValue, expected: f64) { - assert!( - (actual.to_f64() - expected).abs() < 1e-6, - "expected {expected}, got {actual:?}" - ); - } + /// Compare a slider value to an expected float with tolerance (stepping + /// through non-binary steps like 0.1 accumulates representation error). + fn assert_value_close(actual: SliderValue, expected: f64) { + assert!( + (actual.to_f64() - expected).abs() < 1e-6, + "expected {expected}, got {actual:?}" + ); + } - #[gpui::test] - async fn track_hitbox_is_rendered(cx: &mut TestAppContext) { - let (cx, _host) = make_host(cx); - let bounds = cx.debug_bounds("slider-track"); - assert!( - bounds.is_some(), - "slider track was not painted into the rendered frame" - ); - if let Some(bounds) = bounds { - eprintln!("track bounds: {bounds:?}, track point: {:?}", track_point()); - assert!(bounds.contains(&track_point())); - } - } + #[gpui::test] + async fn track_hitbox_is_rendered(cx: &mut TestAppContext) { + let (cx, _host) = make_host(cx); + let bounds = cx.debug_bounds("slider-track"); + assert!( + bounds.is_some(), + "slider track was not painted into the rendered frame" + ); + if let Some(bounds) = bounds { + eprintln!("track bounds: {bounds:?}, track point: {:?}", track_point()); + assert!(bounds.contains(&track_point())); + } + } - /// Seed the active editor with `text` (the test platform cannot deliver - /// IME characters, so tests set the text directly and exercise the - /// commit/cancel paths). - fn seed_editor_text(cx: &mut VisualTestContext, host: &Entity, text: &str) { - let editor = cx - .read(|app| host.read(app).slider.read(app).editor()) - .expect("editor should be open"); - // `cx.update` on VisualTestContext takes a window closure; use the - // underlying TestAppContext to get at the app directly. - cx.cx.update(|app| { - editor.update(app, |editor, cx| editor.emplace(text, cx)); - }); - } + /// Seed the active editor with `text` (the test platform cannot deliver + /// IME characters, so tests set the text directly and exercise the + /// commit/cancel paths). + fn seed_editor_text(cx: &mut VisualTestContext, host: &Entity, text: &str) { + let editor = cx + .read(|app| host.read(app).slider.read(app).editor()) + .expect("editor should be open"); + // `cx.update` on VisualTestContext takes a window closure; use the + // underlying TestAppContext to get at the app directly. + cx.cx.update(|app| { + editor.update(app, |editor, cx| editor.emplace(text, cx)); + }); + } - #[gpui::test] - async fn double_click_opens_editor_and_enter_commits(cx: &mut TestAppContext) { - let (mut cx, host) = make_host(cx); - double_click(&mut cx, track_point()); - assert!(cx.read(|app| host.read(app).slider.read(app).is_editing())); + #[gpui::test] + async fn double_click_opens_editor_and_enter_commits(cx: &mut TestAppContext) { + let (mut cx, host) = make_host(cx); + double_click(&mut cx, track_point()); + assert!(cx.read(|app| host.read(app).slider.read(app).is_editing())); - seed_editor_text(&mut cx, &host, "0.7"); - cx.simulate_keystrokes("enter"); - cx.run_until_parked(); + seed_editor_text(&mut cx, &host, "0.7"); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); - let (value, is_editing, committed) = cx.read(|app| { + let (value, is_editing, committed) = cx.read(|app| { let host = host.read(app); ( host.slider.read(app).value(), @@ -638,178 +633,182 @@ mod tests { }), ) }); - assert!(!is_editing); - assert_value_close(value, 0.7); - assert!(committed); - } + assert!(!is_editing); + assert_value_close(value, 0.7); + assert!(committed); + } - #[gpui::test] - async fn double_click_rejects_invalid_text(cx: &mut TestAppContext) { - let (mut cx, host) = make_host(cx); - double_click(&mut cx, track_point()); - seed_editor_text(&mut cx, &host, "abc"); - cx.simulate_keystrokes("enter"); - cx.run_until_parked(); + #[gpui::test] + async fn double_click_rejects_invalid_text(cx: &mut TestAppContext) { + let (mut cx, host) = make_host(cx); + double_click(&mut cx, track_point()); + seed_editor_text(&mut cx, &host, "abc"); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); - let (value, is_editing, cancelled) = cx.read(|app| { - let host = host.read(app); - ( - host.slider.read(app).value(), - host.slider.read(app).is_editing(), - host.events - .iter() - .any(|e| matches!(e, SliderEvent::EditCancelled { .. })), - ) - }); - assert!(!is_editing); - // The previous value is kept. - assert_eq!(value, SliderValue::Float(0.5)); - assert!(cancelled); - } + let (value, is_editing, cancelled) = cx.read(|app| { + let host = host.read(app); + ( + host.slider.read(app).value(), + host.slider.read(app).is_editing(), + host.events + .iter() + .any(|e| matches!(e, SliderEvent::EditCancelled { .. })), + ) + }); + assert!(!is_editing); + // The previous value is kept. + assert_eq!(value, SliderValue::Float(0.5)); + assert!(cancelled); + } - #[gpui::test] - async fn escape_cancels_editing(cx: &mut TestAppContext) { - let (mut cx, host) = make_host(cx); - double_click(&mut cx, track_point()); - cx.simulate_keystrokes("escape"); - cx.run_until_parked(); + #[gpui::test] + async fn escape_cancels_editing(cx: &mut TestAppContext) { + let (mut cx, host) = make_host(cx); + double_click(&mut cx, track_point()); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); - let (value, is_editing, cancelled) = cx.read(|app| { - let host = host.read(app); - ( - host.slider.read(app).value(), - host.slider.read(app).is_editing(), - host.events - .iter() - .any(|e| matches!(e, SliderEvent::EditCancelled { .. })), - ) - }); - assert!(!is_editing); - assert_eq!(value, SliderValue::Float(0.5)); - assert!(cancelled); - } + let (value, is_editing, cancelled) = cx.read(|app| { + let host = host.read(app); + ( + host.slider.read(app).value(), + host.slider.read(app).is_editing(), + host.events + .iter() + .any(|e| matches!(e, SliderEvent::EditCancelled { .. })), + ) + }); + assert!(!is_editing); + assert_eq!(value, SliderValue::Float(0.5)); + assert!(cancelled); + } - #[gpui::test] - async fn middle_click_resets_to_default(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // Move the value first. - cx.simulate_event(ScrollWheelEvent { - position: track_point(), - delta: ScrollDelta::Pixels(point(px(0.0), px(-80.0))), - ..Default::default() - }); - cx.run_until_parked(); - { - let moved = cx.read(|app| host.read(app).slider.read(app).value().to_f64()); - assert!((moved - 0.5).abs() > 0.01, "value should have moved, got {moved}"); - } + #[gpui::test] + async fn middle_click_resets_to_default(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // Move the value first. + cx.simulate_event(ScrollWheelEvent { + position: track_point(), + delta: ScrollDelta::Pixels(point(px(0.0), px(-80.0))), + ..Default::default() + }); + cx.run_until_parked(); + { + let moved = cx.read(|app| host.read(app).slider.read(app).value().to_f64()); + assert!( + (moved - 0.5).abs() > 0.01, + "value should have moved, got {moved}" + ); + } - // Middle-click resets. - cx.simulate_mouse_down(track_point(), MouseButton::Middle, Modifiers::none()); - cx.simulate_mouse_up(track_point(), MouseButton::Middle, Modifiers::none()); - cx.run_until_parked(); + // Middle-click resets. + cx.simulate_mouse_down(track_point(), MouseButton::Middle, Modifiers::none()); + cx.simulate_mouse_up(track_point(), MouseButton::Middle, Modifiers::none()); + cx.run_until_parked(); - let (value, events) = cx.read(|app| { - let host = host.read(app); - (host.slider.read(app).value(), host.events.clone()) - }); - assert_eq!(value, SliderValue::Float(0.5)); - assert!(value_events(&events).contains(&0.5)); - } + let (value, events) = cx.read(|app| { + let host = host.read(app); + (host.slider.read(app).value(), host.events.clone()) + }); + assert_eq!(value, SliderValue::Float(0.5)); + assert!(value_events(&events).contains(&0.5)); + } - #[gpui::test] - async fn wheel_steps_value(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // Scrolling up (positive y) increases the value. - cx.simulate_event(ScrollWheelEvent { - position: track_point(), - delta: ScrollDelta::Pixels(point(px(0.0), px(80.0))), - ..Default::default() - }); - cx.run_until_parked(); + #[gpui::test] + async fn wheel_steps_value(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // Scrolling up (positive y) increases the value. + cx.simulate_event(ScrollWheelEvent { + position: track_point(), + delta: ScrollDelta::Pixels(point(px(0.0), px(80.0))), + ..Default::default() + }); + cx.run_until_parked(); - let (value, events) = cx.read(|app| { - let host = host.read(app); - (host.slider.read(app).value(), host.events.clone()) - }); - // 80px at 16px per notch = 5 coarse steps of 0.1, clamped to max. - assert_value_close(value, 1.0); - assert!(!value_events(&events).is_empty()); - } + let (value, events) = cx.read(|app| { + let host = host.read(app); + (host.slider.read(app).value(), host.events.clone()) + }); + // 80px at 16px per notch = 5 coarse steps of 0.1, clamped to max. + assert_value_close(value, 1.0); + assert!(!value_events(&events).is_empty()); + } - #[gpui::test] - async fn vertical_drag_changes_value(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // Press on the track, then drag up. The first move past the drag - // threshold anchors the gesture; the second move applies the delta. - let modifiers = Modifiers::none(); - cx.simulate_mouse_down(track_point(), MouseButton::Left, modifiers); - cx.simulate_mouse_move(point(px(200.0), px(45.0)), MouseButton::Left, modifiers); - cx.simulate_mouse_move(point(px(200.0), px(25.0)), MouseButton::Left, modifiers); - // Release inside the track so the drop listener on the slider fires. - cx.simulate_mouse_up(point(px(200.0), px(45.0)), MouseButton::Left, modifiers); - cx.run_until_parked(); + #[gpui::test] + async fn vertical_drag_changes_value(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // Press on the track, then drag up. The first move past the drag + // threshold anchors the gesture; the second move applies the delta. + let modifiers = Modifiers::none(); + cx.simulate_mouse_down(track_point(), MouseButton::Left, modifiers); + cx.simulate_mouse_move(point(px(200.0), px(45.0)), MouseButton::Left, modifiers); + cx.simulate_mouse_move(point(px(200.0), px(25.0)), MouseButton::Left, modifiers); + // Release inside the track so the drop listener on the slider fires. + cx.simulate_mouse_up(point(px(200.0), px(45.0)), MouseButton::Left, modifiers); + cx.run_until_parked(); - let (value, started, finished) = cx.read(|app| { - let host = host.read(app); - ( - host.slider.read(app).value().to_f64(), - host.events - .iter() - .any(|e| matches!(e, SliderEvent::DragStarted { .. })), - host.events - .iter() - .any(|e| matches!(e, SliderEvent::DragFinished { .. })), - ) - }); - // 20px / 100px of range = +0.2 over the default 0.5. - assert!((value - 0.7).abs() < 0.001, "expected ~0.7, got {value}"); - assert!(started); - assert!(finished); - } + let (value, started, finished) = cx.read(|app| { + let host = host.read(app); + ( + host.slider.read(app).value().to_f64(), + host.events + .iter() + .any(|e| matches!(e, SliderEvent::DragStarted { .. })), + host.events + .iter() + .any(|e| matches!(e, SliderEvent::DragFinished { .. })), + ) + }); + // 20px / 100px of range = +0.2 over the default 0.5. + assert!((value - 0.7).abs() < 0.001, "expected ~0.7, got {value}"); + assert!(started); + assert!(finished); + } - #[gpui::test] - async fn focused_slider_steps_with_arrow_keys(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // Click once to focus the slider (click_count 1 is a no-op for value). - cx.simulate_click(track_point(), Modifiers::none()); - cx.simulate_keystrokes("right"); - cx.run_until_parked(); + #[gpui::test] + async fn focused_slider_steps_with_arrow_keys(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // Click once to focus the slider (click_count 1 is a no-op for value). + cx.simulate_click(track_point(), Modifiers::none()); + cx.simulate_keystrokes("right"); + cx.run_until_parked(); - let (value, events) = cx.read(|app| { - let host = host.read(app); - (host.slider.read(app).value(), host.events.clone()) - }); - assert_value_close(value, 0.6); - assert!(value_events(&events).iter().any(|v| (v - 0.6).abs() < 1e-6)); - } + let (value, events) = cx.read(|app| { + let host = host.read(app); + (host.slider.read(app).value(), host.events.clone()) + }); + assert_value_close(value, 0.6); + assert!(value_events(&events).iter().any(|v| (v - 0.6).abs() < 1e-6)); + } - #[gpui::test] - async fn keying_diamond_emits_request(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // The diamond sits at the right edge of the row. - cx.simulate_click(point(px(390.0), px(50.0)), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn keying_diamond_emits_request(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // The diamond sits at the right edge of the row. + cx.simulate_click(point(px(390.0), px(50.0)), Modifiers::none()); + cx.run_until_parked(); - let requested = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, SliderEvent::Keying(request) if request.control == 1) - }) - }); - assert!(requested); - } + let requested = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, SliderEvent::Keying(request) if request.control == 1)) + }); + assert!(requested); + } - #[gpui::test] - async fn clicking_away_commits_editor(cx: &mut TestAppContext) { - let (mut cx, host) = make_host(cx); - double_click(&mut cx, track_point()); - // 0.3 is exactly representable at the slider's 0.1 step. - seed_editor_text(&mut cx, &host, "0.3"); - // Clicking outside the editor overlay commits the typed value. - cx.simulate_click(point(px(200.0), px(90.0)), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn clicking_away_commits_editor(cx: &mut TestAppContext) { + let (mut cx, host) = make_host(cx); + double_click(&mut cx, track_point()); + // 0.3 is exactly representable at the slider's 0.1 step. + seed_editor_text(&mut cx, &host, "0.3"); + // Clicking outside the editor overlay commits the typed value. + cx.simulate_click(point(px(200.0), px(90.0)), Modifiers::none()); + cx.run_until_parked(); - let (value, is_editing, committed) = cx.read(|app| { + let (value, is_editing, committed) = cx.read(|app| { let host = host.read(app); ( host.slider.read(app).value(), @@ -819,8 +818,8 @@ mod tests { }), ) }); - assert!(!is_editing); - assert_value_close(value, 0.3); - assert!(committed); - } + assert!(!is_editing); + assert_value_close(value, 0.3); + assert!(committed); + } } diff --git a/crates/gpui_widgets/src/slider/model.rs b/crates/gpui_widgets/src/slider/model.rs index 59b9443234..6a19ff505f 100644 --- a/crates/gpui_widgets/src/slider/model.rs +++ b/crates/gpui_widgets/src/slider/model.rs @@ -14,374 +14,387 @@ use crate::value::{RationalValue, SliderValue, ValueKind}; /// floating point. #[derive(Debug, Clone, Copy, PartialEq)] pub struct SliderModel { - /// The family of values this slider edits. - pub kind: ValueKind, - /// Canonical numeric position (for `Rational`: the numerator). - pub raw: f64, - /// Inclusive lower bound of `raw`. - pub min: f64, - /// Inclusive upper bound of `raw`. - pub max: f64, - /// Coarse increment (wheel notch / arrow key). - pub step: f64, - /// Fine increment used with a fine-adjust modifier. - pub fine_step: f64, - /// Value restored by a middle-click reset. - pub default_raw: f64, - /// Denominator for `Rational` values (unused otherwise). - pub rational_den: i64, + /// The family of values this slider edits. + pub kind: ValueKind, + /// Canonical numeric position (for `Rational`: the numerator). + pub raw: f64, + /// Inclusive lower bound of `raw`. + pub min: f64, + /// Inclusive upper bound of `raw`. + pub max: f64, + /// Coarse increment (wheel notch / arrow key). + pub step: f64, + /// Fine increment used with a fine-adjust modifier. + pub fine_step: f64, + /// Value restored by a middle-click reset. + pub default_raw: f64, + /// Denominator for `Rational` values (unused otherwise). + pub rational_den: i64, } impl SliderModel { - /// Create a slider over `[min, max]` with `step` increments. - /// - /// For `Rational` sliders `min`, `max`, `step` and `default_raw` are in - /// numerator units; pair with [`Self::with_rational_den`]. - pub fn new(kind: ValueKind, min: f64, max: f64, step: f64, default_raw: f64) -> Self { - let raw = default_raw.clamp(min, max); - Self { - kind, - raw, - min, - max, - step, - fine_step: step / 10.0, - default_raw: default_raw.clamp(min, max), - rational_den: 1, - } - } + /// Create a slider over `[min, max]` with `step` increments. + /// + /// For `Rational` sliders `min`, `max`, `step` and `default_raw` are in + /// numerator units; pair with [`Self::with_rational_den`]. + pub fn new(kind: ValueKind, min: f64, max: f64, step: f64, default_raw: f64) -> Self { + let raw = default_raw.clamp(min, max); + Self { + kind, + raw, + min, + max, + step, + fine_step: step / 10.0, + default_raw: default_raw.clamp(min, max), + rational_den: 1, + } + } - /// Set the denominator of a `Rational` slider (a no-op otherwise). - pub fn with_rational_den(mut self, den: i64) -> Self { - if self.kind == ValueKind::Rational { - self.rational_den = den.max(1); - } - self - } + /// Set the denominator of a `Rational` slider (a no-op otherwise). + pub fn with_rational_den(mut self, den: i64) -> Self { + if self.kind == ValueKind::Rational { + self.rational_den = den.max(1); + } + self + } - /// Override the fine-adjust increment. - pub fn with_fine_step(mut self, fine_step: f64) -> Self { - self.fine_step = fine_step; - self - } + /// Override the fine-adjust increment. + pub fn with_fine_step(mut self, fine_step: f64) -> Self { + self.fine_step = fine_step; + self + } - /// The current value. - pub fn value(&self) -> SliderValue { - match self.kind { - ValueKind::Float => SliderValue::Float(self.raw), - ValueKind::Integer => SliderValue::Integer(self.raw as i64), - ValueKind::Rational => { - SliderValue::Rational(RationalValue::new_or_one(self.raw as i64, self.rational_den)) - } - ValueKind::Angle => SliderValue::Angle(self.raw), - } - } + /// The current value. + pub fn value(&self) -> SliderValue { + match self.kind { + ValueKind::Float => SliderValue::Float(self.raw), + ValueKind::Integer => SliderValue::Integer(self.raw as i64), + ValueKind::Rational => SliderValue::Rational(RationalValue::new_or_one( + self.raw as i64, + self.rational_den, + )), + ValueKind::Angle => SliderValue::Angle(self.raw), + } + } - /// True if `v` is within `[min, max]` (and finite). - pub fn in_range(&self, v: f64) -> bool { - v.is_finite() && v >= self.min && v <= self.max - } + /// True if `v` is within `[min, max]` (and finite). + pub fn in_range(&self, v: f64) -> bool { + v.is_finite() && v >= self.min && v <= self.max + } - /// Clamp a raw position to `[min, max]`. - pub fn clamp(&self, raw: f64) -> f64 { - raw.clamp(self.min, self.max) - } + /// Clamp a raw position to `[min, max]`. + pub fn clamp(&self, raw: f64) -> f64 { + raw.clamp(self.min, self.max) + } - /// Snap a raw position to the nearest multiple of `step`, measured from - /// `min`. Positions outside the range snap back to the bounds. - fn snap_with_step(&self, raw: f64, step: f64) -> f64 { - let raw = self.clamp(raw); - if step <= 0.0 || self.spanned() == 0.0 { - return raw; - } - let steps = ((raw - self.min) / step).round(); - self.clamp(self.min + steps * step) - } + /// Snap a raw position to the nearest multiple of `step`, measured from + /// `min`. Positions outside the range snap back to the bounds. + fn snap_with_step(&self, raw: f64, step: f64) -> f64 { + let raw = self.clamp(raw); + if step <= 0.0 || self.spanned() == 0.0 { + return raw; + } + let steps = ((raw - self.min) / step).round(); + self.clamp(self.min + steps * step) + } - /// Snap a raw position to the nearest coarse step, measured from `min`. - pub fn snap(&self, raw: f64) -> f64 { - self.snap_with_step(raw, self.step) - } + /// Snap a raw position to the nearest coarse step, measured from `min`. + pub fn snap(&self, raw: f64) -> f64 { + self.snap_with_step(raw, self.step) + } - /// The width of the range (`max - min`, never negative). - pub fn spanned(&self) -> f64 { - (self.max - self.min).max(0.0) - } + /// The width of the range (`max - min`, never negative). + pub fn spanned(&self) -> f64 { + (self.max - self.min).max(0.0) + } - /// Set the position from a raw value: clamps and snaps to `step`, - /// returns whether the position changed. - pub fn apply_with_step(&mut self, raw: f64, step: f64) -> bool { - if !raw.is_finite() { - return false; - } - let snapped = self.snap_with_step(raw, step); - if (snapped - self.raw).abs() > f64::EPSILON { - self.raw = snapped; - true - } else { - false - } - } + /// Set the position from a raw value: clamps and snaps to `step`, + /// returns whether the position changed. + pub fn apply_with_step(&mut self, raw: f64, step: f64) -> bool { + if !raw.is_finite() { + return false; + } + let snapped = self.snap_with_step(raw, step); + if (snapped - self.raw).abs() > f64::EPSILON { + self.raw = snapped; + true + } else { + false + } + } - /// Set the position from a raw value: clamps and snaps to the coarse - /// step, returns whether the position changed. - pub fn apply_raw(&mut self, raw: f64) -> bool { - self.apply_with_step(raw, self.step) - } + /// Set the position from a raw value: clamps and snaps to the coarse + /// step, returns whether the position changed. + pub fn apply_raw(&mut self, raw: f64) -> bool { + self.apply_with_step(raw, self.step) + } - /// Set the value, converting between representations where possible. - /// Returns whether the position changed. Non-finite input is rejected. - pub fn set_value(&mut self, value: SliderValue) -> bool { - let finite = match value { - SliderValue::Float(v) | SliderValue::Angle(v) => v.is_finite(), - SliderValue::Integer(_) | SliderValue::Rational(_) => true, - }; - if !finite { - return false; - } - let raw = match (self.kind, value) { - (ValueKind::Float, SliderValue::Float(v)) - | (ValueKind::Angle, SliderValue::Angle(v)) => v, - (ValueKind::Integer, SliderValue::Integer(v)) => v as f64, - (ValueKind::Rational, SliderValue::Rational(v)) => { - (v.num() as f64) * (self.rational_den as f64) / (v.den() as f64) - } - // Cross-kind conversions go through the numeric projection. - (kind, value) => { - let v = value.to_f64(); - match kind { - ValueKind::Integer => v.trunc(), - ValueKind::Rational => { - (v * self.rational_den as f64).round() / self.rational_den as f64 - } - _ => v, - } - } - }; - self.apply_raw(raw) - } + /// Set the value, converting between representations where possible. + /// Returns whether the position changed. Non-finite input is rejected. + pub fn set_value(&mut self, value: SliderValue) -> bool { + let finite = match value { + SliderValue::Float(v) | SliderValue::Angle(v) => v.is_finite(), + SliderValue::Integer(_) | SliderValue::Rational(_) => true, + }; + if !finite { + return false; + } + let raw = match (self.kind, value) { + (ValueKind::Float, SliderValue::Float(v)) + | (ValueKind::Angle, SliderValue::Angle(v)) => v, + (ValueKind::Integer, SliderValue::Integer(v)) => v as f64, + (ValueKind::Rational, SliderValue::Rational(v)) => { + (v.num() as f64) * (self.rational_den as f64) / (v.den() as f64) + } + // Cross-kind conversions go through the numeric projection. + (kind, value) => { + let v = value.to_f64(); + match kind { + ValueKind::Integer => v.trunc(), + ValueKind::Rational => { + (v * self.rational_den as f64).round() / self.rational_den as f64 + } + _ => v, + } + } + }; + self.apply_raw(raw) + } - /// The next position `delta` coarse (or fine) steps away from `from`, - /// clamped to the range. - pub fn step_from(&self, from: f64, delta: i32, fine: bool) -> f64 { - let step = if fine { self.fine_step } else { self.step }; - if step <= 0.0 { - return self.clamp(from); - } - self.clamp(from + delta as f64 * step) - } + /// The next position `delta` coarse (or fine) steps away from `from`, + /// clamped to the range. + pub fn step_from(&self, from: f64, delta: i32, fine: bool) -> f64 { + let step = if fine { self.fine_step } else { self.step }; + if step <= 0.0 { + return self.clamp(from); + } + self.clamp(from + delta as f64 * step) + } - /// Step the current position by `delta` steps, snapping to the effective - /// (coarse or fine) step. - pub fn apply_step(&mut self, delta: i32, fine: bool) -> bool { - let step = if fine { self.fine_step } else { self.step }; - self.apply_with_step(self.step_from(self.raw, delta, fine), step) - } + /// Step the current position by `delta` steps, snapping to the effective + /// (coarse or fine) step. + pub fn apply_step(&mut self, delta: i32, fine: bool) -> bool { + let step = if fine { self.fine_step } else { self.step }; + self.apply_with_step(self.step_from(self.raw, delta, fine), step) + } - /// Reset to the default position; returns whether it changed. - pub fn reset(&mut self) -> bool { - let snapped = self.snap(self.default_raw); - if (snapped - self.raw).abs() > f64::EPSILON { - self.raw = snapped; - true - } else { - false - } - } + /// Reset to the default position; returns whether it changed. + pub fn reset(&mut self) -> bool { + let snapped = self.snap(self.default_raw); + if (snapped - self.raw).abs() > f64::EPSILON { + self.raw = snapped; + true + } else { + false + } + } - /// Whether the current position equals the (snapped) default. - pub fn is_at_default(&self) -> bool { - (self.snap(self.default_raw) - self.raw).abs() <= f64::EPSILON - } + /// Whether the current position equals the (snapped) default. + pub fn is_at_default(&self) -> bool { + (self.snap(self.default_raw) - self.raw).abs() <= f64::EPSILON + } - /// Normalized position in `0..=1` for painting. Returns `0.5` when the - /// range is empty so the handle never leaves the track. - pub fn fraction(&self) -> f64 { - if self.spanned() == 0.0 { - return 0.5; - } - ((self.raw - self.min) / self.spanned()).clamp(0.0, 1.0) - } + /// Normalized position in `0..=1` for painting. Returns `0.5` when the + /// range is empty so the handle never leaves the track. + pub fn fraction(&self) -> f64 { + if self.spanned() == 0.0 { + return 0.5; + } + ((self.raw - self.min) / self.spanned()).clamp(0.0, 1.0) + } - /// Set the position from a normalized `0..=1` fraction of the range. - pub fn set_fraction(&mut self, t: f64) -> bool { - if !t.is_finite() { - return false; - } - let raw = self.min + t.clamp(0.0, 1.0) * self.spanned(); - self.apply_raw(raw) - } + /// Set the position from a normalized `0..=1` fraction of the range. + pub fn set_fraction(&mut self, t: f64) -> bool { + if !t.is_finite() { + return false; + } + let raw = self.min + t.clamp(0.0, 1.0) * self.spanned(); + self.apply_raw(raw) + } - /// Apply a vertical drag of `dy_px` pixels over a track that spans - /// `range_px` pixels. Dragging up (`dy_px < 0`) increases the value. - /// `fine` scales the motion by `1/10` and snaps to the fine step. - pub fn drag_delta(&mut self, dy_px: f32, range_px: f32, fine: bool) -> bool { - if range_px <= 0.0 { - return false; - } - let scale = if fine { 0.1 } else { 1.0 }; - let step = if fine { self.fine_step } else { self.step }; - let delta = -(dy_px as f64) / range_px as f64 * self.spanned() * scale; - self.apply_with_step(self.raw + delta, step) - } + /// Apply a vertical drag of `dy_px` pixels over a track that spans + /// `range_px` pixels. Dragging up (`dy_px < 0`) increases the value. + /// `fine` scales the motion by `1/10` and snaps to the fine step. + pub fn drag_delta(&mut self, dy_px: f32, range_px: f32, fine: bool) -> bool { + if range_px <= 0.0 { + return false; + } + let scale = if fine { 0.1 } else { 1.0 }; + let step = if fine { self.fine_step } else { self.step }; + let delta = -(dy_px as f64) / range_px as f64 * self.spanned() * scale; + self.apply_with_step(self.raw + delta, step) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn float_slider() -> SliderModel { - SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 0.5) - } + fn float_slider() -> SliderModel { + SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 0.5) + } - #[test] - fn new_clamps_default() { - let model = SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 5.0); - assert_eq!(model.raw, 1.0); - assert!(model.is_at_default()); - } + #[test] + fn new_clamps_default() { + let model = SliderModel::new(ValueKind::Float, 0.0, 1.0, 0.1, 5.0); + assert_eq!(model.raw, 1.0); + assert!(model.is_at_default()); + } - #[test] - fn clamps_to_range() { - let model = float_slider(); - assert_eq!(model.clamp(-2.0), 0.0); - assert_eq!(model.clamp(2.0), 1.0); - assert!(model.in_range(0.5)); - assert!(!model.in_range(1.5)); - assert!(!model.in_range(f64::NAN)); - } + #[test] + fn clamps_to_range() { + let model = float_slider(); + assert_eq!(model.clamp(-2.0), 0.0); + assert_eq!(model.clamp(2.0), 1.0); + assert!(model.in_range(0.5)); + assert!(!model.in_range(1.5)); + assert!(!model.in_range(f64::NAN)); + } - #[test] - fn snaps_to_step_from_min() { - let model = float_slider(); - assert!((model.snap(0.27) - 0.3).abs() < 1e-9); - assert!((model.snap(0.24) - 0.2).abs() < 1e-9); - // Off-range values snap back to the bounds. - assert!((model.snap(-5.0) - 0.0).abs() < 1e-9); - assert!((model.snap(5.0) - 1.0).abs() < 1e-9); - } + #[test] + fn snaps_to_step_from_min() { + let model = float_slider(); + assert!((model.snap(0.27) - 0.3).abs() < 1e-9); + assert!((model.snap(0.24) - 0.2).abs() < 1e-9); + // Off-range values snap back to the bounds. + assert!((model.snap(-5.0) - 0.0).abs() < 1e-9); + assert!((model.snap(5.0) - 1.0).abs() < 1e-9); + } - #[test] - fn snaps_relative_to_nonzero_min() { - let model = SliderModel::new(ValueKind::Float, 0.5, 2.5, 0.5, 1.0); - // (1.15 - 0.5) / 0.5 = 1.3 -> round to 1 -> 0.5 + 0.5 = 1.0 - assert!((model.snap(1.15) - 1.0).abs() < 1e-9); - // (1.35 - 0.5) / 0.5 = 1.7 -> round to 2 -> 1.5 - assert!((model.snap(1.35) - 1.5).abs() < 1e-9); - } + #[test] + fn snaps_relative_to_nonzero_min() { + let model = SliderModel::new(ValueKind::Float, 0.5, 2.5, 0.5, 1.0); + // (1.15 - 0.5) / 0.5 = 1.3 -> round to 1 -> 0.5 + 0.5 = 1.0 + assert!((model.snap(1.15) - 1.0).abs() < 1e-9); + // (1.35 - 0.5) / 0.5 = 1.7 -> round to 2 -> 1.5 + assert!((model.snap(1.35) - 1.5).abs() < 1e-9); + } - #[test] - fn apply_raw_rejects_nan() { - let mut model = float_slider(); - assert!(!model.apply_raw(f64::NAN)); - assert_eq!(model.raw, 0.5); - } + #[test] + fn apply_raw_rejects_nan() { + let mut model = float_slider(); + assert!(!model.apply_raw(f64::NAN)); + assert_eq!(model.raw, 0.5); + } - #[test] - fn step_from_clamps() { - let model = float_slider(); - assert!((model.step_from(0.95, 1, false) - 1.0).abs() < 1e-9); - assert!((model.step_from(0.05, -1, false) - 0.0).abs() < 1e-9); - assert!((model.step_from(0.5, 1, false) - 0.6).abs() < 1e-9); - // Fine step is 1/10 of the coarse step. - assert!((model.step_from(0.5, 1, true) - 0.51).abs() < 1e-9); - } + #[test] + fn step_from_clamps() { + let model = float_slider(); + assert!((model.step_from(0.95, 1, false) - 1.0).abs() < 1e-9); + assert!((model.step_from(0.05, -1, false) - 0.0).abs() < 1e-9); + assert!((model.step_from(0.5, 1, false) - 0.6).abs() < 1e-9); + // Fine step is 1/10 of the coarse step. + assert!((model.step_from(0.5, 1, true) - 0.51).abs() < 1e-9); + } - #[test] - fn reset_restores_default() { - let mut model = float_slider(); - model.apply_raw(0.9); - assert!(!model.is_at_default()); - assert!(model.reset()); - assert_eq!(model.raw, 0.5); - assert!(model.is_at_default()); - } + #[test] + fn reset_restores_default() { + let mut model = float_slider(); + model.apply_raw(0.9); + assert!(!model.is_at_default()); + assert!(model.reset()); + assert_eq!(model.raw, 0.5); + assert!(model.is_at_default()); + } - #[test] - fn fraction_and_set_fraction_are_inverse() { - let mut model = float_slider(); - assert_eq!(model.fraction(), 0.5); - model.set_fraction(0.0); - assert_eq!(model.raw, 0.0); - model.set_fraction(1.0); - assert_eq!(model.raw, 1.0); - // Positions are quantized to the step: 0.25 lands on the nearest - // representable value (0.3 with a 0.1 step). - model.set_fraction(0.25); - assert!((model.raw - 0.3).abs() < 1e-9); - assert!((model.fraction() - 0.3).abs() < 1e-9); - } + #[test] + fn fraction_and_set_fraction_are_inverse() { + let mut model = float_slider(); + assert_eq!(model.fraction(), 0.5); + model.set_fraction(0.0); + assert_eq!(model.raw, 0.0); + model.set_fraction(1.0); + assert_eq!(model.raw, 1.0); + // Positions are quantized to the step: 0.25 lands on the nearest + // representable value (0.3 with a 0.1 step). + model.set_fraction(0.25); + assert!((model.raw - 0.3).abs() < 1e-9); + assert!((model.fraction() - 0.3).abs() < 1e-9); + } - #[test] - fn fraction_when_range_is_empty() { - let model = SliderModel::new(ValueKind::Float, 1.0, 1.0, 0.1, 1.0); - assert_eq!(model.fraction(), 0.5); - } + #[test] + fn fraction_when_range_is_empty() { + let model = SliderModel::new(ValueKind::Float, 1.0, 1.0, 0.1, 1.0); + assert_eq!(model.fraction(), 0.5); + } - #[test] - fn drag_up_increases_value() { - let mut model = float_slider(); - // 10px up over a 100px track spans 10% of the range. - assert!(model.drag_delta(-10.0, 100.0, false)); - assert!((model.raw - 0.6).abs() < 1e-9); - // Fine drag moves 10x less. - let mut fine = float_slider(); - fine.drag_delta(-10.0, 100.0, true); - assert!((fine.raw - 0.51).abs() < 1e-9); - // Dragging up past the max clamps. - let mut model = float_slider(); - model.drag_delta(-1000.0, 100.0, false); - assert_eq!(model.raw, 1.0); - } + #[test] + fn drag_up_increases_value() { + let mut model = float_slider(); + // 10px up over a 100px track spans 10% of the range. + assert!(model.drag_delta(-10.0, 100.0, false)); + assert!((model.raw - 0.6).abs() < 1e-9); + // Fine drag moves 10x less. + let mut fine = float_slider(); + fine.drag_delta(-10.0, 100.0, true); + assert!((fine.raw - 0.51).abs() < 1e-9); + // Dragging up past the max clamps. + let mut model = float_slider(); + model.drag_delta(-1000.0, 100.0, false); + assert_eq!(model.raw, 1.0); + } - #[test] - fn integer_kind_round_trips_exactly() { - let mut model = SliderModel::new(ValueKind::Integer, -10.0, 10.0, 1.0, 0.0); - assert_eq!(model.value(), SliderValue::Integer(0)); - model.set_value(SliderValue::Integer(5)); - assert_eq!(model.value(), SliderValue::Integer(5)); - model.apply_step(1, false); - assert_eq!(model.value(), SliderValue::Integer(6)); - // A float value converts by truncation. - model.set_value(SliderValue::Float(3.9)); - assert_eq!(model.value(), SliderValue::Integer(3)); - } + #[test] + fn integer_kind_round_trips_exactly() { + let mut model = SliderModel::new(ValueKind::Integer, -10.0, 10.0, 1.0, 0.0); + assert_eq!(model.value(), SliderValue::Integer(0)); + model.set_value(SliderValue::Integer(5)); + assert_eq!(model.value(), SliderValue::Integer(5)); + model.apply_step(1, false); + assert_eq!(model.value(), SliderValue::Integer(6)); + // A float value converts by truncation. + model.set_value(SliderValue::Float(3.9)); + assert_eq!(model.value(), SliderValue::Integer(3)); + } - #[test] - fn rational_kind_stays_exact() { - let mut model = SliderModel::new(ValueKind::Rational, 0.0, 24.0, 1.0, 0.0) - .with_rational_den(24); - assert_eq!(model.value(), SliderValue::Rational(RationalValue::new(0, 1).unwrap())); - model.apply_raw(1.0); - assert_eq!(model.value(), SliderValue::Rational(RationalValue::new(1, 24).unwrap())); - model.apply_raw(24.0); - assert_eq!(model.value(), SliderValue::Rational(RationalValue::new(1, 1).unwrap())); - // Setting a rational with a different denominator rescales. - model.set_value(SliderValue::Rational(RationalValue::new(1, 2).unwrap())); - assert_eq!(model.value(), SliderValue::Rational(RationalValue::new(1, 2).unwrap())); - } + #[test] + fn rational_kind_stays_exact() { + let mut model = + SliderModel::new(ValueKind::Rational, 0.0, 24.0, 1.0, 0.0).with_rational_den(24); + assert_eq!( + model.value(), + SliderValue::Rational(RationalValue::new(0, 1).unwrap()) + ); + model.apply_raw(1.0); + assert_eq!( + model.value(), + SliderValue::Rational(RationalValue::new(1, 24).unwrap()) + ); + model.apply_raw(24.0); + assert_eq!( + model.value(), + SliderValue::Rational(RationalValue::new(1, 1).unwrap()) + ); + // Setting a rational with a different denominator rescales. + model.set_value(SliderValue::Rational(RationalValue::new(1, 2).unwrap())); + assert_eq!( + model.value(), + SliderValue::Rational(RationalValue::new(1, 2).unwrap()) + ); + } - #[test] - fn angle_kind_round_trips() { - let mut model = SliderModel::new(ValueKind::Angle, 0.0, 360.0, 0.5, 45.0); - assert_eq!(model.value(), SliderValue::Angle(45.0)); - model.set_value(SliderValue::Angle(270.5)); - assert_eq!(model.value(), SliderValue::Angle(270.5)); - } + #[test] + fn angle_kind_round_trips() { + let mut model = SliderModel::new(ValueKind::Angle, 0.0, 360.0, 0.5, 45.0); + assert_eq!(model.value(), SliderValue::Angle(45.0)); + model.set_value(SliderValue::Angle(270.5)); + assert_eq!(model.value(), SliderValue::Angle(270.5)); + } - #[test] - fn set_value_rejects_non_finite() { - let mut model = float_slider(); - assert!(!model.set_value(SliderValue::Float(f64::NAN))); - assert!(!model.set_value(SliderValue::Angle(f64::INFINITY))); - assert_eq!(model.raw, 0.5); - } + #[test] + fn set_value_rejects_non_finite() { + let mut model = float_slider(); + assert!(!model.set_value(SliderValue::Float(f64::NAN))); + assert!(!model.set_value(SliderValue::Angle(f64::INFINITY))); + assert_eq!(model.raw, 0.5); + } - #[test] - fn integer_fine_step_is_whole() { - let model = SliderModel::new(ValueKind::Integer, 0.0, 10.0, 1.0, 5.0); - // Fine step for integers is 1/10 by default; snapping keeps it whole. - let snapped = model.snap(model.step_from(5.0, 1, true)); - assert_eq!(snapped, 5.0); - } + #[test] + fn integer_fine_step_is_whole() { + let model = SliderModel::new(ValueKind::Integer, 0.0, 10.0, 1.0, 5.0); + // Fine step for integers is 1/10 by default; snapping keeps it whole. + let snapped = model.snap(model.step_from(5.0, 1, true)); + assert_eq!(snapped, 5.0); + } } diff --git a/crates/gpui_widgets/src/spinbox.rs b/crates/gpui_widgets/src/spinbox.rs index 16dc425152..91cbe5bf7b 100644 --- a/crates/gpui_widgets/src/spinbox.rs +++ b/crates/gpui_widgets/src/spinbox.rs @@ -6,8 +6,8 @@ //! the value. Every change is emitted as [`SpinBoxEvent::ValueChanged`]. use gpui::{ - App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, KeyDownEvent, - Render, Window, colors::DefaultColors, div, prelude::*, px, + App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, + KeyDownEvent, Render, Window, colors::DefaultColors, div, prelude::*, px, }; use gpui_elements::editable_text::{EditableTextState, StringStorage, text_input}; @@ -17,295 +17,302 @@ use crate::value::{DefaultFormatter, SliderValue, ValueFormatter}; /// A request emitted by a spinbox. #[derive(Debug, Clone, PartialEq)] pub enum SpinBoxEvent { - /// The value changed (commit, button, wheel or arrow key). - ValueChanged { - /// The control's stable id. - control: usize, - /// The new value. - value: SliderValue, - }, - /// Direct text entry was committed. - EditCommitted { - /// The control's stable id. - control: usize, - /// The accepted value. - value: SliderValue, - }, - /// Direct text entry was cancelled (invalid input kept). - EditCancelled { - /// The control's stable id. - control: usize, - }, + /// The value changed (commit, button, wheel or arrow key). + ValueChanged { + /// The control's stable id. + control: usize, + /// The new value. + value: SliderValue, + }, + /// Direct text entry was committed. + EditCommitted { + /// The control's stable id. + control: usize, + /// The accepted value. + value: SliderValue, + }, + /// Direct text entry was cancelled (invalid input kept). + EditCancelled { + /// The control's stable id. + control: usize, + }, } /// A numeric spinbox control. pub struct SpinBox { - control: usize, - model: SliderModel, - formatter: Box, - editor: Entity, - focus_handle: FocusHandle, - /// Keeps the commit-on-blur listener alive for the widget's lifetime. - _commit_subscription: gpui::Subscription, + control: usize, + model: SliderModel, + formatter: Box, + editor: Entity, + focus_handle: FocusHandle, + /// Keeps the commit-on-blur listener alive for the widget's lifetime. + _commit_subscription: gpui::Subscription, } impl SpinBox { - /// Create a spinbox for `control` over `model`. - pub fn new( - control: usize, - model: SliderModel, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let formatter = Box::new(DefaultFormatter); - let text = formatter.format(model.value()); - let editor = cx.new(|cx| EditableTextState::new(StringStorage::from(text.to_string()), cx)); - let focus_handle = editor.read(cx).focus_handle(cx); - let _commit_subscription = - cx.on_focus_out(&focus_handle, window, |this, _event, _window, cx| { - this.commit_edit(cx); - }); - Self { - control, - model, - formatter, - editor, - focus_handle, - _commit_subscription, - } - } + /// Create a spinbox for `control` over `model`. + pub fn new( + control: usize, + model: SliderModel, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let formatter = Box::new(DefaultFormatter); + let text = formatter.format(model.value()); + let editor = cx.new(|cx| EditableTextState::new(StringStorage::from(text.to_string()), cx)); + let focus_handle = editor.read(cx).focus_handle(cx); + let _commit_subscription = + cx.on_focus_out(&focus_handle, window, |this, _event, _window, cx| { + this.commit_edit(cx); + }); + Self { + control, + model, + formatter, + editor, + focus_handle, + _commit_subscription, + } + } - /// Inject a custom formatter/parser. - pub fn with_formatter(mut self, formatter: impl ValueFormatter) -> Self { - self.formatter = Box::new(formatter); - self - } + /// Inject a custom formatter/parser. + pub fn with_formatter(mut self, formatter: impl ValueFormatter) -> Self { + self.formatter = Box::new(formatter); + self + } - /// The current value. - pub fn value(&self) -> SliderValue { - self.model.value() - } + /// The current value. + pub fn value(&self) -> SliderValue { + self.model.value() + } - /// Apply a value from the host and refresh the displayed text. - pub fn set_value(&mut self, value: SliderValue, cx: &mut Context) { - self.model.set_value(value); - self.sync_text(cx); - cx.notify(); - } + /// Apply a value from the host and refresh the displayed text. + pub fn set_value(&mut self, value: SliderValue, cx: &mut Context) { + self.model.set_value(value); + self.sync_text(cx); + cx.notify(); + } - /// Repaint the editor with the formatted value (only when the field is - /// not being edited, so typing is never clobbered). - fn sync_text(&self, cx: &mut Context) { - let text = self.formatter.format(self.model.value()); - self.editor.update(cx, |editor, cx| { - editor.emplace(text.as_ref(), cx); - }); - } + /// Repaint the editor with the formatted value (only when the field is + /// not being edited, so typing is never clobbered). + fn sync_text(&self, cx: &mut Context) { + let text = self.formatter.format(self.model.value()); + self.editor.update(cx, |editor, cx| { + editor.emplace(text.as_ref(), cx); + }); + } - fn apply_and_notify(&mut self, changed: bool, cx: &mut Context) { - if changed { - self.sync_text(cx); - cx.emit(SpinBoxEvent::ValueChanged { - control: self.control, - value: self.model.value(), - }); - cx.notify(); - } - } + fn apply_and_notify(&mut self, changed: bool, cx: &mut Context) { + if changed { + self.sync_text(cx); + cx.emit(SpinBoxEvent::ValueChanged { + control: self.control, + value: self.model.value(), + }); + cx.notify(); + } + } - fn step(&mut self, delta: i32, fine: bool, cx: &mut Context) { - let changed = self.model.apply_step(delta, fine); - self.apply_and_notify(changed, cx); - } + fn step(&mut self, delta: i32, fine: bool, cx: &mut Context) { + let changed = self.model.apply_step(delta, fine); + self.apply_and_notify(changed, cx); + } - fn commit_edit(&mut self, cx: &mut Context) { - let text = self.editor.read(cx).as_str().to_string(); - match self.formatter.parse(&text) { - Ok(value) => { - let changed = self.model.set_value(value); - if changed { - self.apply_and_notify(true, cx); - } - cx.emit(SpinBoxEvent::EditCommitted { - control: self.control, - value: self.model.value(), - }); - self.sync_text(cx); - } - Err(_) => { - cx.emit(SpinBoxEvent::EditCancelled { - control: self.control, - }); - self.sync_text(cx); - } - } - cx.notify(); - } + fn commit_edit(&mut self, cx: &mut Context) { + let text = self.editor.read(cx).as_str().to_string(); + match self.formatter.parse(&text) { + Ok(value) => { + let changed = self.model.set_value(value); + if changed { + self.apply_and_notify(true, cx); + } + cx.emit(SpinBoxEvent::EditCommitted { + control: self.control, + value: self.model.value(), + }); + self.sync_text(cx); + } + Err(_) => { + cx.emit(SpinBoxEvent::EditCancelled { + control: self.control, + }); + self.sync_text(cx); + } + } + cx.notify(); + } } impl EventEmitter for SpinBox {} impl Focusable for SpinBox { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for SpinBox { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let weak = self.editor.downgrade(); - let control = self.control; + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let weak = self.editor.downgrade(); + let control = self.control; - div() - .flex() - .items_center() - .gap(px(2.0)) - .child( - div() - .id(ElementId::named_usize("gpui-widgets-spinbox-field", control)) - .min_w(px(64.0)) - .h(px(24.0)) - .rounded_md() - .border_1() - .border_color(colors.border) - .bg(colors.background) - .px_1() - .flex() - .items_center() - .track_focus(&self.focus_handle) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - match event.keystroke.key.as_str() { - "up" => this.step(1, event.keystroke.modifiers.shift, cx), - "down" => this.step(-1, event.keystroke.modifiers.shift, cx), - "enter" => this.commit_edit(cx), - "escape" => { - // Revert the displayed text without committing. - this.sync_text(cx); - cx.notify(); - } - _ => {} - } - })) - .child( - text_input(ElementId::named_usize( - "gpui-widgets-spinbox-input", - control, - )) - .state(weak) - .accepts_input(true), - ), - ) - .child( - div() - .flex() - .flex_col() - .child( - div() - .id(ElementId::named_usize("gpui-widgets-spinbox-up", control)) - .size(px(12.0)) - .cursor_pointer() - .child(arrow_element(true, colors.border)) - .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.step(1, false, cx); - cx.stop_propagation(); - })), - ) - .child( - div() - .id(ElementId::named_usize("gpui-widgets-spinbox-down", control)) - .size(px(12.0)) - .cursor_pointer() - .child(arrow_element(false, colors.border)) - .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.step(-1, false, cx); - cx.stop_propagation(); - })), - ), - ) - } + div() + .flex() + .items_center() + .gap(px(2.0)) + .child( + div() + .id(ElementId::named_usize( + "gpui-widgets-spinbox-field", + control, + )) + .min_w(px(64.0)) + .h(px(24.0)) + .rounded_md() + .border_1() + .border_color(colors.border) + .bg(colors.background) + .px_1() + .flex() + .items_center() + .track_focus(&self.focus_handle) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + match event.keystroke.key.as_str() { + "up" => this.step(1, event.keystroke.modifiers.shift, cx), + "down" => this.step(-1, event.keystroke.modifiers.shift, cx), + "enter" => this.commit_edit(cx), + "escape" => { + // Revert the displayed text without committing. + this.sync_text(cx); + cx.notify(); + } + _ => {} + } + })) + .child( + text_input(ElementId::named_usize( + "gpui-widgets-spinbox-input", + control, + )) + .state(weak) + .accepts_input(true), + ), + ) + .child( + div() + .flex() + .flex_col() + .child( + div() + .id(ElementId::named_usize("gpui-widgets-spinbox-up", control)) + .size(px(12.0)) + .cursor_pointer() + .child(arrow_element(true, colors.border)) + .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.step(1, false, cx); + cx.stop_propagation(); + })), + ) + .child( + div() + .id(ElementId::named_usize("gpui-widgets-spinbox-down", control)) + .size(px(12.0)) + .cursor_pointer() + .child(arrow_element(false, colors.border)) + .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.step(-1, false, cx); + cx.stop_propagation(); + })), + ), + ) + } } /// A tiny up (▲) or down (▼) arrow painted with a canvas. fn arrow_element(up: bool, color: gpui::Rgba) -> impl IntoElement { - use gpui::{canvas, point, px, Bounds, PathBuilder, Pixels}; + use gpui::{Bounds, PathBuilder, Pixels, canvas, point, px}; - canvas( - move |_bounds, _window, _cx| (), - move |bounds: Bounds, (), window, cx| { - let _ = cx; - let tip = if up { - point(bounds.center().x, bounds.top() + px(3.0)) - } else { - point(bounds.center().x, bounds.bottom() - px(3.0)) - }; - let base = if up { - (bounds.left() + px(3.0), bounds.bottom() - px(3.0)) - } else { - (bounds.left() + px(3.0), bounds.top() + px(3.0)) - }; - let mut path = PathBuilder::fill(); - path.move_to(tip); - path.line_to(point(base.0, base.1)); - path.line_to(point(bounds.right() - px(3.0), base.1)); - path.close(); - if let Ok(path) = path.build() { - window.paint_path(path, color); - } - }, - ) + canvas( + move |_bounds, _window, _cx| (), + move |bounds: Bounds, (), window, cx| { + let _ = cx; + let tip = if up { + point(bounds.center().x, bounds.top() + px(3.0)) + } else { + point(bounds.center().x, bounds.bottom() - px(3.0)) + }; + let base = if up { + (bounds.left() + px(3.0), bounds.bottom() - px(3.0)) + } else { + (bounds.left() + px(3.0), bounds.top() + px(3.0)) + }; + let mut path = PathBuilder::fill(); + path.move_to(tip); + path.line_to(point(base.0, base.1)); + path.line_to(point(bounds.right() - px(3.0), base.1)); + path.close(); + if let Ok(path) = path.build() { + window.paint_path(path, color); + } + }, + ) } #[cfg(test)] mod tests { - use super::*; - use crate::value::ValueKind; - use gpui::{Modifiers, TestAppContext, VisualTestContext, point, px, size}; + use super::*; + use crate::value::ValueKind; + use gpui::{Modifiers, TestAppContext, VisualTestContext, point, px, size}; - fn float_model() -> SliderModel { - SliderModel::new(ValueKind::Float, 0.0, 10.0, 1.0, 5.0) - } + fn float_model() -> SliderModel { + SliderModel::new(ValueKind::Float, 0.0, 10.0, 1.0, 5.0) + } - #[gpui::test] - async fn up_button_steps_value(cx: &mut TestAppContext) { - struct Host { - spinbox: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.spinbox.clone()) - } - } + #[gpui::test] + async fn up_button_steps_value(cx: &mut TestAppContext) { + struct Host { + spinbox: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.spinbox.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { - let spinbox = cx.new(|cx| SpinBox::new(1, float_model(), window, cx)); - let host = Host { - spinbox, - events: Vec::new(), - }; - cx.subscribe( - &host.spinbox, - |host: &mut Host, - _s: Entity, - event: &SpinBoxEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { + let spinbox = cx.new(|cx| SpinBox::new(1, float_model(), window, cx)); + let host = Host { + spinbox, + events: Vec::new(), + }; + cx.subscribe( + &host.spinbox, + |host: &mut Host, + _s: Entity, + event: &SpinBoxEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // The up button sits directly right of the field. - cx.simulate_click(point(px(70.0), px(6.0)), Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // The up button sits directly right of the field. + cx.simulate_click(point(px(70.0), px(6.0)), Modifiers::none()); + cx.run_until_parked(); - let (value, changed) = cx.read(|app| { + let (value, changed) = cx.read(|app| { let host = host.read(app); ( host.spinbox.read(app).value(), @@ -314,60 +321,67 @@ mod tests { }), ) }); - assert!((value.to_f64() - 6.0).abs() < 1e-9, "expected 6, got {value:?}"); - assert!(changed); - } + assert!( + (value.to_f64() - 6.0).abs() < 1e-9, + "expected 6, got {value:?}" + ); + assert!(changed); + } - #[gpui::test] - async fn enter_commits_typed_value(cx: &mut TestAppContext) { - struct Host { - spinbox: Entity, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.spinbox.clone()) - } - } + #[gpui::test] + async fn enter_commits_typed_value(cx: &mut TestAppContext) { + struct Host { + spinbox: Entity, + events: Vec, + } + impl Render for Host { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + div().size_full().child(self.spinbox.clone()) + } + } - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { - let spinbox = cx.new(|cx| SpinBox::new(1, float_model(), window, cx)); - let host = Host { - spinbox, - events: Vec::new(), - }; - cx.subscribe( - &host.spinbox, - |host: &mut Host, - _s: Entity, - event: &SpinBoxEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| { + let spinbox = cx.new(|cx| SpinBox::new(1, float_model(), window, cx)); + let host = Host { + spinbox, + events: Vec::new(), + }; + cx.subscribe( + &host.spinbox, + |host: &mut Host, + _s: Entity, + event: &SpinBoxEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - // Click the field to focus it, seed text programmatically (the test - // platform cannot deliver IME characters), then commit with enter. - cx.simulate_click(point(px(30.0), px(12.0)), Modifiers::none()); - cx.run_until_parked(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // Click the field to focus it, seed text programmatically (the test + // platform cannot deliver IME characters), then commit with enter. + cx.simulate_click(point(px(30.0), px(12.0)), Modifiers::none()); + cx.run_until_parked(); - let editor = cx - .read(|app| host.read(app).spinbox.read(app).editor.clone()) - .clone(); - cx.cx.update(|app| { - editor.update(app, |editor, cx| editor.emplace("7.5", cx)); - }); - cx.simulate_keystrokes("enter"); - cx.run_until_parked(); + let editor = cx + .read(|app| host.read(app).spinbox.read(app).editor.clone()) + .clone(); + cx.cx.update(|app| { + editor.update(app, |editor, cx| editor.emplace("7.5", cx)); + }); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); - let (value, committed) = cx.read(|app| { + let (value, committed) = cx.read(|app| { let host = host.read(app); ( host.spinbox.read(app).value(), @@ -376,8 +390,11 @@ mod tests { }), ) }); - // 7.5 snaps to the integer step... no: step is 1.0 so 7.5 -> 8. - assert!((value.to_f64() - 8.0).abs() < 1e-9, "expected 8, got {value:?}"); - assert!(committed); - } + // 7.5 snaps to the integer step... no: step is 1.0 so 7.5 -> 8. + assert!( + (value.to_f64() - 8.0).abs() < 1e-9, + "expected 8, got {value:?}" + ); + assert!(committed); + } } diff --git a/crates/gpui_widgets/src/theme.rs b/crates/gpui_widgets/src/theme.rs index bf4dca065b..8de2bafd10 100644 --- a/crates/gpui_widgets/src/theme.rs +++ b/crates/gpui_widgets/src/theme.rs @@ -14,93 +14,93 @@ use std::sync::Arc; /// Oak's olive theme palette. #[derive(Debug, Clone, PartialEq)] pub struct OakTheme { - /// The theme's display name. - pub name: gpui::SharedString, - /// Window background (panels, bars). - pub window: Rgba, - /// Base background (content areas, inputs). - pub base: Rgba, - /// Alternate base (stripes, separators). - pub alternate_base: Rgba, - /// Primary text. - pub text: Rgba, - /// Accent (selection, highlight). - pub accent: Rgba, - /// Text on the accent. - pub accent_text: Rgba, - /// Link color. - pub link: Rgba, - /// Disabled text. - pub disabled_text: Rgba, - /// Disabled button text. - pub disabled_button_text: Rgba, + /// The theme's display name. + pub name: gpui::SharedString, + /// Window background (panels, bars). + pub window: Rgba, + /// Base background (content areas, inputs). + pub base: Rgba, + /// Alternate base (stripes, separators). + pub alternate_base: Rgba, + /// Primary text. + pub text: Rgba, + /// Accent (selection, highlight). + pub accent: Rgba, + /// Text on the accent. + pub accent_text: Rgba, + /// Link color. + pub link: Rgba, + /// Disabled text. + pub disabled_text: Rgba, + /// Disabled button text. + pub disabled_button_text: Rgba, } fn rgba(hex: u32) -> Rgba { - let r = ((hex >> 16) & 0xff) as f32 / 255.0; - let g = ((hex >> 8) & 0xff) as f32 / 255.0; - let b = (hex & 0xff) as f32 / 255.0; - Rgba { r, g, b, a: 1.0 } + let r = ((hex >> 16) & 0xff) as f32 / 255.0; + let g = ((hex >> 8) & 0xff) as f32 / 255.0; + let b = (hex & 0xff) as f32 / 255.0; + Rgba { r, g, b, a: 1.0 } } impl OakTheme { - /// The olive-dark palette (oak's default). - pub fn olive_dark() -> Self { - Self { - name: "Olive Dark".into(), - window: rgba(0x353535), - base: rgba(0x191919), - alternate_base: rgba(0x353535), - text: rgb(0xffffff), - accent: rgba(0x2A82DA), - accent_text: rgb(0xffffff), - link: rgba(0xE0B040), - disabled_text: rgba(0xA0A0A0), - disabled_button_text: rgba(0x808080), - } - } + /// The olive-dark palette (oak's default). + pub fn olive_dark() -> Self { + Self { + name: "Olive Dark".into(), + window: rgba(0x353535), + base: rgba(0x191919), + alternate_base: rgba(0x353535), + text: rgb(0xffffff), + accent: rgba(0x2A82DA), + accent_text: rgb(0xffffff), + link: rgba(0xE0B040), + disabled_text: rgba(0xA0A0A0), + disabled_button_text: rgba(0x808080), + } + } - /// The olive-light palette. - pub fn olive_light() -> Self { - Self { - name: "Olive Light".into(), - window: rgba(0xD0D0D0), - base: rgba(0xF0F0F0), - alternate_base: rgba(0xD0D0D0), - text: rgb(0x000000), - accent: rgba(0x2A82DA), - accent_text: rgb(0xffffff), - link: rgba(0x2A82DA), - disabled_text: rgba(0x808080), - disabled_button_text: rgba(0x808080), - } - } + /// The olive-light palette. + pub fn olive_light() -> Self { + Self { + name: "Olive Light".into(), + window: rgba(0xD0D0D0), + base: rgba(0xF0F0F0), + alternate_base: rgba(0xD0D0D0), + text: rgb(0x000000), + accent: rgba(0x2A82DA), + accent_text: rgb(0xffffff), + link: rgba(0x2A82DA), + disabled_text: rgba(0x808080), + disabled_button_text: rgba(0x808080), + } + } - /// The border color derived from this theme (alternate base darkened for - /// dark themes, lightened for light themes). - pub fn border(&self) -> Rgba { - let factor = if relative_luminance(self.text) > relative_luminance(self.base) { - 0.7 - } else { - 1.25 - }; - scale_luma(self.alternate_base, factor) - } + /// The border color derived from this theme (alternate base darkened for + /// dark themes, lightened for light themes). + pub fn border(&self) -> Rgba { + let factor = if relative_luminance(self.text) > relative_luminance(self.base) { + 0.7 + } else { + 1.25 + }; + scale_luma(self.alternate_base, factor) + } - /// Map this theme onto gpui's [`Colors`](gpui::colors::Colors) struct so - /// `cx.default_colors()` picks it up. - pub fn colors(&self) -> gpui::colors::Colors { - gpui::colors::Colors { - text: self.text, - selected_text: self.accent_text, - background: self.base, - disabled: self.disabled_text, - selected: self.accent, - border: self.border(), - separator: self.alternate_base, - container: self.window, - } - } + /// Map this theme onto gpui's [`Colors`](gpui::colors::Colors) struct so + /// `cx.default_colors()` picks it up. + pub fn colors(&self) -> gpui::colors::Colors { + gpui::colors::Colors { + text: self.text, + selected_text: self.accent_text, + background: self.base, + disabled: self.disabled_text, + selected: self.accent, + border: self.border(), + separator: self.alternate_base, + container: self.window, + } + } } /// The current full theme, set by [`apply_theme`]. @@ -111,83 +111,83 @@ impl Global for ThemeGlobal {} /// Apply a theme: swaps the global [`Colors`](gpui::colors::Colors) (re-theming /// every widget that reads `cx.default_colors()`) and stores the full theme. pub fn apply_theme(cx: &mut App, theme: &OakTheme) { - cx.set_global(GlobalColors(Arc::new(theme.colors()))); - cx.set_global(ThemeGlobal(Arc::new(theme.clone()))); + cx.set_global(GlobalColors(Arc::new(theme.colors()))); + cx.set_global(ThemeGlobal(Arc::new(theme.clone()))); } /// The current theme, or olive-dark if none was applied. pub fn current_theme(cx: &App) -> Arc { - cx.try_global::() - .map(|global| global.0.clone()) - .unwrap_or_else(|| Arc::new(OakTheme::olive_dark())) + cx.try_global::() + .map(|global| global.0.clone()) + .unwrap_or_else(|| Arc::new(OakTheme::olive_dark())) } fn relative_luminance(color: Rgba) -> f32 { - // Simple perceptual approximation (sRGB -> luma). - 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b + // Simple perceptual approximation (sRGB -> luma). + 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b } fn scale_luma(color: Rgba, factor: f32) -> Rgba { - let scale = |channel: f32| (channel * factor).clamp(0.0, 1.0); - Rgba { - r: scale(color.r), - g: scale(color.g), - b: scale(color.b), - a: color.a, - } + let scale = |channel: f32| (channel * factor).clamp(0.0, 1.0); + Rgba { + r: scale(color.r), + g: scale(color.g), + b: scale(color.b), + a: color.a, + } } #[cfg(test)] mod tests { - use super::*; - use gpui::{TestAppContext, colors::DefaultColors}; + use super::*; + use gpui::{TestAppContext, colors::DefaultColors}; - #[test] - fn dark_theme_text_contrasts_with_base() { - let theme = OakTheme::olive_dark(); - let text_luma = relative_luminance(theme.text); - let base_luma = relative_luminance(theme.base); - // Text must be clearly brighter than the base. - assert!(text_luma - base_luma > 0.4, "dark theme lacks contrast"); - // The accent must be visible against both. - let accent_luma = relative_luminance(theme.accent); - assert!((accent_luma - base_luma).abs() > 0.1); - } + #[test] + fn dark_theme_text_contrasts_with_base() { + let theme = OakTheme::olive_dark(); + let text_luma = relative_luminance(theme.text); + let base_luma = relative_luminance(theme.base); + // Text must be clearly brighter than the base. + assert!(text_luma - base_luma > 0.4, "dark theme lacks contrast"); + // The accent must be visible against both. + let accent_luma = relative_luminance(theme.accent); + assert!((accent_luma - base_luma).abs() > 0.1); + } - #[test] - fn light_theme_text_contrasts_with_base() { - let theme = OakTheme::olive_light(); - let text_luma = relative_luminance(theme.text); - let base_luma = relative_luminance(theme.base); - assert!(base_luma - text_luma > 0.5, "light theme lacks contrast"); - } + #[test] + fn light_theme_text_contrasts_with_base() { + let theme = OakTheme::olive_light(); + let text_luma = relative_luminance(theme.text); + let base_luma = relative_luminance(theme.base); + assert!(base_luma - text_luma > 0.5, "light theme lacks contrast"); + } - #[test] - fn colors_map_keeps_text_and_selected() { - let colors = OakTheme::olive_dark().colors(); - assert_eq!(colors.selected, rgb(0x2A82DA)); - assert_eq!(colors.selected_text, rgb(0xffffff)); - } + #[test] + fn colors_map_keeps_text_and_selected() { + let colors = OakTheme::olive_dark().colors(); + assert_eq!(colors.selected, rgb(0x2A82DA)); + assert_eq!(colors.selected_text, rgb(0xffffff)); + } - #[test] - fn border_derives_from_alternate_base() { - let dark = OakTheme::olive_dark(); - let border = dark.border(); - // Dark theme: border is darker than the alternate base. - assert!(relative_luminance(border) < relative_luminance(dark.alternate_base)); - let light = OakTheme::olive_light(); - assert!(relative_luminance(light.border()) > relative_luminance(light.alternate_base)); - } + #[test] + fn border_derives_from_alternate_base() { + let dark = OakTheme::olive_dark(); + let border = dark.border(); + // Dark theme: border is darker than the alternate base. + assert!(relative_luminance(border) < relative_luminance(dark.alternate_base)); + let light = OakTheme::olive_light(); + assert!(relative_luminance(light.border()) > relative_luminance(light.alternate_base)); + } - #[gpui::test] - async fn apply_theme_switches_default_colors(cx: &mut TestAppContext) { - cx.update(|app| { - apply_theme(app, &OakTheme::olive_light()); - let colors = app.default_colors().clone(); - assert_eq!(colors.background, rgb(0xF0F0F0)); - assert_eq!(colors.text, rgb(0x000000)); - // And the extended theme is queryable. - assert_eq!(current_theme(app).name, "Olive Light"); - }); - } + #[gpui::test] + async fn apply_theme_switches_default_colors(cx: &mut TestAppContext) { + cx.update(|app| { + apply_theme(app, &OakTheme::olive_light()); + let colors = app.default_colors().clone(); + assert_eq!(colors.background, rgb(0xF0F0F0)); + assert_eq!(colors.text, rgb(0x000000)); + // And the extended theme is queryable. + assert_eq!(current_theme(app).name, "Olive Light"); + }); + } } diff --git a/crates/gpui_widgets/src/tooltip.rs b/crates/gpui_widgets/src/tooltip.rs index 52abb52743..8abfbd1104 100644 --- a/crates/gpui_widgets/src/tooltip.rs +++ b/crates/gpui_widgets/src/tooltip.rs @@ -6,30 +6,30 @@ //! `el.tooltip(move |window, cx| tooltip_view(label.clone(), window, cx))`. use gpui::{ - AnyView, App, Context, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, + AnyView, App, Context, Render, SharedString, Window, colors::DefaultColors, div, prelude::*, }; /// The tooltip view: a small rounded label in the theme's accent. pub struct TooltipView { - label: SharedString, + label: SharedString, } impl Render for TooltipView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - div() - .px_2() - .py_1() - .rounded_md() - .bg(colors.selected) - .text_color(colors.selected_text) - .text_xs() - .child(self.label.clone()) - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .px_2() + .py_1() + .rounded_md() + .bg(colors.selected) + .text_color(colors.selected_text) + .text_xs() + .child(self.label.clone()) + } } /// Builds a tooltip view for `label`, for use in a /// [`Div::tooltip`](gpui::Div::tooltip) builder. pub fn tooltip_view(label: SharedString, _window: &mut Window, cx: &mut App) -> AnyView { - cx.new(|_cx| TooltipView { label }).into() + cx.new(|_cx| TooltipView { label }).into() } diff --git a/crates/gpui_widgets/src/value.rs b/crates/gpui_widgets/src/value.rs index 8f9440d13a..db9940d1f6 100644 --- a/crates/gpui_widgets/src/value.rs +++ b/crates/gpui_widgets/src/value.rs @@ -11,174 +11,174 @@ use gpui::SharedString; /// The family of values a control can edit. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ValueKind { - /// A real number. - Float, - /// A whole number. - Integer, - /// An exact fraction `num / den`. - Rational, - /// An angle in degrees. - Angle, + /// A real number. + Float, + /// A whole number. + Integer, + /// An exact fraction `num / den`. + Rational, + /// An angle in degrees. + Angle, } /// The value edited by a form control. #[derive(Debug, Clone, Copy, PartialEq)] pub enum SliderValue { - /// A real number. - Float(f64), - /// A whole number. - Integer(i64), - /// An exact fraction. - Rational(RationalValue), - /// An angle in degrees. - Angle(f64), + /// A real number. + Float(f64), + /// A whole number. + Integer(i64), + /// An exact fraction. + Rational(RationalValue), + /// An angle in degrees. + Angle(f64), } impl SliderValue { - /// The [`ValueKind`] of this value. - pub fn kind(self) -> ValueKind { - match self { - SliderValue::Float(_) => ValueKind::Float, - SliderValue::Integer(_) => ValueKind::Integer, - SliderValue::Rational(_) => ValueKind::Rational, - SliderValue::Angle(_) => ValueKind::Angle, - } - } + /// The [`ValueKind`] of this value. + pub fn kind(self) -> ValueKind { + match self { + SliderValue::Float(_) => ValueKind::Float, + SliderValue::Integer(_) => ValueKind::Integer, + SliderValue::Rational(_) => ValueKind::Rational, + SliderValue::Angle(_) => ValueKind::Angle, + } + } - /// The numeric projection used for painting and interpolation. - /// - /// For [`SliderValue::Rational`] this is `num / den` as `f64`; the exact - /// pair is preserved by [`SliderValue::Rational`] itself. - pub fn to_f64(self) -> f64 { - match self { - SliderValue::Float(v) => v, - SliderValue::Integer(v) => v as f64, - SliderValue::Rational(v) => v.to_f64(), - SliderValue::Angle(v) => v, - } - } + /// The numeric projection used for painting and interpolation. + /// + /// For [`SliderValue::Rational`] this is `num / den` as `f64`; the exact + /// pair is preserved by [`SliderValue::Rational`] itself. + pub fn to_f64(self) -> f64 { + match self { + SliderValue::Float(v) => v, + SliderValue::Integer(v) => v as f64, + SliderValue::Rational(v) => v.to_f64(), + SliderValue::Angle(v) => v, + } + } - /// Rebuild a value from its numeric projection. - /// - /// [`SliderValue::Integer`] truncates toward zero and - /// [`SliderValue::Rational`] reconstructs the closest fraction with a - /// bounded denominator, so this is only lossless when the source value - /// was exact to begin with. - pub fn from_f64(kind: ValueKind, value: f64) -> Self { - match kind { - ValueKind::Float => SliderValue::Float(value), - ValueKind::Integer => SliderValue::Integer(value.trunc() as i64), - ValueKind::Rational => SliderValue::Rational(RationalValue::from_f64(value, 1_000_000)), - ValueKind::Angle => SliderValue::Angle(value), - } - } + /// Rebuild a value from its numeric projection. + /// + /// [`SliderValue::Integer`] truncates toward zero and + /// [`SliderValue::Rational`] reconstructs the closest fraction with a + /// bounded denominator, so this is only lossless when the source value + /// was exact to begin with. + pub fn from_f64(kind: ValueKind, value: f64) -> Self { + match kind { + ValueKind::Float => SliderValue::Float(value), + ValueKind::Integer => SliderValue::Integer(value.trunc() as i64), + ValueKind::Rational => SliderValue::Rational(RationalValue::from_f64(value, 1_000_000)), + ValueKind::Angle => SliderValue::Angle(value), + } + } } /// An exact rational number, kept normalized (`den > 0`, `gcd(|num|, den) == 1`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct RationalValue { - num: i64, - den: i64, + num: i64, + den: i64, } /// Error returned when a rational cannot be constructed from raw parts. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum RationalError { - /// The denominator must not be zero. - #[error("rational denominator must not be zero")] - ZeroDenominator, + /// The denominator must not be zero. + #[error("rational denominator must not be zero")] + ZeroDenominator, } impl RationalValue { - /// The zero rational `0/1`. - pub const ZERO: Self = Self { num: 0, den: 1 }; - /// The one rational `1/1`. - pub const ONE: Self = Self { num: 1, den: 1 }; + /// The zero rational `0/1`. + pub const ZERO: Self = Self { num: 0, den: 1 }; + /// The one rational `1/1`. + pub const ONE: Self = Self { num: 1, den: 1 }; - /// Construct a normalized rational, rejecting a zero denominator. - pub fn new(num: i64, den: i64) -> Result { - if den == 0 { - return Err(RationalError::ZeroDenominator); - } - Ok(Self::new_unchecked(num, den)) - } + /// Construct a normalized rational, rejecting a zero denominator. + pub fn new(num: i64, den: i64) -> Result { + if den == 0 { + return Err(RationalError::ZeroDenominator); + } + Ok(Self::new_unchecked(num, den)) + } - /// Construct a normalized rational, clamping a zero denominator to one. - pub fn new_or_one(num: i64, den: i64) -> Self { - if den == 0 { - return Self { num, den: 1 }; - } - Self::new_unchecked(num, den) - } + /// Construct a normalized rational, clamping a zero denominator to one. + pub fn new_or_one(num: i64, den: i64) -> Self { + if den == 0 { + return Self { num, den: 1 }; + } + Self::new_unchecked(num, den) + } - fn new_unchecked(num: i64, den: i64) -> Self { - let (num, den) = if den < 0 { (-num, -den) } else { (num, den) }; - let gcd = num.unsigned_abs().gcd(den as u64) as i64; - Self { - num: num / gcd, - den: den / gcd, - } - } + fn new_unchecked(num: i64, den: i64) -> Self { + let (num, den) = if den < 0 { (-num, -den) } else { (num, den) }; + let gcd = num.unsigned_abs().gcd(den as u64) as i64; + Self { + num: num / gcd, + den: den / gcd, + } + } - /// The numerator of the normalized fraction. - pub fn num(self) -> i64 { - self.num - } + /// The numerator of the normalized fraction. + pub fn num(self) -> i64 { + self.num + } - /// The denominator of the normalized fraction (always positive). - pub fn den(self) -> i64 { - self.den - } + /// The denominator of the normalized fraction (always positive). + pub fn den(self) -> i64 { + self.den + } - /// The value as `f64`. - pub fn to_f64(self) -> f64 { - self.num as f64 / self.den as f64 - } + /// The value as `f64`. + pub fn to_f64(self) -> f64 { + self.num as f64 / self.den as f64 + } - /// The closest rational with `1 <= den <= max_den`, via continued - /// fractions. `NaN` and infinities map to the zero rational; negative - /// values keep their sign. - pub fn from_f64(value: f64, max_den: i64) -> Self { - if !value.is_finite() { - return Self::ZERO; - } - let max_den = max_den.max(1); - let negative = value < 0.0; - let value = value.abs(); - // Continued fraction convergents with bounded denominator. - let mut n0 = 0i64; - let mut d0 = 1i64; - let mut n1 = 1i64; - let mut d1 = 0i64; - let mut remainder = value; - for _ in 0..64 { - let a = remainder.floor() as i64; - let n2 = a.saturating_mul(n1).saturating_add(n0); - let d2 = a.saturating_mul(d1).saturating_add(d0); - if d2 > max_den { - break; - } - n0 = n1; - d0 = d1; - n1 = n2; - d1 = d2; - let frac = remainder - remainder.floor(); - if frac.abs() < 1e-12 { - break; - } - remainder = 1.0 / frac; - } - let (num, den) = if negative { (-n1, d1) } else { (n1, d1) }; - Self::new_unchecked(num, den) - } + /// The closest rational with `1 <= den <= max_den`, via continued + /// fractions. `NaN` and infinities map to the zero rational; negative + /// values keep their sign. + pub fn from_f64(value: f64, max_den: i64) -> Self { + if !value.is_finite() { + return Self::ZERO; + } + let max_den = max_den.max(1); + let negative = value < 0.0; + let value = value.abs(); + // Continued fraction convergents with bounded denominator. + let mut n0 = 0i64; + let mut d0 = 1i64; + let mut n1 = 1i64; + let mut d1 = 0i64; + let mut remainder = value; + for _ in 0..64 { + let a = remainder.floor() as i64; + let n2 = a.saturating_mul(n1).saturating_add(n0); + let d2 = a.saturating_mul(d1).saturating_add(d0); + if d2 > max_den { + break; + } + n0 = n1; + d0 = d1; + n1 = n2; + d1 = d2; + let frac = remainder - remainder.floor(); + if frac.abs() < 1e-12 { + break; + } + remainder = 1.0 / frac; + } + let (num, den) = if negative { (-n1, d1) } else { (n1, d1) }; + Self::new_unchecked(num, den) + } } /// A failure to parse a value from text. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ParseValueError { - /// The text did not match the expected format for this value kind. - #[error("could not parse {0:?} as {1:?}")] - InvalidFormat(SharedString, ValueKind), + /// The text did not match the expected format for this value kind. + #[error("could not parse {0:?} as {1:?}")] + InvalidFormat(SharedString, ValueKind), } /// Formats and parses a [`SliderValue`] for display and direct entry. @@ -187,11 +187,11 @@ pub enum ParseValueError { /// specialize the representation (e.g. timecode instead of frames, or /// fractions with a fixed denominator). pub trait ValueFormatter: 'static { - /// Render a value as text. - fn format(&self, value: SliderValue) -> SharedString; - /// Parse text into a value. Must reject malformed input with - /// [`ParseValueError`]; controls refuse to apply rejected input. - fn parse(&self, text: &str) -> Result; + /// Render a value as text. + fn format(&self, value: SliderValue) -> SharedString; + /// Parse text into a value. Must reject malformed input with + /// [`ParseValueError`]; controls refuse to apply rejected input. + fn parse(&self, text: &str) -> Result; } /// The default formatter: @@ -204,195 +204,231 @@ pub trait ValueFormatter: 'static { pub struct DefaultFormatter; impl ValueFormatter for DefaultFormatter { - fn format(&self, value: SliderValue) -> SharedString { - match value { - SliderValue::Float(v) => { - if !v.is_finite() { - return "0".into(); - } - let rounded = (v * 1000.0).round() / 1000.0; - let mut text = format!("{rounded}"); - if text.contains('.') { - while text.ends_with('0') { - text.pop(); - } - if text.ends_with('.') { - text.pop(); - } - } - text.into() - } - SliderValue::Integer(v) => format!("{v}").into(), - SliderValue::Rational(v) => format!("{}/{}", v.num(), v.den()).into(), - SliderValue::Angle(v) => { - let rounded = (v * 1000.0).round() / 1000.0; - format!("{rounded}°").into() - } - } - } + fn format(&self, value: SliderValue) -> SharedString { + match value { + SliderValue::Float(v) => { + if !v.is_finite() { + return "0".into(); + } + let rounded = (v * 1000.0).round() / 1000.0; + let mut text = format!("{rounded}"); + if text.contains('.') { + while text.ends_with('0') { + text.pop(); + } + if text.ends_with('.') { + text.pop(); + } + } + text.into() + } + SliderValue::Integer(v) => format!("{v}").into(), + SliderValue::Rational(v) => format!("{}/{}", v.num(), v.den()).into(), + SliderValue::Angle(v) => { + let rounded = (v * 1000.0).round() / 1000.0; + format!("{rounded}°").into() + } + } + } - fn parse(&self, text: &str) -> Result { - let text = text.trim(); - let kind = |reason: SharedString| ParseValueError::InvalidFormat(reason, ValueKind::Float); - if let Some((num, den)) = text.split_once('/') { - let num: i64 = num - .trim() - .parse() - .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; - let den: i64 = den - .trim() - .parse() - .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; - return RationalValue::new(num, den) - .map(SliderValue::Rational) - .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational)); - } - let (body, is_angle) = text - .strip_suffix('°') - .map(|body| (body, true)) - .or_else(|| { - text.strip_suffix("deg") - .or_else(|| text.strip_suffix("DEG")) - .map(|body| (body, true)) - }) - .unwrap_or((text, false)); - if is_angle { - let value: f64 = body - .trim() - .parse() - .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Angle))?; - return Ok(SliderValue::Angle(value)); - } - // An exact decimal with no fractional part parses as an integer when - // there are no digits after the point; otherwise as a float. - let value: f64 = body - .trim() - .parse() - .map_err(|_| kind(text.into()))?; - if value.fract() == 0.0 && value.abs() < i64::MAX as f64 { - Ok(SliderValue::Integer(value as i64)) - } else { - Ok(SliderValue::Float(value)) - } - } + fn parse(&self, text: &str) -> Result { + let text = text.trim(); + let kind = |reason: SharedString| ParseValueError::InvalidFormat(reason, ValueKind::Float); + if let Some((num, den)) = text.split_once('/') { + let num: i64 = num + .trim() + .parse() + .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; + let den: i64 = den + .trim() + .parse() + .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; + return RationalValue::new(num, den) + .map(SliderValue::Rational) + .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational)); + } + let (body, is_angle) = text + .strip_suffix('°') + .map(|body| (body, true)) + .or_else(|| { + text.strip_suffix("deg") + .or_else(|| text.strip_suffix("DEG")) + .map(|body| (body, true)) + }) + .unwrap_or((text, false)); + if is_angle { + let value: f64 = body + .trim() + .parse() + .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Angle))?; + return Ok(SliderValue::Angle(value)); + } + // An exact decimal with no fractional part parses as an integer when + // there are no digits after the point; otherwise as a float. + let value: f64 = body.trim().parse().map_err(|_| kind(text.into()))?; + if value.fract() == 0.0 && value.abs() < i64::MAX as f64 { + Ok(SliderValue::Integer(value as i64)) + } else { + Ok(SliderValue::Float(value)) + } + } } /// Extension: `u64::gcd` used by [`RationalValue`]. trait Gcd { - fn gcd(self, other: Self) -> Self; + fn gcd(self, other: Self) -> Self; } impl Gcd for u64 { - fn gcd(mut self, mut other: Self) -> Self { - while other != 0 { - (self, other) = (other, self % other); - } - self - } + fn gcd(mut self, mut other: Self) -> Self { + while other != 0 { + (self, other) = (other, self % other); + } + self + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn rational_normalizes() { - assert_eq!( - RationalValue::new(4, 6).unwrap(), - RationalValue::new(2, 3).unwrap() - ); - assert_eq!(RationalValue::new(2, 3).unwrap().num(), 2); - assert_eq!(RationalValue::new(2, 3).unwrap().den(), 3); - assert_eq!(RationalValue::new(-2, 4).unwrap(), RationalValue::new(-1, 2).unwrap()); - assert_eq!(RationalValue::new(1, -2).unwrap(), RationalValue::new(-1, 2).unwrap()); - } + #[test] + fn rational_normalizes() { + assert_eq!( + RationalValue::new(4, 6).unwrap(), + RationalValue::new(2, 3).unwrap() + ); + assert_eq!(RationalValue::new(2, 3).unwrap().num(), 2); + assert_eq!(RationalValue::new(2, 3).unwrap().den(), 3); + assert_eq!( + RationalValue::new(-2, 4).unwrap(), + RationalValue::new(-1, 2).unwrap() + ); + assert_eq!( + RationalValue::new(1, -2).unwrap(), + RationalValue::new(-1, 2).unwrap() + ); + } - #[test] - fn rational_rejects_zero_denominator() { - assert_eq!( - RationalValue::new(1, 0), - Err(RationalError::ZeroDenominator) - ); - assert_eq!(RationalValue::new_or_one(1, 0), RationalValue::new(1, 1).unwrap()); - } + #[test] + fn rational_rejects_zero_denominator() { + assert_eq!( + RationalValue::new(1, 0), + Err(RationalError::ZeroDenominator) + ); + assert_eq!( + RationalValue::new_or_one(1, 0), + RationalValue::new(1, 1).unwrap() + ); + } - #[test] - fn rational_from_f64_exact() { - assert_eq!(RationalValue::from_f64(0.5, 1000), RationalValue::new(1, 2).unwrap()); - assert_eq!(RationalValue::from_f64(0.25, 1000), RationalValue::new(1, 4).unwrap()); - assert_eq!(RationalValue::from_f64(2.0, 1000), RationalValue::new(2, 1).unwrap()); - assert_eq!(RationalValue::from_f64(-1.5, 1000), RationalValue::new(-3, 2).unwrap()); - assert_eq!(RationalValue::from_f64(0.3333333333, 1000), RationalValue::new(1, 3).unwrap()); - assert_eq!(RationalValue::from_f64(f64::NAN, 1000), RationalValue::ZERO); - assert_eq!(RationalValue::from_f64(f64::INFINITY, 1000), RationalValue::ZERO); - } + #[test] + fn rational_from_f64_exact() { + assert_eq!( + RationalValue::from_f64(0.5, 1000), + RationalValue::new(1, 2).unwrap() + ); + assert_eq!( + RationalValue::from_f64(0.25, 1000), + RationalValue::new(1, 4).unwrap() + ); + assert_eq!( + RationalValue::from_f64(2.0, 1000), + RationalValue::new(2, 1).unwrap() + ); + assert_eq!( + RationalValue::from_f64(-1.5, 1000), + RationalValue::new(-3, 2).unwrap() + ); + assert_eq!( + RationalValue::from_f64(0.3333333333, 1000), + RationalValue::new(1, 3).unwrap() + ); + assert_eq!(RationalValue::from_f64(f64::NAN, 1000), RationalValue::ZERO); + assert_eq!( + RationalValue::from_f64(f64::INFINITY, 1000), + RationalValue::ZERO + ); + } - #[test] - fn rational_from_f64_bounded_denominator() { - // 1/999983 needs a huge denominator; the bounded approximation may be - // 0 but must never exceed the bound or produce a negative denominator. - let value = RationalValue::from_f64(1.0 / 999983.0, 1000); - assert!(value.den() <= 1000); - assert!(value.den() > 0); - // A friendlier irrational keeps a nonzero approximation. - let approx = RationalValue::from_f64(std::f64::consts::FRAC_1_PI, 1000); - assert!(approx.num() > 0); - assert!(approx.den() <= 1000); - assert!((approx.to_f64() - std::f64::consts::FRAC_1_PI).abs() < 1e-3); - } + #[test] + fn rational_from_f64_bounded_denominator() { + // 1/999983 needs a huge denominator; the bounded approximation may be + // 0 but must never exceed the bound or produce a negative denominator. + let value = RationalValue::from_f64(1.0 / 999983.0, 1000); + assert!(value.den() <= 1000); + assert!(value.den() > 0); + // A friendlier irrational keeps a nonzero approximation. + let approx = RationalValue::from_f64(std::f64::consts::FRAC_1_PI, 1000); + assert!(approx.num() > 0); + assert!(approx.den() <= 1000); + assert!((approx.to_f64() - std::f64::consts::FRAC_1_PI).abs() < 1e-3); + } - #[test] - fn value_kind_projection() { - assert_eq!(SliderValue::Integer(-3).to_f64(), -3.0); - assert_eq!(SliderValue::Rational(RationalValue::new(1, 3).unwrap()).to_f64(), 1.0 / 3.0); - assert_eq!(SliderValue::from_f64(ValueKind::Integer, 3.9), SliderValue::Integer(3)); - assert_eq!( - SliderValue::from_f64(ValueKind::Rational, 0.5), - SliderValue::Rational(RationalValue::new(1, 2).unwrap()) - ); - assert_eq!(SliderValue::from_f64(ValueKind::Angle, 90.0), SliderValue::Angle(90.0)); - } + #[test] + fn value_kind_projection() { + assert_eq!(SliderValue::Integer(-3).to_f64(), -3.0); + assert_eq!( + SliderValue::Rational(RationalValue::new(1, 3).unwrap()).to_f64(), + 1.0 / 3.0 + ); + assert_eq!( + SliderValue::from_f64(ValueKind::Integer, 3.9), + SliderValue::Integer(3) + ); + assert_eq!( + SliderValue::from_f64(ValueKind::Rational, 0.5), + SliderValue::Rational(RationalValue::new(1, 2).unwrap()) + ); + assert_eq!( + SliderValue::from_f64(ValueKind::Angle, 90.0), + SliderValue::Angle(90.0) + ); + } - #[test] - fn default_formatter_round_trips() { - let formatter = DefaultFormatter; - for value in [ - SliderValue::Float(1.5), - SliderValue::Integer(-42), - SliderValue::Rational(RationalValue::new(1, 3).unwrap()), - SliderValue::Angle(90.0), - ] { - let text = formatter.format(value); - let parsed = formatter.parse(text.as_ref()).unwrap(); - assert_eq!(parsed.kind(), value.kind()); - } - } + #[test] + fn default_formatter_round_trips() { + let formatter = DefaultFormatter; + for value in [ + SliderValue::Float(1.5), + SliderValue::Integer(-42), + SliderValue::Rational(RationalValue::new(1, 3).unwrap()), + SliderValue::Angle(90.0), + ] { + let text = formatter.format(value); + let parsed = formatter.parse(text.as_ref()).unwrap(); + assert_eq!(parsed.kind(), value.kind()); + } + } - #[test] - fn default_formatter_trims_and_truncates() { - let formatter = DefaultFormatter; - assert_eq!(formatter.format(SliderValue::Float(1.5)), "1.5"); - assert_eq!(formatter.format(SliderValue::Float(0.123456)), "0.123"); - assert_eq!(formatter.format(SliderValue::Float(2.0)), "2"); - assert_eq!(formatter.format(SliderValue::Angle(45.5)), "45.5°"); - assert_eq!(formatter.format(SliderValue::Integer(7)), "7"); - } + #[test] + fn default_formatter_trims_and_truncates() { + let formatter = DefaultFormatter; + assert_eq!(formatter.format(SliderValue::Float(1.5)), "1.5"); + assert_eq!(formatter.format(SliderValue::Float(0.123456)), "0.123"); + assert_eq!(formatter.format(SliderValue::Float(2.0)), "2"); + assert_eq!(formatter.format(SliderValue::Angle(45.5)), "45.5°"); + assert_eq!(formatter.format(SliderValue::Integer(7)), "7"); + } - #[test] - fn default_formatter_rejects_invalid_input() { - let formatter = DefaultFormatter; - assert!(formatter.parse("abc").is_err()); - assert!(formatter.parse("1/0").is_err()); - assert!(formatter.parse("12/ab").is_err()); - assert!(formatter.parse("").is_err()); - assert!(formatter.parse("90°abc").is_err()); - } + #[test] + fn default_formatter_rejects_invalid_input() { + let formatter = DefaultFormatter; + assert!(formatter.parse("abc").is_err()); + assert!(formatter.parse("1/0").is_err()); + assert!(formatter.parse("12/ab").is_err()); + assert!(formatter.parse("").is_err()); + assert!(formatter.parse("90°abc").is_err()); + } - #[test] - fn default_formatter_parses_angle_suffix() { - let formatter = DefaultFormatter; - assert_eq!(formatter.parse("90°").unwrap(), SliderValue::Angle(90.0)); - assert_eq!(formatter.parse("45 deg").unwrap(), SliderValue::Angle(45.0)); - assert_eq!(formatter.parse("180 DEG").unwrap(), SliderValue::Angle(180.0)); - } + #[test] + fn default_formatter_parses_angle_suffix() { + let formatter = DefaultFormatter; + assert_eq!(formatter.parse("90°").unwrap(), SliderValue::Angle(90.0)); + assert_eq!(formatter.parse("45 deg").unwrap(), SliderValue::Angle(45.0)); + assert_eq!( + formatter.parse("180 DEG").unwrap(), + SliderValue::Angle(180.0) + ); + } } diff --git a/crates/gpui_widgets/src/viewer/clock.rs b/crates/gpui_widgets/src/viewer/clock.rs index 1c2f4f1622..c925f1e8d7 100644 --- a/crates/gpui_widgets/src/viewer/clock.rs +++ b/crates/gpui_widgets/src/viewer/clock.rs @@ -9,10 +9,10 @@ use gpui::timeline::{Frame, FrameRate}; /// A read-only view of the engine's playback clock. pub trait PlaybackClock: 'static { - /// The current playhead frame. - fn current_frame(&self) -> Frame; - /// Whether playback is running. - fn is_playing(&self) -> bool; - /// The sequence's frame rate. - fn frame_rate(&self) -> FrameRate; + /// The current playhead frame. + fn current_frame(&self) -> Frame; + /// Whether playback is running. + fn is_playing(&self) -> bool; + /// The sequence's frame rate. + fn frame_rate(&self) -> FrameRate; } diff --git a/crates/gpui_widgets/src/viewer/mod.rs b/crates/gpui_widgets/src/viewer/mod.rs index 92877b0668..8d11ed47b3 100644 --- a/crates/gpui_widgets/src/viewer/mod.rs +++ b/crates/gpui_widgets/src/viewer/mod.rs @@ -14,9 +14,9 @@ pub use transport::*; use gpui::timeline::{FrameRate, TimeDisplay, format_timecode}; use gpui::{ - AnyElement, App, AsyncWindowContext, ClickEvent, Context, Entity, EventEmitter, FocusHandle, - Focusable, ObjectFit, Render, RenderImage, SharedString, SurfaceSource, Window, colors::DefaultColors, - div, img, prelude::*, px, surface, + AnyElement, App, AsyncWindowContext, ClickEvent, Context, Entity, EventEmitter, FocusHandle, + Focusable, ObjectFit, Render, RenderImage, SharedString, SurfaceSource, Window, + colors::DefaultColors, div, img, prelude::*, px, surface, }; use std::sync::Arc; @@ -25,48 +25,48 @@ use crate::{icons, tooltip::tooltip_view}; /// A request emitted by the viewer. #[derive(Debug, Clone, PartialEq)] pub enum ViewerEvent { - /// Start playback. - PlayRequested { - /// The viewer's stable id. - control: usize, - }, - /// Pause playback. - PauseRequested { - /// The viewer's stable id. - control: usize, - }, - /// Step the playhead by `delta` frames. - StepRequested { - /// The viewer's stable id. - control: usize, - /// Frames to step (negative steps backward). - delta: i64, - }, - /// Set the loop-in point at the playhead. - InPointRequested { - /// The viewer's stable id. - control: usize, - }, - /// Set the loop-out point at the playhead. - OutPointRequested { - /// The viewer's stable id. - control: usize, - }, - /// Clear the loop range. - ClearRangeRequested { - /// The viewer's stable id. - control: usize, - }, - /// Toggle the safe-frame overlay. - ToggleSafeFramesRequested { - /// The viewer's stable id. - control: usize, - }, - /// Toggle the zoom (contain vs cover). - ToggleZoomRequested { - /// The viewer's stable id. - control: usize, - }, + /// Start playback. + PlayRequested { + /// The viewer's stable id. + control: usize, + }, + /// Pause playback. + PauseRequested { + /// The viewer's stable id. + control: usize, + }, + /// Step the playhead by `delta` frames. + StepRequested { + /// The viewer's stable id. + control: usize, + /// Frames to step (negative steps backward). + delta: i64, + }, + /// Set the loop-in point at the playhead. + InPointRequested { + /// The viewer's stable id. + control: usize, + }, + /// Set the loop-out point at the playhead. + OutPointRequested { + /// The viewer's stable id. + control: usize, + }, + /// Clear the loop range. + ClearRangeRequested { + /// The viewer's stable id. + control: usize, + }, + /// Toggle the safe-frame overlay. + ToggleSafeFramesRequested { + /// The viewer's stable id. + control: usize, + }, + /// Toggle the zoom (contain vs cover). + ToggleZoomRequested { + /// The viewer's stable id. + control: usize, + }, } /// The picture source of a [`ViewerWidget`]. @@ -76,554 +76,583 @@ pub enum ViewerEvent { /// [`ViewerFrameSource::CpuFrame`]. #[derive(Clone)] pub enum ViewerFrameSource { - /// A platform surface: a CoreVideo pixel buffer on macOS, or a GPU - /// texture handle on Linux/FreeBSD. - Surface(SurfaceSource), - /// A CPU-side frame as raw bytes in a [`RenderImage`] (BGRA8, row-major, - /// top-to-bottom), uploaded through gpui's sprite atlas on every - /// platform — the path to use when no platform surface is available. - CpuFrame(Arc), + /// A platform surface: a CoreVideo pixel buffer on macOS, or a GPU + /// texture handle on Linux/FreeBSD. + Surface(SurfaceSource), + /// A CPU-side frame as raw bytes in a [`RenderImage`] (BGRA8, row-major, + /// top-to-bottom), uploaded through gpui's sprite atlas on every + /// platform — the path to use when no platform surface is available. + CpuFrame(Arc), } /// The viewer widget. pub struct ViewerWidget { - control: usize, - clock: Entity, - frame_rate: FrameRate, - transport: TransportState, - frame_source: Option, - focus_handle: FocusHandle, - show_safe_frames: bool, - zoom: bool, + control: usize, + clock: Entity, + frame_rate: FrameRate, + transport: TransportState, + frame_source: Option, + focus_handle: FocusHandle, + show_safe_frames: bool, + zoom: bool, } impl ViewerWidget { - /// Create a viewer driven by `clock`. - pub fn new( - control: usize, - clock: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let frame_rate = clock.read(cx).frame_rate(); + /// Create a viewer driven by `clock`. + pub fn new( + control: usize, + clock: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let frame_rate = clock.read(cx).frame_rate(); - // Poll the engine clock on a timer and reflect it locally. - let this = cx.weak_entity(); - window.spawn(cx, async move |cx: &mut AsyncWindowContext| { - loop { - cx.background_executor() - .timer(std::time::Duration::from_millis(16)) - .await; - let _ = cx.update(|_window, app| { - if let Some(this) = this.upgrade() { - this.update(app, |this, cx| this.poll_clock(cx)); - } - }); - } - }) - .detach(); + // Poll the engine clock on a timer and reflect it locally. + let this = cx.weak_entity(); + window + .spawn(cx, async move |cx: &mut AsyncWindowContext| { + loop { + cx.background_executor() + .timer(std::time::Duration::from_millis(16)) + .await; + let _ = cx.update(|_window, app| { + if let Some(this) = this.upgrade() { + this.update(app, |this, cx| this.poll_clock(cx)); + } + }); + } + }) + .detach(); - Self { - control, - clock, - frame_rate, - transport: TransportState::new(), - frame_source: None, - focus_handle: cx.focus_handle(), - show_safe_frames: false, - zoom: false, - } - } + Self { + control, + clock, + frame_rate, + transport: TransportState::new(), + frame_source: None, + focus_handle: cx.focus_handle(), + show_safe_frames: false, + zoom: false, + } + } - /// The current transport state. - pub fn transport(&self) -> TransportState { - self.transport - } + /// The current transport state. + pub fn transport(&self) -> TransportState { + self.transport + } - /// Set the picture source (the bridge's pixel buffer) and repaint. - pub fn set_frame_source(&mut self, source: Option, cx: &mut Context) { - self.frame_source = source.map(ViewerFrameSource::Surface); - cx.notify(); - } + /// Set the picture source (the bridge's pixel buffer) and repaint. + pub fn set_frame_source(&mut self, source: Option, cx: &mut Context) { + self.frame_source = source.map(ViewerFrameSource::Surface); + cx.notify(); + } - /// Set the picture source to a CPU-side frame and repaint. - /// - /// This is the path for non-macOS platforms and engines that decode to - /// raw pixels instead of platform surfaces: hand in a - /// [`RenderImage`](gpui::RenderImage) whose bytes are BGRA8 (the same - /// format gpui's `img` element uses) and the viewer uploads it through - /// the sprite atlas. `None` clears the picture (showing the placeholder). - pub fn set_cpu_frame(&mut self, frame: Option>, cx: &mut Context) { - self.frame_source = frame.map(ViewerFrameSource::CpuFrame); - cx.notify(); - } + /// Set the picture source to a CPU-side frame and repaint. + /// + /// This is the path for non-macOS platforms and engines that decode to + /// raw pixels instead of platform surfaces: hand in a + /// [`RenderImage`](gpui::RenderImage) whose bytes are BGRA8 (the same + /// format gpui's `img` element uses) and the viewer uploads it through + /// the sprite atlas. `None` clears the picture (showing the placeholder). + pub fn set_cpu_frame(&mut self, frame: Option>, cx: &mut Context) { + self.frame_source = frame.map(ViewerFrameSource::CpuFrame); + cx.notify(); + } - fn poll_clock(&mut self, cx: &mut Context) { - let clock = self.clock.read(cx); - let frame = clock.current_frame(); - let playing = clock.is_playing(); - if frame != self.transport.frame || playing != self.transport.playing { - self.transport.frame = frame; - self.transport.playing = playing; - cx.notify(); - } - } + fn poll_clock(&mut self, cx: &mut Context) { + let clock = self.clock.read(cx); + let frame = clock.current_frame(); + let playing = clock.is_playing(); + if frame != self.transport.frame || playing != self.transport.playing { + self.transport.frame = frame; + self.transport.playing = playing; + cx.notify(); + } + } - fn emit(&mut self, event: ViewerEvent, cx: &mut Context) { - cx.emit(event); - cx.notify(); - } + fn emit(&mut self, event: ViewerEvent, cx: &mut Context) { + cx.emit(event); + cx.notify(); + } } impl EventEmitter for ViewerWidget {} impl Focusable for ViewerWidget { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for ViewerWidget { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.default_colors().clone(); - let timecode = format_timecode( - self.transport.frame, - self.frame_rate, - TimeDisplay::Timecode, - ); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let timecode = + format_timecode(self.transport.frame, self.frame_rate, TimeDisplay::Timecode); - // The picture area: surface (or placeholder), safe frames and zoom. - let mut picture = div() - .id("gpui-widgets-viewer-picture") - .flex_1() - .relative() - .bg(gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 1.0, - }); + // The picture area: surface (or placeholder), safe frames and zoom. + let mut picture = div() + .id("gpui-widgets-viewer-picture") + .flex_1() + .relative() + .bg(gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 1.0, + }); - if let Some(source) = &self.frame_source { - let fit = if self.zoom { ObjectFit::Cover } else { ObjectFit::Contain }; - let picture_element: AnyElement = match source { - ViewerFrameSource::Surface(surface_source) => { - surface(surface_source.clone()).size_full().object_fit(fit).into_any() - } - ViewerFrameSource::CpuFrame(image) => { - img(image.clone()).size_full().object_fit(fit).into_any() - } - }; - picture = picture.child(picture_element); - } else { - picture = picture.child( - div() - .size_full() - .flex() - .items_center() - .justify_center() - .text_color(colors.disabled) - .child(crate::i18n::tr("viewer.no_frame_source", "No frame source")), - ); - } + if let Some(source) = &self.frame_source { + let fit = if self.zoom { + ObjectFit::Cover + } else { + ObjectFit::Contain + }; + let picture_element: AnyElement = match source { + ViewerFrameSource::Surface(surface_source) => surface(surface_source.clone()) + .size_full() + .object_fit(fit) + .into_any(), + ViewerFrameSource::CpuFrame(image) => { + img(image.clone()).size_full().object_fit(fit).into_any() + } + }; + picture = picture.child(picture_element); + } else { + picture = picture.child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_color(colors.disabled) + .child(crate::i18n::tr("viewer.no_frame_source", "No frame source")), + ); + } - if self.show_safe_frames { - picture = picture.child( - div() - .absolute() - .left_0() - .right_0() - .top_0() - .bottom_0() - .flex() - .items_center() - .justify_center() - .child( - div() - .w(px(560.0)) - .h(px(315.0)) - .border_1() - .border_color(colors.selected), - ), - ); - } + if self.show_safe_frames { + picture = picture.child( + div() + .absolute() + .left_0() + .right_0() + .top_0() + .bottom_0() + .flex() + .items_center() + .justify_center() + .child( + div() + .w(px(560.0)) + .h(px(315.0)) + .border_1() + .border_color(colors.selected), + ), + ); + } - // Transport bar. The transport controls are icon buttons (16px icon - // on a 24px hit target, localized tooltips); without a registered - // icon resolver the buttons fall back to the glyph labels below. - let playing = self.transport.playing; - let play_label = if playing { "⏸" } else { "▶" }; - let in_icon = icons::path("prev", cx); - let step_back_icon = icons::path("rew", cx); - let play_icon = icons::path(if playing { "pause" } else { "play" }, cx); - let step_forward_icon = icons::path("ff", cx); - let out_icon = icons::path("next", cx); - let transport_bar = div() - .flex() - .items_center() - .gap_2() - .px_2() - .py_1() - .bg(colors.container) - .child(transport_button( - "gpui-widgets-viewer-in", - in_icon, - "⏮", - crate::i18n::tr("viewer.in_point", "入点"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit(ViewerEvent::InPointRequested { control: this.control }, cx); - }), - )) - .child(transport_button( - "gpui-widgets-viewer-step-back", - step_back_icon, - "⏪", - crate::i18n::tr("viewer.step_back", "上一帧"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit( - ViewerEvent::StepRequested { - control: this.control, - delta: -1, - }, - cx, - ); - }), - )) - .child(transport_button( - "gpui-widgets-viewer-play", - play_icon, - play_label, - if playing { - crate::i18n::tr("viewer.pause", "暂停") - } else { - crate::i18n::tr("viewer.play", "播放") - }, - cx.listener(|this, _event: &ClickEvent, _window, cx| { - let event = if this.transport.playing { - ViewerEvent::PauseRequested { control: this.control } - } else { - ViewerEvent::PlayRequested { control: this.control } - }; - this.emit(event, cx); - }), - )) - .child(transport_button( - "gpui-widgets-viewer-step-forward", - step_forward_icon, - "⏩", - crate::i18n::tr("viewer.step_forward", "下一帧"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit( - ViewerEvent::StepRequested { - control: this.control, - delta: 1, - }, - cx, - ); - }), - )) - .child(transport_button( - "gpui-widgets-viewer-out", - out_icon, - "⏭", - crate::i18n::tr("viewer.out_point", "出点"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit(ViewerEvent::OutPointRequested { control: this.control }, cx); - }), - )) - .child(transport_button( - "gpui-widgets-viewer-clear-range", - None, - x_glyph(colors.text), - crate::i18n::tr("viewer.clear_range", "清除入出点"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.emit(ViewerEvent::ClearRangeRequested { control: this.control }, cx); - }), - )) - .child( - div() - .px_2() - .text_color(colors.text) - .child(timecode), - ) - .child( - div().flex_1(), - ) - .child(button( - "gpui-widgets-viewer-safe", - crate::i18n::tr("viewer.safe_frames", "安全框"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.show_safe_frames = !this.show_safe_frames; - this.emit( - ViewerEvent::ToggleSafeFramesRequested { control: this.control }, - cx, - ); - }), - )) - .child(button( - "gpui-widgets-viewer-zoom", - crate::i18n::tr("viewer.zoom", "缩放"), - cx.listener(|this, _event: &ClickEvent, _window, cx| { - this.zoom = !this.zoom; - this.emit(ViewerEvent::ToggleZoomRequested { control: this.control }, cx); - }), - )); + // Transport bar. The transport controls are icon buttons (16px icon + // on a 24px hit target, localized tooltips); without a registered + // icon resolver the buttons fall back to the glyph labels below. + let playing = self.transport.playing; + let play_label = if playing { "⏸" } else { "▶" }; + let in_icon = icons::path("prev", cx); + let step_back_icon = icons::path("rew", cx); + let play_icon = icons::path(if playing { "pause" } else { "play" }, cx); + let step_forward_icon = icons::path("ff", cx); + let out_icon = icons::path("next", cx); + let transport_bar = div() + .flex() + .items_center() + .gap_2() + .px_2() + .py_1() + .bg(colors.container) + .child(transport_button( + "gpui-widgets-viewer-in", + in_icon, + "⏮", + crate::i18n::tr("viewer.in_point", "入点"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit( + ViewerEvent::InPointRequested { + control: this.control, + }, + cx, + ); + }), + )) + .child(transport_button( + "gpui-widgets-viewer-step-back", + step_back_icon, + "⏪", + crate::i18n::tr("viewer.step_back", "上一帧"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit( + ViewerEvent::StepRequested { + control: this.control, + delta: -1, + }, + cx, + ); + }), + )) + .child(transport_button( + "gpui-widgets-viewer-play", + play_icon, + play_label, + if playing { + crate::i18n::tr("viewer.pause", "暂停") + } else { + crate::i18n::tr("viewer.play", "播放") + }, + cx.listener(|this, _event: &ClickEvent, _window, cx| { + let event = if this.transport.playing { + ViewerEvent::PauseRequested { + control: this.control, + } + } else { + ViewerEvent::PlayRequested { + control: this.control, + } + }; + this.emit(event, cx); + }), + )) + .child(transport_button( + "gpui-widgets-viewer-step-forward", + step_forward_icon, + "⏩", + crate::i18n::tr("viewer.step_forward", "下一帧"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit( + ViewerEvent::StepRequested { + control: this.control, + delta: 1, + }, + cx, + ); + }), + )) + .child(transport_button( + "gpui-widgets-viewer-out", + out_icon, + "⏭", + crate::i18n::tr("viewer.out_point", "出点"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit( + ViewerEvent::OutPointRequested { + control: this.control, + }, + cx, + ); + }), + )) + .child(transport_button( + "gpui-widgets-viewer-clear-range", + None, + x_glyph(colors.text), + crate::i18n::tr("viewer.clear_range", "清除入出点"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.emit( + ViewerEvent::ClearRangeRequested { + control: this.control, + }, + cx, + ); + }), + )) + .child(div().px_2().text_color(colors.text).child(timecode)) + .child(div().flex_1()) + .child(button( + "gpui-widgets-viewer-safe", + crate::i18n::tr("viewer.safe_frames", "安全框"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.show_safe_frames = !this.show_safe_frames; + this.emit( + ViewerEvent::ToggleSafeFramesRequested { + control: this.control, + }, + cx, + ); + }), + )) + .child(button( + "gpui-widgets-viewer-zoom", + crate::i18n::tr("viewer.zoom", "缩放"), + cx.listener(|this, _event: &ClickEvent, _window, cx| { + this.zoom = !this.zoom; + this.emit( + ViewerEvent::ToggleZoomRequested { + control: this.control, + }, + cx, + ); + }), + )); - div().size_full().flex().flex_col().child(picture).child(transport_bar) - } + div() + .size_full() + .flex() + .flex_col() + .child(picture) + .child(transport_bar) + } } /// A transport icon button: a 16px icon on a 24px hit target with a /// localized tooltip. Falls back to the `fallback` glyph when `icon` is /// `None` (no resolver registered, or no file for the name). fn transport_button( - id: &'static str, - icon: Option, - fallback: impl IntoElement, - tooltip: SharedString, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + id: &'static str, + icon: Option, + fallback: impl IntoElement, + tooltip: SharedString, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> impl IntoElement { - let mut el = div() - .id(id) - .debug_selector(move || id.into()) - .w(px(24.0)) - .h(px(24.0)) - .flex() - .items_center() - .justify_center() - .rounded_md() - .cursor_pointer() - .hover(|style| style.bg(gpui::colors::Colors::dark().selected)) - .tooltip(move |window, cx| tooltip_view(tooltip.clone(), window, cx)) - .on_click(on_click); - if let Some(path) = icon { - el = el.child(img(path).w(px(16.0)).h(px(16.0))); - } else { - el = el.child(fallback); - } - el + let mut el = div() + .id(id) + .debug_selector(move || id.into()) + .w(px(24.0)) + .h(px(24.0)) + .flex() + .items_center() + .justify_center() + .rounded_md() + .cursor_pointer() + .hover(|style| style.bg(gpui::colors::Colors::dark().selected)) + .tooltip(move |window, cx| tooltip_view(tooltip.clone(), window, cx)) + .on_click(on_click); + if let Some(path) = icon { + el = el.child(img(path).w(px(16.0)).h(px(16.0))); + } else { + el = el.child(fallback); + } + el } /// A small painted ✕ (clear-range / close), drawn with a canvas so it stays /// crisp and theme-colored instead of relying on a font glyph that may /// rasterize faintly or not at all. fn x_glyph(color: gpui::Rgba) -> impl IntoElement { - use gpui::{canvas, point, px, Bounds, PathBuilder, Pixels}; + use gpui::{Bounds, PathBuilder, Pixels, canvas, point, px}; - canvas( - move |_bounds, _window, _cx| (), - move |bounds: Bounds, (), window, cx| { - let _ = cx; - // Two diagonal strokes across the 16px box, with a small inset so - // the mark reads as a clean X. - let inset = px(4.0); - for stroke in [true, false] { - let mut path = PathBuilder::stroke(px(1.5)); - let (x0, x1) = if stroke { - (bounds.left() + inset, bounds.right() - inset) - } else { - (bounds.right() - inset, bounds.left() + inset) - }; - path.move_to(point(x0, bounds.top() + inset)); - path.line_to(point(x1, bounds.bottom() - inset)); - if let Ok(path) = path.build() { - window.paint_path(path, color); - } - } - }, - ) + canvas( + move |_bounds, _window, _cx| (), + move |bounds: Bounds, (), window, cx| { + let _ = cx; + // Two diagonal strokes across the 16px box, with a small inset so + // the mark reads as a clean X. + let inset = px(4.0); + for stroke in [true, false] { + let mut path = PathBuilder::stroke(px(1.5)); + let (x0, x1) = if stroke { + (bounds.left() + inset, bounds.right() - inset) + } else { + (bounds.right() - inset, bounds.left() + inset) + }; + path.move_to(point(x0, bounds.top() + inset)); + path.line_to(point(x1, bounds.bottom() - inset)); + if let Ok(path) = path.build() { + window.paint_path(path, color); + } + } + }, + ) } /// A small labeled button. fn button( - id: &'static str, - label: impl IntoElement, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + id: &'static str, + label: impl IntoElement, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> impl IntoElement { - div() - .id(id) - .debug_selector(move || id.into()) - .px_2() - .py_1() - .rounded_md() - .cursor_pointer() - .hover(|style| style.bg(gpui::colors::Colors::dark().selected)) - .on_click(on_click) - .child(label) + div() + .id(id) + .debug_selector(move || id.into()) + .px_2() + .py_1() + .rounded_md() + .cursor_pointer() + .hover(|style| style.bg(gpui::colors::Colors::dark().selected)) + .on_click(on_click) + .child(label) } #[cfg(test)] mod tests { - use super::*; - use gpui::timeline::{Frame, FrameRate}; - use gpui::{Modifiers, TestAppContext, VisualTestContext, px, size}; + use super::*; + use gpui::timeline::{Frame, FrameRate}; + use gpui::{Modifiers, TestAppContext, VisualTestContext, px, size}; - struct MockClock { - frame: Frame, - playing: bool, - } - impl PlaybackClock for MockClock { - fn current_frame(&self) -> Frame { - self.frame - } - fn is_playing(&self) -> bool { - self.playing - } - fn frame_rate(&self) -> FrameRate { - FrameRate::new(30, 1) - } - } + struct MockClock { + frame: Frame, + playing: bool, + } + impl PlaybackClock for MockClock { + fn current_frame(&self) -> Frame { + self.frame + } + fn is_playing(&self) -> bool { + self.playing + } + fn frame_rate(&self) -> FrameRate { + FrameRate::new(30, 1) + } + } - struct Host { - viewer: Entity>, - events: Vec, - } - impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child(self.viewer.clone()) - } - } + struct Host { + viewer: Entity>, + events: Vec, + } + impl Render for Host { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.viewer.clone()) + } + } - #[gpui::test] - async fn play_button_emits_play_request(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - // The play button sits on the left of the transport bar at the bottom. - let play = cx - .debug_bounds("gpui-widgets-viewer-play") - .expect("play button rendered"); - cx.simulate_click(play.center(), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn play_button_emits_play_request(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + // The play button sits on the left of the transport bar at the bottom. + let play = cx + .debug_bounds("gpui-widgets-viewer-play") + .expect("play button rendered"); + cx.simulate_click(play.center(), Modifiers::none()); + cx.run_until_parked(); - let requested = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, ViewerEvent::PlayRequested { control: 1 }) - }) - }); - assert!(requested, "expected a PlayRequested event"); - } + let requested = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, ViewerEvent::PlayRequested { control: 1 })) + }); + assert!(requested, "expected a PlayRequested event"); + } - #[test] - fn timecode_formatting_reuses_timeline() { - let frame = Frame(3000); - let text = format_timecode(frame, FrameRate::new(30, 1), TimeDisplay::Timecode); - assert_eq!(text, "00:01:40:00"); - } + #[test] + fn timecode_formatting_reuses_timeline() { + let frame = Frame(3000); + let text = format_timecode(frame, FrameRate::new(30, 1), TimeDisplay::Timecode); + assert_eq!(text, "00:01:40:00"); + } - #[gpui::test] - async fn cpu_frame_source_renders_without_a_platform_surface(cx: &mut TestAppContext) { - // The CPU-frame path (for non-macOS platforms without CVPixelBuffer) - // accepts raw BGRA8 bytes in a RenderImage and renders through the - // sprite atlas — no SurfaceSource involved. - use gpui::RenderImage; - use image::{Frame, RgbaImage}; + #[gpui::test] + async fn cpu_frame_source_renders_without_a_platform_surface(cx: &mut TestAppContext) { + // The CPU-frame path (for non-macOS platforms without CVPixelBuffer) + // accepts raw BGRA8 bytes in a RenderImage and renders through the + // sprite atlas — no SurfaceSource involved. + use gpui::RenderImage; + use image::{Frame, RgbaImage}; - // A 2x2 opaque red frame, converted RGBA -> BGRA as gpui expects. - let mut rgba = RgbaImage::from_pixel(2, 2, image::Rgba([255, 0, 0, 255])); - for pixel in rgba.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - let frame = Arc::new(RenderImage::new(smallvec::SmallVec::from_elem( - Frame::new(rgba), - 1, - ))); + // A 2x2 opaque red frame, converted RGBA -> BGRA as gpui expects. + let mut rgba = RgbaImage::from_pixel(2, 2, image::Rgba([255, 0, 0, 255])); + for pixel in rgba.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + let frame = Arc::new(RenderImage::new(smallvec::SmallVec::from_elem( + Frame::new(rgba), + 1, + ))); - let (cx, host) = make_host(cx); - cx.update(|window, app| { - host.read(app) - .viewer - .clone() - .update(app, |viewer, cx| viewer.set_cpu_frame(Some(frame), cx)); - window.draw(app); - }); - cx.run_until_parked(); + let (cx, host) = make_host(cx); + cx.update(|window, app| { + host.read(app) + .viewer + .clone() + .update(app, |viewer, cx| viewer.set_cpu_frame(Some(frame), cx)); + window.draw(app); + }); + cx.run_until_parked(); - let is_cpu = cx.read(|app| { - matches!( - host.read(app).viewer.read(app).frame_source, - Some(ViewerFrameSource::CpuFrame(_)) - ) - }); - assert!(is_cpu, "the frame source should be the CPU-frame variant"); + let is_cpu = cx.read(|app| { + matches!( + host.read(app).viewer.read(app).frame_source, + Some(ViewerFrameSource::CpuFrame(_)) + ) + }); + assert!(is_cpu, "the frame source should be the CPU-frame variant"); - // Clearing the CPU frame falls back to the placeholder. - cx.update(|window, app| { - host.read(app) - .viewer - .clone() - .update(app, |viewer, cx| viewer.set_cpu_frame(None, cx)); - window.draw(app); - }); - cx.run_until_parked(); - let is_none = cx.read(|app| host.read(app).viewer.read(app).frame_source.is_none()); - assert!(is_none); - } + // Clearing the CPU frame falls back to the placeholder. + cx.update(|window, app| { + host.read(app) + .viewer + .clone() + .update(app, |viewer, cx| viewer.set_cpu_frame(None, cx)); + window.draw(app); + }); + cx.run_until_parked(); + let is_none = cx.read(|app| host.read(app).viewer.read(app).frame_source.is_none()); + assert!(is_none); + } - fn make_host(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { - cx.update(|cx| cx.init_colors()); - let window = cx.open_window(size(px(640.0), px(420.0)), |window, cx| { - let clock = cx.new(|_| MockClock { - frame: Frame(0), - playing: false, - }); - let viewer = cx.new(|cx| ViewerWidget::new(1, clock, window, cx)); - let host = Host { - viewer, - events: Vec::new(), - }; - cx.subscribe( - &host.viewer, - |host: &mut Host, - _v: Entity>, - event: &ViewerEvent, - _cx: &mut Context| { - host.events.push(event.clone()); - }, - ) - .detach(); - host - }); - cx.run_until_parked(); - let host = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); - (cx, host) - } + fn make_host(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity) { + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(640.0), px(420.0)), |window, cx| { + let clock = cx.new(|_| MockClock { + frame: Frame(0), + playing: false, + }); + let viewer = cx.new(|cx| ViewerWidget::new(1, clock, window, cx)); + let host = Host { + viewer, + events: Vec::new(), + }; + cx.subscribe( + &host.viewer, + |host: &mut Host, + _v: Entity>, + event: &ViewerEvent, + _cx: &mut Context| { + host.events.push(event.clone()); + }, + ) + .detach(); + host + }); + cx.run_until_parked(); + let host = window.root(cx).unwrap(); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + (cx, host) + } - #[gpui::test] - async fn step_button_emits_step_request(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - let step = cx - .debug_bounds("gpui-widgets-viewer-step-forward") - .expect("step button rendered"); - cx.simulate_click(step.center(), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn step_button_emits_step_request(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + let step = cx + .debug_bounds("gpui-widgets-viewer-step-forward") + .expect("step button rendered"); + cx.simulate_click(step.center(), Modifiers::none()); + cx.run_until_parked(); - let requested = cx.read(|app| { - host.read(app).events.iter().any(|e| { - matches!(e, ViewerEvent::StepRequested { delta: 1, .. }) - }) - }); - assert!(requested, "expected a StepRequested(+1) event"); - } + let requested = cx.read(|app| { + host.read(app) + .events + .iter() + .any(|e| matches!(e, ViewerEvent::StepRequested { delta: 1, .. })) + }); + assert!(requested, "expected a StepRequested(+1) event"); + } - #[gpui::test] - async fn safe_frame_toggle_emits_and_switches(cx: &mut TestAppContext) { - let (cx, host) = make_host(cx); - let toggle = cx - .debug_bounds("gpui-widgets-viewer-safe") - .expect("safe-frame button rendered"); - cx.simulate_click(toggle.center(), Modifiers::none()); - cx.run_until_parked(); + #[gpui::test] + async fn safe_frame_toggle_emits_and_switches(cx: &mut TestAppContext) { + let (cx, host) = make_host(cx); + let toggle = cx + .debug_bounds("gpui-widgets-viewer-safe") + .expect("safe-frame button rendered"); + cx.simulate_click(toggle.center(), Modifiers::none()); + cx.run_until_parked(); - let (requested, shown) = cx.read(|app| { - let host = host.read(app); - ( - host.events.iter().any(|e| { - matches!(e, ViewerEvent::ToggleSafeFramesRequested { .. }) - }), - host.viewer.read(app).show_safe_frames, - ) - }); - assert!(requested, "expected a ToggleSafeFramesRequested event"); - assert!(shown, "safe frames should now be shown locally"); - } + let (requested, shown) = cx.read(|app| { + let host = host.read(app); + ( + host.events + .iter() + .any(|e| matches!(e, ViewerEvent::ToggleSafeFramesRequested { .. })), + host.viewer.read(app).show_safe_frames, + ) + }); + assert!(requested, "expected a ToggleSafeFramesRequested event"); + assert!(shown, "safe frames should now be shown locally"); + } } diff --git a/crates/gpui_widgets/src/viewer/transport.rs b/crates/gpui_widgets/src/viewer/transport.rs index a2243b0996..36b793475c 100644 --- a/crates/gpui_widgets/src/viewer/transport.rs +++ b/crates/gpui_widgets/src/viewer/transport.rs @@ -7,150 +7,150 @@ use gpui::timeline::Frame; /// The playback state of a viewer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TransportState { - /// The current playhead frame. - pub frame: Frame, - /// Whether playback is running (the engine drives the position; the - /// view only reflects it). - pub playing: bool, - /// The loop-in point, if set. - pub in_point: Option, - /// The loop-out point, if set. - pub out_point: Option, - /// Whether playback loops between the in/out points. - pub loop_range: bool, + /// The current playhead frame. + pub frame: Frame, + /// Whether playback is running (the engine drives the position; the + /// view only reflects it). + pub playing: bool, + /// The loop-in point, if set. + pub in_point: Option, + /// The loop-out point, if set. + pub out_point: Option, + /// Whether playback loops between the in/out points. + pub loop_range: bool, } impl Default for TransportState { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl TransportState { - /// Create a stopped transport at frame 0. - pub fn new() -> Self { - Self { - frame: Frame(0), - playing: false, - in_point: None, - out_point: None, - loop_range: false, - } - } + /// Create a stopped transport at frame 0. + pub fn new() -> Self { + Self { + frame: Frame(0), + playing: false, + in_point: None, + out_point: None, + loop_range: false, + } + } - /// Toggle play/pause; returns the new state. - pub fn toggle_play(&mut self) -> bool { - self.playing = !self.playing; - self.playing - } + /// Toggle play/pause; returns the new state. + pub fn toggle_play(&mut self) -> bool { + self.playing = !self.playing; + self.playing + } - /// Advance one frame. Clamps at `length` (exclusive); when a loop range - /// is active the position wraps to `in_point` instead of stopping at - /// `out_point`. - pub fn advance(&mut self, length: Frame) -> Frame { - let next = self.frame.0 + 1; - self.frame = if let Some(out) = self.out_point { - if self.loop_range && next >= out.0 { - Frame(self.in_point.unwrap_or(Frame(0)).0) - } else { - Frame(next.min(length.0 - 1)) - } - } else { - Frame(next.min(length.0 - 1)) - }; - self.frame - } + /// Advance one frame. Clamps at `length` (exclusive); when a loop range + /// is active the position wraps to `in_point` instead of stopping at + /// `out_point`. + pub fn advance(&mut self, length: Frame) -> Frame { + let next = self.frame.0 + 1; + self.frame = if let Some(out) = self.out_point { + if self.loop_range && next >= out.0 { + Frame(self.in_point.unwrap_or(Frame(0)).0) + } else { + Frame(next.min(length.0 - 1)) + } + } else { + Frame(next.min(length.0 - 1)) + }; + self.frame + } - /// Step the playhead by `delta` frames, clamped to `[0, length)`. - pub fn step(&mut self, delta: i64, length: Frame) -> Frame { - self.frame = Frame((self.frame.0 + delta).clamp(0, length.0 - 1)); - self.frame - } + /// Step the playhead by `delta` frames, clamped to `[0, length)`. + pub fn step(&mut self, delta: i64, length: Frame) -> Frame { + self.frame = Frame((self.frame.0 + delta).clamp(0, length.0 - 1)); + self.frame + } - /// Set the loop-in point at the current frame. - pub fn set_in_point(&mut self, length: Frame) { - self.in_point = Some(Frame(self.frame.0.min(length.0 - 1))); - self.loop_range = true; - } + /// Set the loop-in point at the current frame. + pub fn set_in_point(&mut self, length: Frame) { + self.in_point = Some(Frame(self.frame.0.min(length.0 - 1))); + self.loop_range = true; + } - /// Set the loop-out point at the current frame. - pub fn set_out_point(&mut self, length: Frame) { - self.out_point = Some(Frame(self.frame.0.min(length.0 - 1))); - self.loop_range = true; - } + /// Set the loop-out point at the current frame. + pub fn set_out_point(&mut self, length: Frame) { + self.out_point = Some(Frame(self.frame.0.min(length.0 - 1))); + self.loop_range = true; + } - /// Clear the loop range. - pub fn clear_range(&mut self) { - self.in_point = None; - self.out_point = None; - self.loop_range = false; - } + /// Clear the loop range. + pub fn clear_range(&mut self) { + self.in_point = None; + self.out_point = None; + self.loop_range = false; + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn toggle_play_flips() { - let mut t = TransportState::new(); - assert!(!t.playing); - assert!(t.toggle_play()); - assert!(!t.toggle_play()); - } + #[test] + fn toggle_play_flips() { + let mut t = TransportState::new(); + assert!(!t.playing); + assert!(t.toggle_play()); + assert!(!t.toggle_play()); + } - #[test] - fn advance_clamps_at_length() { - let mut t = TransportState::new(); - t.frame = Frame(9); - assert_eq!(t.advance(Frame(10)), Frame(9)); - assert_eq!(t.advance(Frame(10)), Frame(9)); - } + #[test] + fn advance_clamps_at_length() { + let mut t = TransportState::new(); + t.frame = Frame(9); + assert_eq!(t.advance(Frame(10)), Frame(9)); + assert_eq!(t.advance(Frame(10)), Frame(9)); + } - #[test] - fn advance_loops_within_range() { - let mut t = TransportState::new(); - t.frame = Frame(5); - t.in_point = Some(Frame(4)); - t.out_point = Some(Frame(8)); - t.loop_range = true; - assert_eq!(t.advance(Frame(100)), Frame(6)); - t.frame = Frame(7); - assert_eq!(t.advance(Frame(100)), Frame(4)); - } + #[test] + fn advance_loops_within_range() { + let mut t = TransportState::new(); + t.frame = Frame(5); + t.in_point = Some(Frame(4)); + t.out_point = Some(Frame(8)); + t.loop_range = true; + assert_eq!(t.advance(Frame(100)), Frame(6)); + t.frame = Frame(7); + assert_eq!(t.advance(Frame(100)), Frame(4)); + } - #[test] - fn advance_ignores_out_point_without_loop() { - let mut t = TransportState::new(); - t.out_point = Some(Frame(8)); - t.loop_range = false; - t.frame = Frame(7); - assert_eq!(t.advance(Frame(100)), Frame(8)); - } + #[test] + fn advance_ignores_out_point_without_loop() { + let mut t = TransportState::new(); + t.out_point = Some(Frame(8)); + t.loop_range = false; + t.frame = Frame(7); + assert_eq!(t.advance(Frame(100)), Frame(8)); + } - #[test] - fn step_clamps_to_range() { - let mut t = TransportState::new(); - t.frame = Frame(2); - assert_eq!(t.step(-5, Frame(10)), Frame(0)); - assert_eq!(t.step(100, Frame(10)), Frame(9)); - // 9 + 1 -> 10, clamped to the last index 9. - assert_eq!(t.step(1, Frame(10)), Frame(9)); - } + #[test] + fn step_clamps_to_range() { + let mut t = TransportState::new(); + t.frame = Frame(2); + assert_eq!(t.step(-5, Frame(10)), Frame(0)); + assert_eq!(t.step(100, Frame(10)), Frame(9)); + // 9 + 1 -> 10, clamped to the last index 9. + assert_eq!(t.step(1, Frame(10)), Frame(9)); + } - #[test] - fn in_out_points_respect_length() { - let mut t = TransportState::new(); - t.frame = Frame(50); - t.set_in_point(Frame(10)); - assert_eq!(t.in_point, Some(Frame(9))); - t.frame = Frame(50); - t.set_out_point(Frame(10)); - assert_eq!(t.out_point, Some(Frame(9))); - assert!(t.loop_range); - t.clear_range(); - assert_eq!(t.in_point, None); - assert_eq!(t.out_point, None); - assert!(!t.loop_range); - } + #[test] + fn in_out_points_respect_length() { + let mut t = TransportState::new(); + t.frame = Frame(50); + t.set_in_point(Frame(10)); + assert_eq!(t.in_point, Some(Frame(9))); + t.frame = Frame(50); + t.set_out_point(Frame(10)); + assert_eq!(t.out_point, Some(Frame(9))); + assert!(t.loop_range); + t.clear_range(); + assert_eq!(t.in_point, None); + assert_eq!(t.out_point, None); + assert!(!t.loop_range); + } } diff --git a/crates/gpui_windows/build.rs b/crates/gpui_windows/build.rs index 4d93b0a417..0b484983fe 100644 --- a/crates/gpui_windows/build.rs +++ b/crates/gpui_windows/build.rs @@ -1,244 +1,243 @@ #![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(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}, - }; + 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(); + 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()); + println!("cargo:rerun-if-changed={}", shader_path.display()); - // Check if fxc.exe is available - let fxc_path = find_fxc_compiler(); + // 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", - "blur_downsample", - "blur", - "blur_composite", - ]; + // Define all modules + let modules = [ + "quad", + "shadow", + "path_rasterization", + "path_sprite", + "underline", + "monochrome_sprite", + "subpixel_sprite", + "polychrome_sprite", + "blur_downsample", + "blur", + "blur_composite", + ]; - 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 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, - ); - } - } + { + 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")?; + /// 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 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(); + 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::>() - }); + 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 - ))?, - }; + 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), - )); - } + if let Some(highest_version) = versions.last() { + return Ok(Some( + install_folder_bin + .join(highest_version) + .join(arch) + .join(binary), + )); + } - Ok(None) - } + 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; - } + /// 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(); - } + // 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(); - } + 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"); - } + 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); + 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); - } + // 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(); + 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); - } - } - } + 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"); - } + 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)))] diff --git a/crates/gpui_windows/src/clipboard.rs b/crates/gpui_windows/src/clipboard.rs index cd0694ab31..35f0429f95 100644 --- a/crates/gpui_windows/src/clipboard.rs +++ b/crates/gpui_windows/src/clipboard.rs @@ -4,385 +4,385 @@ 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}, + 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, + 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"))); + 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"))); + 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"))); + 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"))); + LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF"))); static CLIPBOARD_PNG_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG"))); + LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG"))); static CLIPBOARD_JPG_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF"))); + 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 + 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 + 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) + 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 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(()) - })(); + 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}"); - } + 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 _clip = ClipboardGuard::open()?; - let mut entries = Vec::new(); - let mut have_text = false; - let mut have_image = false; - let mut have_files = false; + 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) }; + 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 !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 }) + 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), + 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), - } - } + 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(()) + 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])) + 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 + 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)?; + 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)?; + 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(()) + 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)?; - } + 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(()) + // 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) + 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 })) + 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) + 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, - })) + 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(), - ))) + 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; - } + 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 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 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 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) + 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 - ); - } + 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 - } - } + 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 - } - } - } + 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}"); - } - } + 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, + 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 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) } - } + 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() }; - } + fn drop(&mut self) { + unsafe { GlobalUnlock(self.global).ok() }; + } } diff --git a/crates/gpui_windows/src/destination_list.rs b/crates/gpui_windows/src/destination_list.rs index d6967c01d2..5da6aaff93 100644 --- a/crates/gpui_windows/src/destination_list.rs +++ b/crates/gpui_windows/src/destination_list.rs @@ -3,205 +3,205 @@ 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}, + 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]>]>, + 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) 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, + 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."), - } - } + 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)], + 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) + 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, + 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 list: ICustomDestinationList = + unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?; - let mut slots = 0; - let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?; + 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 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(); + 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); - } + removed.push(args); + } - Ok((list, removed)) + Ok((list, removed)) } fn add_dock_menu( - list: &ICustomDestinationList, - dock_menus: &[(SharedString, SharedString)], + 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(()) - } + 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>, + list: &ICustomDestinationList, + entries: &[SmallVec<[PathBuf; 2]>], + removed: &Vec>, ) -> anyhow::Result<()> { - unsafe { - let tasks: IObjectCollection = - CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; + 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(" "), - ); + 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 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(", "); + 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, - )?)?; - } + 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(()) - } + 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, + 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()?; + 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) - } + Ok(link) + } } diff --git a/crates/gpui_windows/src/direct_manipulation.rs b/crates/gpui_windows/src/direct_manipulation.rs index 08a1e5243e..25827f415d 100644 --- a/crates/gpui_windows/src/direct_manipulation.rs +++ b/crates/gpui_windows/src/direct_manipulation.rs @@ -5,10 +5,10 @@ use ::util::ResultExt; use anyhow::Result; use gpui::*; use windows::Win32::{ - Foundation::*, - Graphics::{DirectManipulation::*, Gdi::*}, - System::Com::*, - UI::{Input::Pointer::*, WindowsAndMessaging::*}, + Foundation::*, + Graphics::{DirectManipulation::*, Gdi::*}, + System::Com::*, + UI::{Input::Pointer::*, WindowsAndMessaging::*}, }; use crate::*; @@ -19,341 +19,341 @@ use crate::*; 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>>, + 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)?; + 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 update_manager: IDirectManipulationUpdateManager = manager.GetUpdateManager()?; - let viewport: IDirectManipulationViewport = manager.CreateViewport(None, window)?; + 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)?; + 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, - )?; + 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)?; + 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()?; + manager.Activate(window)?; + viewport.Enable()?; - let scale_factor = Rc::new(Cell::new(scale_factor)); - let pending_events = Rc::new(RefCell::new(Vec::new())); + 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 event_handler: IDirectManipulationViewportEventHandler = + DirectManipulationEventHandler::new( + window, + Rc::clone(&scale_factor), + Rc::clone(&pending_events), + ) + .into(); - let handler_cookie = viewport.AddEventHandler(Some(window), &event_handler)?; + let handler_cookie = viewport.AddEventHandler(Some(window), &event_handler)?; - update_manager.Update(None)?; + update_manager.Update(None)?; - Ok(Self { - manager, - update_manager, - viewport, - _handler_cookie: handler_cookie, - window, - scale_factor, - pending_events, - }) - } - } + 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 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 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 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()) - } + 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(); - } - } + 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, + 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>>, + 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 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 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) - } - } + 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(()); - } + 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(); - } + // 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(); + 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(); - } - } - } + // 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); - } + self.last_scale.set(1.0); + self.last_x_offset.set(0.0); + self.last_y_offset.set(0.0); + } - Ok(()) - } + Ok(()) + } - fn OnViewportUpdated( - &self, - _viewport: windows_core::Ref<'_, IDirectManipulationViewport>, - ) -> windows_core::Result<()> { - 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)?; + 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)?; - } + // 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; + 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(()); - } + 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(); + 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(()); - } + 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(); + 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); - } + // 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 => {} - } + 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); + self.last_scale.set(scale); + self.last_x_offset.set(x_offset); + self.last_y_offset.set(y_offset); - Ok(()) - } + 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) + const EPSILON_SCALE: f32 = 0.00001; + (f1 - f2).abs() < EPSILON_SCALE * f1.abs().max(f2.abs()).max(EPSILON_SCALE) } diff --git a/crates/gpui_windows/src/direct_write.rs b/crates/gpui_windows/src/direct_write.rs index 550d194da7..1a8580cb76 100644 --- a/crates/gpui_windows/src/direct_write.rs +++ b/crates/gpui_windows/src/direct_write.rs @@ -1,7 +1,7 @@ use std::{ - borrow::Cow, - ffi::{c_uint, c_void}, - mem::ManuallyDrop, + borrow::Cow, + ffi::{c_uint, c_void}, + mem::ManuallyDrop, }; use ::util::{ResultExt, maybe}; @@ -9,17 +9,17 @@ use anyhow::{Context, Result}; use collections::HashMap; 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::*, + 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; @@ -28,1897 +28,1897 @@ use gpui::*; #[derive(Debug)] struct FontInfo { - font_family_h: HSTRING, - font_face: IDWriteFontFace3, - features: IDWriteTypography, - fallbacks: Option, - font_collection: IDWriteFontCollection1, + font_family_h: HSTRING, + font_face: IDWriteFontFace3, + features: IDWriteTypography, + fallbacks: Option, + font_collection: IDWriteFontCollection1, } pub(crate) struct DirectWriteTextSystem { - components: DirectWriteComponents, - state: RwLock, + 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, + 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); - } - } + 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, + 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, + 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(); + 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 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 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 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() - }; + 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, - }) - } + 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()); + 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 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_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)? - }; + 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(), - }), - }) - } + 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) - } + 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 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 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_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 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 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 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_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 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 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 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 - } - } + 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: IDWriteTextLayout1 = components - .factory - .CreateTextLayout(text_wide, &format, f32::INFINITY, f32::INFINITY)? - .cast()?; - 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)?; - if let Some(spacing) = first_run.letter_spacing { - layout.SetCharacterSpacing(0.0, spacing.as_f32(), 0.0, 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)?; - if let Some(spacing) = run.letter_spacing { - text_layout.SetCharacterSpacing(0.0, spacing.as_f32(), 0.0, text_range)?; - } - - break_ligatures = !break_ligatures; - } - - let mut runs = Vec::new(); - let renderer_context = RendererContext { - text_system: self, - components, - index_converter: StringIndexConverter::new(text), - runs: &mut runs, - width: 0.0, - }; - text_layout.Draw( - Some((&raw const renderer_context).cast::()), - &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> { - 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 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 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 - }; - - 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 { 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)]; - 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::(), - ) - }; - } - - // 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) - } + 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: IDWriteTextLayout1 = components + .factory + .CreateTextLayout(text_wide, &format, f32::INFINITY, f32::INFINITY)? + .cast()?; + 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)?; + if let Some(spacing) = first_run.letter_spacing { + layout.SetCharacterSpacing(0.0, spacing.as_f32(), 0.0, 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)?; + if let Some(spacing) = run.letter_spacing { + text_layout.SetCharacterSpacing(0.0, spacing.as_f32(), 0.0, text_range)?; + } + + break_ligatures = !break_ligatures; + } + + let mut runs = Vec::new(); + let renderer_context = RendererContext { + text_system: self, + components, + index_converter: StringIndexConverter::new(text), + runs: &mut runs, + width: 0.0, + }; + text_layout.Draw( + Some((&raw const renderer_context).cast::()), + &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> { + 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 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 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 + }; + + 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 { 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)]; + 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::(), + ) + }; + } + + // 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, + 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; + 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 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() - }; + 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, - ) - }; + 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, - }) - } + 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], + 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()) - } + fn new(locale_str: HSTRING) -> Self { + let inner = TextRenderer::new(locale_str); + TextRendererWrapper(inner.into()) + } } #[implement(IDWriteTextRenderer)] struct TextRenderer { - locale: HSTRING, + locale: HSTRING, } impl TextRenderer { - fn new(locale: HSTRING) -> Self { - TextRenderer { locale } - } + 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, + 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], + 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, - } - } + 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); + 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; + 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; - } + // 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; + 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 - }; + // 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; + let glyph_count = next_glyph - current_glyph; - // Update state for next call - self.utf16_idx = end_utf16_idx; - self.glyph_idx = next_glyph; + // Update state for next call + self.utf16_idx = end_utf16_idx; + self.glyph_idx = next_glyph; - Some((utf16_len, glyph_count)) - } + 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 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 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) - } + 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", - )); - }; + 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 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 color_font = unsafe { font_face.IsColorFont().as_bool() }; - let glyph_ids = unsafe { std::slice::from_raw_parts(glyphrun.glyphIndices, glyph_count) }; - let glyph_advances = - unsafe { std::slice::from_raw_parts(glyphrun.glyphAdvances, glyph_count) }; - let glyph_offsets = - unsafe { std::slice::from_raw_parts(glyphrun.glyphOffsets, glyph_count) }; - let cluster_map = - unsafe { std::slice::from_raw_parts(desc.clusterMap, desc.stringLength as usize) }; + let glyph_ids = unsafe { std::slice::from_raw_parts(glyphrun.glyphIndices, glyph_count) }; + let glyph_advances = + unsafe { std::slice::from_raw_parts(glyphrun.glyphAdvances, glyph_count) }; + let glyph_offsets = + unsafe { std::slice::from_raw_parts(glyphrun.glyphOffsets, glyph_count) }; + let cluster_map = + unsafe { std::slice::from_raw_parts(desc.clusterMap, desc.stringLength as usize) }; - 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(()) - } + 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 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 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", - )) - } + 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", + )) + } } struct StringIndexConverter<'a> { - text: &'a str, - utf8_ix: usize, - utf16_ix: usize, + 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, - } - } + 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(); - } + #[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 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, - } + 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!(), - } + 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) + DWRITE_FONT_WEIGHT(weight.0 as i32) } fn font_weight_from_dwrite(value: DWRITE_FONT_WEIGHT) -> FontWeight { - FontWeight(value.0 as f32) + FontWeight(value.0 as f32) } fn get_font_names_from_collection( - collection: &IDWriteFontCollection1, - locale: &HSTRING, + 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); - } + 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 - } + 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, - }) + 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, + direct_write_features: &IDWriteTypography, + features: &FontFeatures, ) -> Result<()> { - let tag_values = features.tag_value_list(); - if tag_values.is_empty() { - return Ok(()); - } + 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); + // 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; - } + 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)?; - } + 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(()) + 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, - } + 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]]) + 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)) + 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 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)?; - } + 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])) + 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 - } + 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 - } + 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, + 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() + 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 crate::direct_write::ClusterAnalyzer; + use crate::direct_write::ClusterAnalyzer; - #[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); + #[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); - } + 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); + } } diff --git a/crates/gpui_windows/src/directx_atlas.rs b/crates/gpui_windows/src/directx_atlas.rs index a6642dc7dc..cdc351227e 100644 --- a/crates/gpui_windows/src/directx_atlas.rs +++ b/crates/gpui_windows/src/directx_atlas.rs @@ -2,319 +2,319 @@ 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::*, + 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, + 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, + 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, + 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 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 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(); - } + 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 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(); + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); - let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { - return; - }; + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; - let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Subpixel => &mut lock.subpixel_textures, - }; + 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; - }; + let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else { + return; + }; - if let Some(mut texture) = texture_slot.take() { - texture.decrement_ref_count(); - if texture.is_unreferenced() { - textures.free_list.push(texture.id.index as usize); - } else { - *texture_slot = Some(texture); - } - } - } + if let Some(mut texture) = texture_slot.take() { + 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, - }; + 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); - } - } + 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) - } + 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(); + 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() - } - } + 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() - } - } - } + 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 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], - ) { - 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 upload( + &self, + device_context: &ID3D11DeviceContext, + bounds: Bounds, + bytes: &[u8], + ) { + 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 decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } - fn is_unreferenced(&mut self) -> bool { - self.live_atlas_keys == 0 - } + 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()) + 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), - } + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } } diff --git a/crates/gpui_windows/src/directx_devices.rs b/crates/gpui_windows/src/directx_devices.rs index 882e404a56..6e43284722 100644 --- a/crates/gpui_windows/src/directx_devices.rs +++ b/crates/gpui_windows/src/directx_devices.rs @@ -2,193 +2,193 @@ use anyhow::{Context, Result}; use itertools::Itertools; use util::ResultExt; 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, - }, - }, + 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") + (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, + 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!(), - } + 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, - }) - } + 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}; + #[cfg(debug_assertions)] + { + use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue}; - unsafe { DXGIGetDebugInterface1::(0) } - .log_err() - .is_some() - } - #[cfg(not(debug_assertions))] - { - false - } + 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)?) } + 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, + dxgi_factory: &IDXGIFactory6, + debug_layer_available: bool, ) -> Result<( - IDXGIAdapter1, - ID3D11Device, - ID3D11DeviceContext, - D3D_FEATURE_LEVEL, + 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)); - } - } + 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!() + unreachable!() } #[inline] fn get_device( - adapter: &IDXGIAdapter1, - context: Option<*mut Option>, - feature_level: Option<*mut D3D_FEATURE_LEVEL>, - debug_layer_available: bool, + 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" - )) - } + 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_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index d514555051..00f647ff74 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -1,22 +1,22 @@ use std::{ - slice, - sync::{Arc, OnceLock}, + slice, + sync::{Arc, OnceLock}, }; use ::util::ResultExt; use anyhow::{Context, Result}; use windows::{ - Win32::{ - Foundation::HWND, - Graphics::{ - Direct3D::*, - Direct3D11::*, - DirectComposition::*, - DirectWrite::*, - Dxgi::{Common::*, *}, - }, - }, - core::Interface, + Win32::{ + Foundation::HWND, + Graphics::{ + Direct3D::*, + Direct3D11::*, + DirectComposition::*, + DirectWrite::*, + Dxgi::{Common::*, *}, + }, + }, + core::Interface, }; use crate::directx_renderer::shader_resources::{RawShaderBytes, ShaderModule, ShaderTarget}; @@ -30,9 +30,9 @@ use gpui::*; /// forcing this backend to handle (or deliberately ignore) the new filter rather than silently /// dropping it. fn max_blur_radius(filters: &[ScaledFilter]) -> f32 { - filters.iter().fold(0.0, |acc, filter| match filter { - ScaledFilter::Blur(radius) => acc.max(radius.0), - }) + filters.iter().fold(0.0, |acc, filter| match filter { + ScaledFilter::Blur(radius) => acc.max(radius.0), + }) } pub(crate) const DISABLE_DIRECT_COMPOSITION: &str = "GPUI_DISABLE_DIRECT_COMPOSITION"; @@ -47,65 +47,65 @@ const PATH_MULTISAMPLE_COUNT: u32 = 4; const MAX_FILTER_DEPTH: usize = 2; pub(crate) struct FontInfo { - pub gamma_ratios: [f32; 4], - pub grayscale_enhanced_contrast: f32, - pub subpixel_enhanced_contrast: f32, - pub is_bgr: bool, + 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, + hwnd: HWND, + atlas: Arc, + devices: Option, + resources: Option, + globals: DirectXGlobalElements, + pipelines: DirectXRenderPipelines, + direct_composition: Option, + font_info: &'static FontInfo, - width: u32, - height: u32, + width: u32, + height: u32, - /// Whether we want to skip drawing 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, + /// Whether we want to skip drawing 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, - /// The render target currently bound for the main scene this frame (the offscreen - /// `scene_color` when blur filters are present, a content-filter group texture inside such a - /// group, or the swapchain otherwise). `draw_paths_to_intermediate` restores to this after - /// its own pass so paths land on the correct target. - active_render_target: Option, + /// The render target currently bound for the main scene this frame (the offscreen + /// `scene_color` when blur filters are present, a content-filter group texture inside such a + /// group, or the swapchain otherwise). `draw_paths_to_intermediate` restores to this after + /// its own pass so paths land on the correct target. + active_render_target: Option, } /// 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, + pub(crate) adapter: IDXGIAdapter1, + pub(crate) dxgi_factory: IDXGIFactory6, + pub(crate) device: ID3D11Device, + pub(crate) device_context: ID3D11DeviceContext, + dxgi_device: Option, } struct DirectXResources { - // Direct3D rendering objects - swap_chain: IDXGISwapChain1, - render_target: Option, - render_target_view: Option, + // 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, + // Path intermediate textures (with MSAA) + path_intermediate_texture: ID3D11Texture2D, + path_intermediate_srv: Option, + path_intermediate_msaa_texture: ID3D11Texture2D, + path_intermediate_msaa_view: Option, - // Offscreen targets for blur filters (each is render-target + shader-resource). - blur: BlurResources, + // Offscreen targets for blur filters (each is render-target + shader-resource). + blur: BlurResources, - // Cached viewport - viewport: D3D11_VIEWPORT, + // Cached viewport + viewport: D3D11_VIEWPORT, } /// Offscreen render targets used by the blur filters. The scene is rendered into `scene_color` @@ -114,361 +114,361 @@ struct DirectXResources { /// (indexed by isolation depth), up to [`MAX_FILTER_DEPTH`], so nested content blurs isolate /// correctly; deeper nests render inline. struct BlurResources { - #[expect(dead_code)] - scene_color: ID3D11Texture2D, - scene_color_rtv: Option, - scene_color_srv: Option, - #[expect(dead_code)] - ping: ID3D11Texture2D, - ping_rtv: Option, - ping_srv: Option, - #[expect(dead_code)] - pong: ID3D11Texture2D, - pong_rtv: Option, - pong_srv: Option, - // Kept alive for the lifetime of their views; indexed by isolation depth. - #[expect(dead_code)] - groups: Vec, - group_rtvs: Vec>, - group_srvs: Vec>, + #[expect(dead_code)] + scene_color: ID3D11Texture2D, + scene_color_rtv: Option, + scene_color_srv: Option, + #[expect(dead_code)] + ping: ID3D11Texture2D, + ping_rtv: Option, + ping_srv: Option, + #[expect(dead_code)] + pong: ID3D11Texture2D, + pong_rtv: Option, + pong_srv: Option, + // Kept alive for the lifetime of their views; indexed by isolation depth. + #[expect(dead_code)] + groups: Vec, + group_rtvs: Vec>, + group_srvs: Vec>, } impl BlurResources { - fn new(device: &ID3D11Device, width: u32, height: u32) -> Result { - let half_w = (width / 2).max(1); - let half_h = (height / 2).max(1); - let (scene_color, scene_color_rtv, scene_color_srv) = - create_color_target(device, width, height)?; - let (ping, ping_rtv, ping_srv) = create_color_target(device, half_w, half_h)?; - let (pong, pong_rtv, pong_srv) = create_color_target(device, half_w, half_h)?; - let mut groups = Vec::with_capacity(MAX_FILTER_DEPTH); - let mut group_rtvs = Vec::with_capacity(MAX_FILTER_DEPTH); - let mut group_srvs = Vec::with_capacity(MAX_FILTER_DEPTH); - for _ in 0..MAX_FILTER_DEPTH { - let (group, group_rtv, group_srv) = create_color_target(device, width, height)?; - groups.push(group); - group_rtvs.push(group_rtv); - group_srvs.push(group_srv); - } - Ok(Self { - scene_color, - scene_color_rtv, - scene_color_srv, - ping, - ping_rtv, - ping_srv, - pong, - pong_rtv, - pong_srv, - groups, - group_rtvs, - group_srvs, - }) - } + fn new(device: &ID3D11Device, width: u32, height: u32) -> Result { + let half_w = (width / 2).max(1); + let half_h = (height / 2).max(1); + let (scene_color, scene_color_rtv, scene_color_srv) = + create_color_target(device, width, height)?; + let (ping, ping_rtv, ping_srv) = create_color_target(device, half_w, half_h)?; + let (pong, pong_rtv, pong_srv) = create_color_target(device, half_w, half_h)?; + let mut groups = Vec::with_capacity(MAX_FILTER_DEPTH); + let mut group_rtvs = Vec::with_capacity(MAX_FILTER_DEPTH); + let mut group_srvs = Vec::with_capacity(MAX_FILTER_DEPTH); + for _ in 0..MAX_FILTER_DEPTH { + let (group, group_rtv, group_srv) = create_color_target(device, width, height)?; + groups.push(group); + group_rtvs.push(group_rtv); + group_srvs.push(group_srv); + } + Ok(Self { + scene_color, + scene_color_rtv, + scene_color_srv, + ping, + ping_rtv, + ping_srv, + pong, + pong_rtv, + pong_srv, + groups, + group_rtvs, + group_srvs, + }) + } } 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, - // Blur (backdrop-filter / filter). These don't use the generic PipelineState since they - // sample a texture rather than read a structured instance buffer; their parameters live in - // a dedicated constant buffer at register b1. - blur_downsample_vertex: ID3D11VertexShader, - blur_downsample_fragment: ID3D11PixelShader, - blur_vertex: ID3D11VertexShader, - blur_fragment: ID3D11PixelShader, - blur_composite_vertex: ID3D11VertexShader, - blur_composite_fragment: ID3D11PixelShader, - blur_params_buffer: ID3D11Buffer, - blur_blend_replace: ID3D11BlendState, - blur_blend_composite: ID3D11BlendState, + 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, + // Blur (backdrop-filter / filter). These don't use the generic PipelineState since they + // sample a texture rather than read a structured instance buffer; their parameters live in + // a dedicated constant buffer at register b1. + blur_downsample_vertex: ID3D11VertexShader, + blur_downsample_fragment: ID3D11PixelShader, + blur_vertex: ID3D11VertexShader, + blur_fragment: ID3D11PixelShader, + blur_composite_vertex: ID3D11VertexShader, + blur_composite_fragment: ID3D11PixelShader, + blur_params_buffer: ID3D11Buffer, + blur_blend_replace: ID3D11BlendState, + blur_blend_composite: ID3D11BlendState, } struct DirectXGlobalElements { - global_params_buffer: Option, - sampler: Option, + global_params_buffer: Option, + sampler: Option, } struct DirectComposition { - comp_device: IDCompositionDevice, - comp_target: IDCompositionTarget, - comp_visual: IDCompositionVisual, + 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")?) - }; + 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")?) + }; - Ok(Self { - adapter: adapter.clone(), - dxgi_factory: dxgi_factory.clone(), - device: device.clone(), - device_context: device_context.clone(), - dxgi_device, - }) - } + Ok(Self { + adapter: adapter.clone(), + dxgi_factory: dxgi_factory.clone(), + device: device.clone(), + device_context: device_context.clone(), + dxgi_device, + }) + } } 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."); - } + 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 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 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) - }; + 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, - active_render_target: None, - }) - } + 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, + active_render_target: None, + }) + } - pub(crate) fn sprite_atlas(&self) -> Arc { - self.atlas.clone() - } + 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))); - } - Ok(()) - } + 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))); + } + 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") - } + #[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") - }) - } + 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(); + 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(); - } + 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.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(); - } + 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 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) - }; + 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); + 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(()) - } + 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.pre_draw(&match background_appearance { - WindowBackgroundAppearance::Opaque => [1.0f32; 4], - _ => [0.0f32; 4], - })?; + 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.pre_draw(&match background_appearance { + WindowBackgroundAppearance::Opaque => [1.0f32; 4], + _ => [0.0f32; 4], + })?; - self.upload_scene_buffers(scene)?; + self.upload_scene_buffers(scene)?; - // Only route through the offscreen scene texture when the scene contains blur filters; - // otherwise render straight to the swapchain exactly as before. - let use_offscreen = - !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); + // Only route through the offscreen scene texture when the scene contains blur filters; + // otherwise render straight to the swapchain exactly as before. + let use_offscreen = + !scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty(); - // Clone the views we need (AddRef) so the loop can rebind render targets without holding a - // borrow of `self` across the `&mut self` draw_* calls. - let (scene_rtv, scene_srv, group_rtvs, group_srvs, swapchain_rtv) = { - let r = self.resources.as_ref().context("resources missing")?; - ( - r.blur.scene_color_rtv.clone(), - r.blur.scene_color_srv.clone(), - r.blur.group_rtvs.clone(), - r.blur.group_srvs.clone(), - r.render_target_view.clone(), - ) - }; - let ctx = self - .devices - .as_ref() - .context("devices missing")? - .device_context - .clone(); + // Clone the views we need (AddRef) so the loop can rebind render targets without holding a + // borrow of `self` across the `&mut self` draw_* calls. + let (scene_rtv, scene_srv, group_rtvs, group_srvs, swapchain_rtv) = { + let r = self.resources.as_ref().context("resources missing")?; + ( + r.blur.scene_color_rtv.clone(), + r.blur.scene_color_srv.clone(), + r.blur.group_rtvs.clone(), + r.blur.group_srvs.clone(), + r.render_target_view.clone(), + ) + }; + let ctx = self + .devices + .as_ref() + .context("devices missing")? + .device_context + .clone(); - if use_offscreen { - unsafe { - if let Some(rtv) = scene_rtv.as_ref() { - ctx.ClearRenderTargetView(rtv, &[0.0; 4]); - } - ctx.OMSetRenderTargets(Some(slice::from_ref(&scene_rtv)), None); - } - self.active_render_target = scene_rtv.clone(); - } else { - self.active_render_target = swapchain_rtv.clone(); - } + if use_offscreen { + unsafe { + if let Some(rtv) = scene_rtv.as_ref() { + ctx.ClearRenderTargetView(rtv, &[0.0; 4]); + } + ctx.OMSetRenderTargets(Some(slice::from_ref(&scene_rtv)), None); + } + self.active_render_target = scene_rtv.clone(); + } else { + self.active_render_target = swapchain_rtv.clone(); + } - // Current target for the main scene + a parent stack for content-filter groups. - let mut current_rtv = self.active_render_target.clone(); - let mut current_srv = if use_offscreen { - scene_srv.clone() - } else { - None - }; - // (parent_rtv, parent_srv, isolated) - let mut filter_stack: Vec<( - Option, - Option, - bool, - )> = Vec::new(); + // Current target for the main scene + a parent stack for content-filter groups. + let mut current_rtv = self.active_render_target.clone(); + let mut current_srv = if use_offscreen { + scene_srv.clone() + } else { + None + }; + // (parent_rtv, parent_srv, isolated) + let mut filter_stack: Vec<( + Option, + Option, + bool, + )> = Vec::new(); - for batch in scene.batches() { - match batch { + for batch in scene.batches() { + 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) => { @@ -571,878 +571,878 @@ impl DirectXRenderer { scene.polychrome_sprites.len(), scene.surfaces.len(), ))?; - } + } - // Present the offscreen scene by blitting it into the swapchain. - if use_offscreen { - self.dx_blit(&scene_srv, &swapchain_rtv)?; - } - self.active_render_target = None; - self.present() - } + // Present the offscreen scene by blitting it into the swapchain. + if use_offscreen { + self.dx_blit(&scene_srv, &swapchain_rtv)?; + } + self.active_render_target = None; + self.present() + } - 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; + 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(); + // 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")?; - } + // 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)?; + resources.recreate_resources(devices, width, height)?; - unsafe { - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - } + unsafe { + devices + .device_context + .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); + } - Ok(()) - } + Ok(()) + } - fn upload_scene_buffers(&mut self, scene: &Scene) -> Result<()> { - let devices = self.devices.as_ref().context("devices missing")?; + fn upload_scene_buffers(&mut self, scene: &Scene) -> Result<()> { + let devices = self.devices.as_ref().context("devices missing")?; - if !scene.shadows.is_empty() { - self.pipelines.shadow_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &scene.shadows, - )?; - } + 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.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.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.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.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, - )?; - } + if !scene.polychrome_sprites.is_empty() { + self.pipelines.poly_sprites.update_buffer( + &devices.device, + &devices.device_context, + &scene.polychrome_sprites, + )?; + } - Ok(()) - } + 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, - &devices.device_context, - slice::from_ref( - &self - .resources - .as_ref() - .context("resources missing")? - .viewport, - ), - slice::from_ref(&self.globals.global_params_buffer), - 4, - start as u32, - len as u32, - ) - } + 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, + &devices.device_context, + slice::from_ref( + &self + .resources + .as_ref() + .context("resources missing")? + .viewport, + ), + slice::from_ref(&self.globals.global_params_buffer), + 4, + 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, - &devices.device_context, - slice::from_ref( - &self - .resources - .as_ref() - .context("resources missing")? - .viewport, - ), - slice::from_ref(&self.globals.global_params_buffer), - 4, - 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, + &devices.device_context, + slice::from_ref( + &self + .resources + .as_ref() + .context("resources missing")? + .viewport, + ), + slice::from_ref(&self.globals.global_params_buffer), + 4, + start as u32, + len as u32, + ) + } - fn draw_paths_to_intermediate(&mut self, paths: &[Path]) -> Result<()> { - if paths.is_empty() { - return Ok(()); - } + 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, - ); - } + 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(); + // 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(), - })); - } + 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(), + })); + } - self.pipelines.path_rasterization_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &vertices, - )?; + self.pipelines.path_rasterization_pipeline.update_buffer( + &devices.device, + &devices.device_context, + &vertices, + )?; - self.pipelines.path_rasterization_pipeline.draw( - &devices.device_context, - slice::from_ref(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - vertices.len() as u32, - 1, - )?; + self.pipelines.path_rasterization_pipeline.draw( + &devices.device_context, + slice::from_ref(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 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 the active render target (the offscreen scene/group target when blurring, - // otherwise the swapchain) so the path sprites land on the correct surface. - let restore_target = if self.active_render_target.is_some() { - &self.active_render_target - } else { - &resources.render_target_view - }; - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(restore_target)), None); - } + // 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 the active render target (the offscreen scene/group target when blurring, + // otherwise the swapchain) so the path sprites land on the correct surface. + let restore_target = if self.active_render_target.is_some() { + &self.active_render_target + } else { + &resources.render_target_view + }; + devices + .device_context + .OMSetRenderTargets(Some(slice::from_ref(restore_target)), None); + } - Ok(()) - } + Ok(()) + } - fn draw_paths_from_intermediate(&mut self, paths: &[Path]) -> Result<()> { - let Some(first_path) = paths.first() else { - return 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 }] - }; + // 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, - )?; + 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(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - slice::from_ref(&self.globals.sampler), - sprites.len() as u32, - ) - } + // 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(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 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")?; - let resources = self.resources.as_ref().context("resources missing")?; - self.pipelines.underline_pipeline.draw_range( - &devices.device, - &devices.device_context, - slice::from_ref(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - 4, - start as u32, - 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")?; + let resources = self.resources.as_ref().context("resources missing")?; + self.pipelines.underline_pipeline.draw_range( + &devices.device, + &devices.device_context, + slice::from_ref(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 4, + 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 resources = self.resources.as_ref().context("resources missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.mono_sprites.draw_range_with_texture( - &devices.device, - &devices.device_context, - &texture_view, - slice::from_ref(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - slice::from_ref(&self.globals.sampler), - 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 resources = self.resources.as_ref().context("resources missing")?; + let texture_view = self.atlas.get_texture_view(texture_id); + self.pipelines.mono_sprites.draw_range_with_texture( + &devices.device, + &devices.device_context, + &texture_view, + slice::from_ref(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 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 resources = self.resources.as_ref().context("resources missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.subpixel_sprites.draw_range_with_texture( - &devices.device, - &devices.device_context, - &texture_view, - slice::from_ref(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - 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 resources = self.resources.as_ref().context("resources missing")?; + let texture_view = self.atlas.get_texture_view(texture_id); + self.pipelines.subpixel_sprites.draw_range_with_texture( + &devices.device, + &devices.device_context, + &texture_view, + slice::from_ref(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 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 resources = self.resources.as_ref().context("resources missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.poly_sprites.draw_range_with_texture( - &devices.device, - &devices.device_context, - &texture_view, - slice::from_ref(&resources.viewport), - slice::from_ref(&self.globals.global_params_buffer), - 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 resources = self.resources.as_ref().context("resources missing")?; + let texture_view = self.atlas.get_texture_view(texture_id); + self.pipelines.poly_sprites.draw_range_with_texture( + &devices.device, + &devices.device_context, + &texture_view, + slice::from_ref(&resources.viewport), + slice::from_ref(&self.globals.global_params_buffer), + 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(()) - } + fn draw_surfaces(&mut self, surfaces: &[PaintSurface]) -> Result<()> { + if surfaces.is_empty() { + return Ok(()); + } + Ok(()) + } - /// Run a single blur pass: a full-screen (or composite) draw sampling `source_srv` into - /// `target_rtv`, with `params` in the blur constant buffer (b1). - #[allow(clippy::too_many_arguments)] - fn dx_blur_pass( - &self, - vertex: &ID3D11VertexShader, - fragment: &ID3D11PixelShader, - blend: &ID3D11BlendState, - target_rtv: &Option, - source_srv: &Option, - params: BlurParams, - viewport: &D3D11_VIEWPORT, - topology: D3D_PRIMITIVE_TOPOLOGY, - vertex_count: u32, - clear: bool, - ) -> Result<()> { - let devices = self.devices.as_ref().context("devices missing")?; - let ctx = &devices.device_context; - update_buffer(ctx, &self.pipelines.blur_params_buffer, &[params])?; - let null_srv: [Option; 1] = [None]; - let blur_params = [Some(self.pipelines.blur_params_buffer.clone())]; - unsafe { - // Unbind any SRV at slot 0 so the target texture isn't simultaneously bound as input. - ctx.PSSetShaderResources(0, Some(&null_srv)); - if clear { - ctx.ClearRenderTargetView( - target_rtv.as_ref().context("blur target view missing")?, - &[0.0; 4], - ); - } - ctx.OMSetRenderTargets(Some(slice::from_ref(target_rtv)), None); - ctx.RSSetViewports(Some(slice::from_ref(viewport))); - ctx.IASetPrimitiveTopology(topology); - ctx.VSSetShader(vertex, None); - ctx.PSSetShader(fragment, None); - ctx.VSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); - ctx.PSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); - ctx.VSSetConstantBuffers(1, Some(&blur_params)); - ctx.PSSetConstantBuffers(1, Some(&blur_params)); - ctx.PSSetSamplers(0, Some(slice::from_ref(&self.globals.sampler))); - ctx.PSSetShaderResources(0, Some(slice::from_ref(source_srv))); - ctx.OMSetBlendState(blend, None, 0xFFFFFFFF); - ctx.DrawInstanced(vertex_count, 1, 0, 0); - // Unbind the source so the target can be rebound as a render target next. - ctx.PSSetShaderResources(0, Some(&null_srv)); - } - Ok(()) - } + /// Run a single blur pass: a full-screen (or composite) draw sampling `source_srv` into + /// `target_rtv`, with `params` in the blur constant buffer (b1). + #[allow(clippy::too_many_arguments)] + fn dx_blur_pass( + &self, + vertex: &ID3D11VertexShader, + fragment: &ID3D11PixelShader, + blend: &ID3D11BlendState, + target_rtv: &Option, + source_srv: &Option, + params: BlurParams, + viewport: &D3D11_VIEWPORT, + topology: D3D_PRIMITIVE_TOPOLOGY, + vertex_count: u32, + clear: bool, + ) -> Result<()> { + let devices = self.devices.as_ref().context("devices missing")?; + let ctx = &devices.device_context; + update_buffer(ctx, &self.pipelines.blur_params_buffer, &[params])?; + let null_srv: [Option; 1] = [None]; + let blur_params = [Some(self.pipelines.blur_params_buffer.clone())]; + unsafe { + // Unbind any SRV at slot 0 so the target texture isn't simultaneously bound as input. + ctx.PSSetShaderResources(0, Some(&null_srv)); + if clear { + ctx.ClearRenderTargetView( + target_rtv.as_ref().context("blur target view missing")?, + &[0.0; 4], + ); + } + ctx.OMSetRenderTargets(Some(slice::from_ref(target_rtv)), None); + ctx.RSSetViewports(Some(slice::from_ref(viewport))); + ctx.IASetPrimitiveTopology(topology); + ctx.VSSetShader(vertex, None); + ctx.PSSetShader(fragment, None); + ctx.VSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); + ctx.PSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); + ctx.VSSetConstantBuffers(1, Some(&blur_params)); + ctx.PSSetConstantBuffers(1, Some(&blur_params)); + ctx.PSSetSamplers(0, Some(slice::from_ref(&self.globals.sampler))); + ctx.PSSetShaderResources(0, Some(slice::from_ref(source_srv))); + ctx.OMSetBlendState(blend, None, 0xFFFFFFFF); + ctx.DrawInstanced(vertex_count, 1, 0, 0); + // Unbind the source so the target can be rebound as a render target next. + ctx.PSSetShaderResources(0, Some(&null_srv)); + } + Ok(()) + } - /// Blur `source_srv` (full-resolution) using the half-res ping/pong textures and composite the - /// result into `target_rtv`, clipped to `bounds`/`corner_radii`/`content_mask` and modulated - /// by `opacity`. Shared by the backdrop and content-filter paths. - #[allow(clippy::too_many_arguments)] - fn dx_blur_and_composite( - &self, - source_srv: &Option, - target_rtv: &Option, - bounds: Bounds, - content_mask: Bounds, - corner_radii: [f32; 4], - blur_radius: f32, - opacity: f32, - // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. - clip_rounded: bool, - ) -> Result<()> { - // Sigma is halved because the blur runs at half resolution. - let sigma = (blur_radius * 0.5).max(0.0); - if sigma <= 0.0 { - return Ok(()); - } - // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather - // than truncating the kernel — keeps very large radii from clipping. Matches wgpu. - let ideal_taps = (3.0 * sigma).ceil(); - let tap_count = ideal_taps.clamp(1.0, 32.0); - let tap_step = (ideal_taps / tap_count).max(1.0); - // Content blur bleeds ~3·radius past the box, so its composite quad covers a dilated rect. - let composite_bounds = if clip_rounded { - bounds - } else { - bounds.dilate(ScaledPixels(3.0 * blur_radius)) - }; - let half_w = (self.width / 2).max(1); - let half_h = (self.height / 2).max(1); - let half_vp = D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: half_w as f32, - Height: half_h as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }; - let (full_vp, ping_rtv, ping_srv, pong_rtv, pong_srv) = { - let r = self.resources.as_ref().context("resources missing")?; - ( - r.viewport, - r.blur.ping_rtv.clone(), - r.blur.ping_srv.clone(), - r.blur.pong_rtv.clone(), - r.blur.pong_srv.clone(), - ) - }; + /// Blur `source_srv` (full-resolution) using the half-res ping/pong textures and composite the + /// result into `target_rtv`, clipped to `bounds`/`corner_radii`/`content_mask` and modulated + /// by `opacity`. Shared by the backdrop and content-filter paths. + #[allow(clippy::too_many_arguments)] + fn dx_blur_and_composite( + &self, + source_srv: &Option, + target_rtv: &Option, + bounds: Bounds, + content_mask: Bounds, + corner_radii: [f32; 4], + blur_radius: f32, + opacity: f32, + // Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds. + clip_rounded: bool, + ) -> Result<()> { + // Sigma is halved because the blur runs at half resolution. + let sigma = (blur_radius * 0.5).max(0.0); + if sigma <= 0.0 { + return Ok(()); + } + // Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather + // than truncating the kernel — keeps very large radii from clipping. Matches wgpu. + let ideal_taps = (3.0 * sigma).ceil(); + let tap_count = ideal_taps.clamp(1.0, 32.0); + let tap_step = (ideal_taps / tap_count).max(1.0); + // Content blur bleeds ~3·radius past the box, so its composite quad covers a dilated rect. + let composite_bounds = if clip_rounded { + bounds + } else { + bounds.dilate(ScaledPixels(3.0 * blur_radius)) + }; + let half_w = (self.width / 2).max(1); + let half_h = (self.height / 2).max(1); + let half_vp = D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: half_w as f32, + Height: half_h as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }; + let (full_vp, ping_rtv, ping_srv, pong_rtv, pong_srv) = { + let r = self.resources.as_ref().context("resources missing")?; + ( + r.viewport, + r.blur.ping_rtv.clone(), + r.blur.ping_srv.clone(), + r.blur.pong_rtv.clone(), + r.blur.pong_srv.clone(), + ) + }; - // Downsample source -> ping, then separable gaussian ping -> pong -> ping. - self.dx_blur_pass( - &self.pipelines.blur_downsample_vertex, - &self.pipelines.blur_downsample_fragment, - &self.pipelines.blur_blend_replace, - &ping_rtv, - source_srv, - BlurParams { - downsample: 1.0, - ..Default::default() - }, - &half_vp, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - 3, - true, - )?; - self.dx_blur_pass( - &self.pipelines.blur_vertex, - &self.pipelines.blur_fragment, - &self.pipelines.blur_blend_replace, - &pong_rtv, - &ping_srv, - BlurParams { - direction: [1.0 / half_w as f32, 0.0], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - &half_vp, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - 3, - true, - )?; - self.dx_blur_pass( - &self.pipelines.blur_vertex, - &self.pipelines.blur_fragment, - &self.pipelines.blur_blend_replace, - &ping_rtv, - &pong_srv, - BlurParams { - direction: [0.0, 1.0 / half_h as f32], - sigma, - tap_count, - tap_step, - ..Default::default() - }, - &half_vp, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - 3, - true, - )?; - // Composite the blurred result into the target (preserving its contents). - self.dx_blur_pass( - &self.pipelines.blur_composite_vertex, - &self.pipelines.blur_composite_fragment, - &self.pipelines.blur_blend_composite, - target_rtv, - &ping_srv, - BlurParams { - bounds: composite_bounds, - content_mask, - corner_radii, - opacity, - clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, - ..Default::default() - }, - &full_vp, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - 4, - false, - )?; - Ok(()) - } + // Downsample source -> ping, then separable gaussian ping -> pong -> ping. + self.dx_blur_pass( + &self.pipelines.blur_downsample_vertex, + &self.pipelines.blur_downsample_fragment, + &self.pipelines.blur_blend_replace, + &ping_rtv, + source_srv, + BlurParams { + downsample: 1.0, + ..Default::default() + }, + &half_vp, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, + 3, + true, + )?; + self.dx_blur_pass( + &self.pipelines.blur_vertex, + &self.pipelines.blur_fragment, + &self.pipelines.blur_blend_replace, + &pong_rtv, + &ping_srv, + BlurParams { + direction: [1.0 / half_w as f32, 0.0], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + &half_vp, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, + 3, + true, + )?; + self.dx_blur_pass( + &self.pipelines.blur_vertex, + &self.pipelines.blur_fragment, + &self.pipelines.blur_blend_replace, + &ping_rtv, + &pong_srv, + BlurParams { + direction: [0.0, 1.0 / half_h as f32], + sigma, + tap_count, + tap_step, + ..Default::default() + }, + &half_vp, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, + 3, + true, + )?; + // Composite the blurred result into the target (preserving its contents). + self.dx_blur_pass( + &self.pipelines.blur_composite_vertex, + &self.pipelines.blur_composite_fragment, + &self.pipelines.blur_blend_composite, + target_rtv, + &ping_srv, + BlurParams { + bounds: composite_bounds, + content_mask, + corner_radii, + opacity, + clip_rounded: if clip_rounded { 1.0 } else { 0.0 }, + ..Default::default() + }, + &full_vp, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + 4, + false, + )?; + Ok(()) + } - /// Copy the offscreen scene texture into the swapchain render target. - fn dx_blit( - &self, - source_srv: &Option, - target_rtv: &Option, - ) -> Result<()> { - let full_vp = self - .resources - .as_ref() - .context("resources missing")? - .viewport; - self.dx_blur_pass( - &self.pipelines.blur_downsample_vertex, - &self.pipelines.blur_downsample_fragment, - &self.pipelines.blur_blend_replace, - target_rtv, - source_srv, - BlurParams::default(), - &full_vp, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - 3, - true, - ) - } + /// Copy the offscreen scene texture into the swapchain render target. + fn dx_blit( + &self, + source_srv: &Option, + target_rtv: &Option, + ) -> Result<()> { + let full_vp = self + .resources + .as_ref() + .context("resources missing")? + .viewport; + self.dx_blur_pass( + &self.pipelines.blur_downsample_vertex, + &self.pipelines.blur_downsample_fragment, + &self.pipelines.blur_blend_replace, + target_rtv, + source_srv, + BlurParams::default(), + &full_vp, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, + 3, + true, + ) + } - 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 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 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; - } + 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, - )? - }; + 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)?; - let blur = BlurResources::new(&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)?; + let blur = BlurResources::new(&devices.device, width, height)?; - 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, - blur, - viewport, - }) - } + 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, + blur, + 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.blur = BlurResources::new(&devices.device, width, height)?; - self.viewport = viewport; - Ok(()) - } + #[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.blur = BlurResources::new(&devices.device, width, height)?; + 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)?, - )?; + 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)?, + )?; - let blur_downsample_vertex = create_vertex_shader( - device, - RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Vertex)?.as_bytes(), - )?; - let blur_downsample_fragment = create_fragment_shader( - device, - RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Fragment)?.as_bytes(), - )?; - let blur_vertex = create_vertex_shader( - device, - RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Vertex)?.as_bytes(), - )?; - let blur_fragment = create_fragment_shader( - device, - RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Fragment)?.as_bytes(), - )?; - let blur_composite_vertex = create_vertex_shader( - device, - RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Vertex)?.as_bytes(), - )?; - let blur_composite_fragment = create_fragment_shader( - device, - RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Fragment)?.as_bytes(), - )?; - let blur_params_buffer = create_constant_buffer(device, std::mem::size_of::())?; - let blur_blend_replace = create_blend_state_no_blend(device)?; - // Premultiplied (One / InvSrcAlpha) — the composite outputs a premultiplied blurred sample; - // straight-alpha blending would darken the faded edges. - let blur_blend_composite = create_blend_state_for_path_sprite(device)?; + let blur_downsample_vertex = create_vertex_shader( + device, + RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Vertex)?.as_bytes(), + )?; + let blur_downsample_fragment = create_fragment_shader( + device, + RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Fragment)?.as_bytes(), + )?; + let blur_vertex = create_vertex_shader( + device, + RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Vertex)?.as_bytes(), + )?; + let blur_fragment = create_fragment_shader( + device, + RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Fragment)?.as_bytes(), + )?; + let blur_composite_vertex = create_vertex_shader( + device, + RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Vertex)?.as_bytes(), + )?; + let blur_composite_fragment = create_fragment_shader( + device, + RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Fragment)?.as_bytes(), + )?; + let blur_params_buffer = create_constant_buffer(device, std::mem::size_of::())?; + let blur_blend_replace = create_blend_state_no_blend(device)?; + // Premultiplied (One / InvSrcAlpha) — the composite outputs a premultiplied blurred sample; + // straight-alpha blending would darken the faded edges. + let blur_blend_composite = create_blend_state_for_path_sprite(device)?; - Ok(Self { - shadow_pipeline, - quad_pipeline, - path_rasterization_pipeline, - path_sprite_pipeline, - underline_pipeline, - mono_sprites, - subpixel_sprites, - poly_sprites, - blur_downsample_vertex, - blur_downsample_fragment, - blur_vertex, - blur_fragment, - blur_composite_vertex, - blur_composite_fragment, - blur_params_buffer, - blur_blend_replace, - blur_blend_composite, - }) - } + Ok(Self { + shadow_pipeline, + quad_pipeline, + path_rasterization_pipeline, + path_sprite_pipeline, + underline_pipeline, + mono_sprites, + subpixel_sprites, + poly_sprites, + blur_downsample_vertex, + blur_downsample_fragment, + blur_vertex, + blur_fragment, + blur_composite_vertex, + blur_composite_fragment, + blur_params_buffer, + blur_blend_replace, + blur_blend_composite, + }) + } } 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() }?; + 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, - }) - } + 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(()) - } + 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 global_params_buffer = unsafe { - 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, - ..Default::default() - }; - let mut buffer = None; - device.CreateBuffer(&desc, None, Some(&mut buffer))?; - buffer - }; + pub fn new(device: &ID3D11Device) -> Result { + let global_params_buffer = unsafe { + 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, + ..Default::default() + }; + let mut buffer = None; + device.CreateBuffer(&desc, None, Some(&mut buffer))?; + buffer + }; - 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 - }; + 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 { - global_params_buffer, - sampler, - }) - } + Ok(Self { + global_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], + gamma_ratios: [f32; 4], + viewport_size: [f32; 2], + grayscale_enhanced_contrast: f32, + subpixel_enhanced_contrast: f32, + is_bgr: u32, + _pad: [u32; 3], } /// Mirrors the `BlurParams` cbuffer (register b1) in `shaders.hlsl`. 80 bytes (a multiple of 16, @@ -1450,1121 +1450,1121 @@ struct GlobalParams { #[repr(C)] #[derive(Clone, Copy)] struct BlurParams { - bounds: Bounds, - content_mask: Bounds, - corner_radii: [f32; 4], - direction: [f32; 2], - sigma: f32, - opacity: f32, - tap_count: f32, - /// 1.0 clips the composite to the rounded rect (backdrop); 0.0 lets content blur bleed past - /// its bounds like CSS `filter: blur`. - clip_rounded: f32, - /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the - /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy - /// (the scene blit, which must not downsample). Downsample pass only. - downsample: f32, - /// Spacing between taps in pixels (gaussian passes only); >1 lets `tap_count` taps span very - /// large radii without truncating the gaussian, matching the wgpu backend. - tap_step: f32, + bounds: Bounds, + content_mask: Bounds, + corner_radii: [f32; 4], + direction: [f32; 2], + sigma: f32, + opacity: f32, + tap_count: f32, + /// 1.0 clips the composite to the rounded rect (backdrop); 0.0 lets content blur bleed past + /// its bounds like CSS `filter: blur`. + clip_rounded: f32, + /// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the + /// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy + /// (the scene blit, which must not downsample). Downsample pass only. + downsample: f32, + /// Spacing between taps in pixels (gaussian passes only); >1 lets `tap_count` taps span very + /// large radii without truncating the gaussian, matching the wgpu backend. + tap_step: f32, } impl Default for BlurParams { - fn default() -> Self { - BlurParams { - bounds: Bounds::default(), - content_mask: Bounds::default(), - corner_radii: [0.0; 4], - direction: [0.0, 0.0], - sigma: 0.0, - opacity: 1.0, - tap_count: 0.0, - clip_rounded: 0.0, - downsample: 0.0, - tap_step: 0.0, - } - } + fn default() -> Self { + BlurParams { + bounds: Bounds::default(), + content_mask: Bounds::default(), + corner_radii: [0.0; 4], + direction: [0.0, 0.0], + sigma: 0.0, + opacity: 1.0, + tap_count: 0.0, + clip_rounded: 0.0, + downsample: 0.0, + tap_step: 0.0, + } + } } struct PipelineState { - label: &'static str, - vertex: ID3D11VertexShader, - fragment: ID3D11PixelShader, - buffer: ID3D11Buffer, - buffer_size: usize, - view: Option, - blend_state: ID3D11BlendState, - _marker: std::marker::PhantomData, + 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)?; + 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, - }) - } + 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 new_buffer_size = data.len().next_power_of_two(); - 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 update_buffer( + &mut self, + device: &ID3D11Device, + device_context: &ID3D11DeviceContext, + data: &[T], + ) -> Result<()> { + if self.buffer_size < data.len() { + let new_buffer_size = data.len().next_power_of_two(); + 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, - viewport: &[D3D11_VIEWPORT], - global_params: &[Option], - topology: D3D_PRIMITIVE_TOPOLOGY, - vertex_count: u32, - instance_count: u32, - ) -> Result<()> { - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - topology, - viewport, - &self.vertex, - &self.fragment, - global_params, - &self.blend_state, - ); - unsafe { - device_context.DrawInstanced(vertex_count, instance_count, 0, 0); - } - Ok(()) - } + fn draw( + &self, + device_context: &ID3D11DeviceContext, + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + topology: D3D_PRIMITIVE_TOPOLOGY, + vertex_count: u32, + instance_count: u32, + ) -> Result<()> { + set_pipeline_state( + device_context, + slice::from_ref(&self.view), + topology, + viewport, + &self.vertex, + &self.fragment, + global_params, + &self.blend_state, + ); + unsafe { + device_context.DrawInstanced(vertex_count, instance_count, 0, 0); + } + Ok(()) + } - fn draw_with_texture( - &self, - device_context: &ID3D11DeviceContext, - texture: &[Option], - viewport: &[D3D11_VIEWPORT], - global_params: &[Option], - sampler: &[Option], - instance_count: u32, - ) -> Result<()> { - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - viewport, - &self.vertex, - &self.fragment, - global_params, - &self.blend_state, - ); - unsafe { - device_context.PSSetSamplers(0, Some(sampler)); - device_context.VSSetShaderResources(0, Some(texture)); - device_context.PSSetShaderResources(0, Some(texture)); + fn draw_with_texture( + &self, + device_context: &ID3D11DeviceContext, + texture: &[Option], + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + sampler: &[Option], + instance_count: u32, + ) -> Result<()> { + set_pipeline_state( + device_context, + slice::from_ref(&self.view), + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + viewport, + &self.vertex, + &self.fragment, + global_params, + &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(()) - } + device_context.DrawInstanced(4, instance_count, 0, 0); + } + Ok(()) + } - fn draw_range( - &self, - device: &ID3D11Device, - device_context: &ID3D11DeviceContext, - viewport: &[D3D11_VIEWPORT], - global_params: &[Option], - vertex_count: u32, - first_instance: u32, - instance_count: u32, - ) -> Result<()> { - let view = create_buffer_view_range(device, &self.buffer, first_instance, instance_count)?; - set_pipeline_state( - device_context, - slice::from_ref(&view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - viewport, - &self.vertex, - &self.fragment, - global_params, - &self.blend_state, - ); - unsafe { - device_context.DrawInstanced(vertex_count, instance_count, 0, 0); - } - Ok(()) - } + fn draw_range( + &self, + device: &ID3D11Device, + device_context: &ID3D11DeviceContext, + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + vertex_count: u32, + first_instance: u32, + instance_count: u32, + ) -> Result<()> { + let view = create_buffer_view_range(device, &self.buffer, first_instance, instance_count)?; + set_pipeline_state( + device_context, + slice::from_ref(&view), + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + viewport, + &self.vertex, + &self.fragment, + global_params, + &self.blend_state, + ); + unsafe { + device_context.DrawInstanced(vertex_count, instance_count, 0, 0); + } + Ok(()) + } - fn draw_range_with_texture( - &self, - device: &ID3D11Device, - device_context: &ID3D11DeviceContext, - texture: &[Option], - viewport: &[D3D11_VIEWPORT], - global_params: &[Option], - sampler: &[Option], - first_instance: u32, - instance_count: u32, - ) -> Result<()> { - let view = create_buffer_view_range(device, &self.buffer, first_instance, instance_count)?; - set_pipeline_state( - device_context, - slice::from_ref(&view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - viewport, - &self.vertex, - &self.fragment, - global_params, - &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_with_texture( + &self, + device: &ID3D11Device, + device_context: &ID3D11DeviceContext, + texture: &[Option], + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + sampler: &[Option], + first_instance: u32, + instance_count: u32, + ) -> Result<()> { + let view = create_buffer_view_range(device, &self.buffer, first_instance, instance_count)?; + set_pipeline_state( + device_context, + slice::from_ref(&view), + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + viewport, + &self.vertex, + &self.fragment, + global_params, + &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, + xy_position: Point, + st_position: Point, + color: Background, + bounds: Bounds, } #[derive(Clone, Copy)] #[repr(C)] struct PathSprite { - bounds: Bounds, + 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(); - } - } + 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)? }) + Ok(unsafe { DCompositionCreateDevice(dxgi_device)? }) } fn create_swap_chain_for_composition( - dxgi_factory: &IDXGIFactory6, - device: &ID3D11Device, - width: u32, - height: u32, + 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)? }) + 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, + dxgi_factory: &IDXGIFactory6, + device: &ID3D11Device, + hwnd: HWND, + width: u32, + height: u32, ) -> Result { - use windows::Win32::Graphics::Dxgi::DXGI_MWA_NO_ALT_ENTER; + 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) + 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, + devices: &DirectXRendererDevices, + swap_chain: &IDXGISwapChain1, + width: u32, + height: u32, ) -> Result<( - ID3D11Texture2D, - Option, - ID3D11Texture2D, - Option, - ID3D11Texture2D, - Option, - D3D11_VIEWPORT, + 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 = set_viewport(&devices.device_context, width as f32, height as f32); - Ok(( - render_target, - render_target_view, - path_intermediate_texture, - path_intermediate_srv, - path_intermediate_msaa_texture, - path_intermediate_msaa_view, - 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 = set_viewport(&devices.device_context, width as f32, height as f32); + Ok(( + render_target, + render_target_view, + path_intermediate_texture, + path_intermediate_srv, + path_intermediate_msaa_texture, + path_intermediate_msaa_view, + viewport, + )) } #[inline] /// Flatten a `Corners` into the `[tl, tr, br, bl]` order expected by the blur composite shader. fn corner_radii_array(corners: Corners) -> [f32; 4] { - [ - corners.top_left.0, - corners.top_right.0, - corners.bottom_right.0, - corners.bottom_left.0, - ] + [ + corners.top_left.0, + corners.top_right.0, + corners.bottom_right.0, + corners.bottom_left.0, + ] } fn create_render_target_and_its_view( - swap_chain: &IDXGISwapChain1, - device: &ID3D11Device, + 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)) + 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, + 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 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))? }; + let mut shader_resource_view = None; + unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut shader_resource_view))? }; - Ok((texture, Some(shader_resource_view.unwrap()))) + Ok((texture, Some(shader_resource_view.unwrap()))) } /// Create a color texture usable as both a render target and a shader resource, returning both /// views. Used for the blur offscreen targets. #[inline] fn create_color_target( - device: &ID3D11Device, - width: u32, - height: u32, + device: &ID3D11Device, + width: u32, + height: u32, ) -> Result<( - ID3D11Texture2D, - Option, - Option, + ID3D11Texture2D, + Option, + Option, )> { - let texture = unsafe { - let mut output = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: width.max(1), - Height: height.max(1), - 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 rtv = None; - unsafe { device.CreateRenderTargetView(&texture, None, Some(&mut rtv))? }; - let mut srv = None; - unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut srv))? }; - Ok((texture, rtv, srv)) + let texture = unsafe { + let mut output = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: width.max(1), + Height: height.max(1), + 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 rtv = None; + unsafe { device.CreateRenderTargetView(&texture, None, Some(&mut rtv))? }; + let mut srv = None; + unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut srv))? }; + Ok((texture, rtv, srv)) } #[inline] fn create_path_intermediate_msaa_texture_and_view( - device: &ID3D11Device, - width: u32, - height: u32, + 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()))) + 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_viewport(device_context: &ID3D11DeviceContext, width: f32, height: f32) -> D3D11_VIEWPORT { - let viewport = [D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: width, - Height: height, - MinDepth: 0.0, - MaxDepth: 1.0, - }]; - unsafe { device_context.RSSetViewports(Some(&viewport)) }; - viewport[0] + let viewport = [D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: width, + Height: height, + MinDepth: 0.0, + MaxDepth: 1.0, + }]; + unsafe { device_context.RSSetViewports(Some(&viewport)) }; + viewport[0] } #[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(()) + 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()) - } + 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; + 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()) - } + 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()) - } + // 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()) - } + // 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()) + } } /// Create a CPU-writable dynamic constant buffer of the given byte size (rounded up to 16). #[inline] fn create_constant_buffer(device: &ID3D11Device, byte_size: usize) -> Result { - let desc = D3D11_BUFFER_DESC { - ByteWidth: byte_size.next_multiple_of(16) as u32, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - ..Default::default() - }; - let mut buffer = None; - unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?; - Ok(buffer.unwrap()) + let desc = D3D11_BUFFER_DESC { + ByteWidth: byte_size.next_multiple_of(16) as u32, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + ..Default::default() + }; + let mut buffer = None; + unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?; + Ok(buffer.unwrap()) } /// A blend state that overwrites the target (no blending) — used for the blur downsample and /// gaussian passes. #[inline] fn create_blend_state_no_blend(device: &ID3D11Device) -> Result { - let mut desc = D3D11_BLEND_DESC::default(); - desc.RenderTarget[0].BlendEnable = false.into(); - 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()) - } + let mut desc = D3D11_BLEND_DESC::default(); + desc.RenderTarget[0].BlendEnable = false.into(); + 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()) - } + 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()) - } + unsafe { + let mut shader = None; + device.CreatePixelShader(bytes, None, Some(&mut shader))?; + Ok(shader.unwrap()) + } } #[inline] fn create_buffer( - device: &ID3D11Device, - element_size: usize, - buffer_size: usize, + 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()) + 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, + device: &ID3D11Device, + buffer: &ID3D11Buffer, ) -> Result> { - let mut view = None; - unsafe { device.CreateShaderResourceView(buffer, None, Some(&mut view)) }?; - Ok(view) + let mut view = None; + unsafe { device.CreateShaderResourceView(buffer, None, Some(&mut view)) }?; + Ok(view) } #[inline] fn create_buffer_view_range( - device: &ID3D11Device, - buffer: &ID3D11Buffer, - first_element: u32, - num_elements: u32, + device: &ID3D11Device, + buffer: &ID3D11Buffer, + first_element: u32, + num_elements: u32, ) -> Result> { - let desc = D3D11_SHADER_RESOURCE_VIEW_DESC { - Format: DXGI_FORMAT_UNKNOWN, - ViewDimension: D3D11_SRV_DIMENSION_BUFFER, - Anonymous: D3D11_SHADER_RESOURCE_VIEW_DESC_0 { - Buffer: D3D11_BUFFER_SRV { - Anonymous1: D3D11_BUFFER_SRV_0 { - FirstElement: first_element, - }, - Anonymous2: D3D11_BUFFER_SRV_1 { - NumElements: num_elements, - }, - }, - }, - }; - let mut view = None; - unsafe { device.CreateShaderResourceView(buffer, Some(&desc), Some(&mut view)) }?; - Ok(view) + let desc = D3D11_SHADER_RESOURCE_VIEW_DESC { + Format: DXGI_FORMAT_UNKNOWN, + ViewDimension: D3D11_SRV_DIMENSION_BUFFER, + Anonymous: D3D11_SHADER_RESOURCE_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_SRV { + Anonymous1: D3D11_BUFFER_SRV_0 { + FirstElement: first_element, + }, + Anonymous2: D3D11_BUFFER_SRV_1 { + NumElements: num_elements, + }, + }, + }, + }; + let mut view = None; + unsafe { device.CreateShaderResourceView(buffer, Some(&desc), Some(&mut view)) }?; + Ok(view) } #[inline] fn update_buffer( - device_context: &ID3D11DeviceContext, - buffer: &ID3D11Buffer, - data: &[T], + 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(()) + 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 set_pipeline_state( - device_context: &ID3D11DeviceContext, - buffer_view: &[Option], - topology: D3D_PRIMITIVE_TOPOLOGY, - viewport: &[D3D11_VIEWPORT], - vertex_shader: &ID3D11VertexShader, - fragment_shader: &ID3D11PixelShader, - global_params: &[Option], - blend_state: &ID3D11BlendState, + device_context: &ID3D11DeviceContext, + buffer_view: &[Option], + topology: D3D_PRIMITIVE_TOPOLOGY, + viewport: &[D3D11_VIEWPORT], + vertex_shader: &ID3D11VertexShader, + fragment_shader: &ID3D11PixelShader, + global_params: &[Option], + blend_state: &ID3D11BlendState, ) { - unsafe { - device_context.VSSetShaderResources(1, Some(buffer_view)); - device_context.PSSetShaderResources(1, Some(buffer_view)); - device_context.IASetPrimitiveTopology(topology); - device_context.RSSetViewports(Some(viewport)); - device_context.VSSetShader(vertex_shader, None); - device_context.PSSetShader(fragment_shader, None); - device_context.VSSetConstantBuffers(0, Some(global_params)); - device_context.PSSetConstantBuffers(0, Some(global_params)); - device_context.OMSetBlendState(blend_state, None, 0xFFFFFFFF); - } + unsafe { + device_context.VSSetShaderResources(1, Some(buffer_view)); + device_context.PSSetShaderResources(1, Some(buffer_view)); + device_context.IASetPrimitiveTopology(topology); + device_context.RSSetViewports(Some(viewport)); + device_context.VSSetShader(vertex_shader, None); + device_context.PSSetShader(fragment_shader, None); + device_context.VSSetConstantBuffers(0, Some(global_params)); + device_context.PSSetConstantBuffers(0, Some(global_params)); + 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(()) + 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; + use anyhow::Result; - #[cfg(debug_assertions)] - use windows::{ - Win32::Graphics::Direct3D::{ - Fxc::{D3DCOMPILE_DEBUG, D3DCOMPILE_SKIP_OPTIMIZATION, D3DCompileFromFile}, - ID3DBlob, - }, - core::{HSTRING, PCSTR}, - }; + #[cfg(debug_assertions)] + use windows::{ + Win32::Graphics::Direct3D::{ + Fxc::{D3DCOMPILE_DEBUG, D3DCOMPILE_SKIP_OPTIMIZATION, D3DCompileFromFile}, + ID3DBlob, + }, + core::{HSTRING, PCSTR}, + }; - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderModule { - Quad, - Shadow, - Underline, - PathRasterization, - PathSprite, - MonochromeSprite, - SubpixelSprite, - PolychromeSprite, - EmojiRasterization, - BlurDownsample, - Blur, - BlurComposite, - } + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub(crate) enum ShaderModule { + Quad, + Shadow, + Underline, + PathRasterization, + PathSprite, + MonochromeSprite, + SubpixelSprite, + PolychromeSprite, + EmojiRasterization, + BlurDownsample, + Blur, + BlurComposite, + } - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderTarget { - Vertex, - Fragment, - } + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub(crate) enum ShaderTarget { + Vertex, + Fragment, + } - pub(crate) struct RawShaderBytes<'t> { - inner: &'t [u8], + pub(crate) struct RawShaderBytes<'t> { + inner: &'t [u8], - #[cfg(debug_assertions)] - _blob: ID3DBlob, - } + #[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 }) - } - } + 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 - } + 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, - }, - ShaderModule::BlurDownsample => match target { - ShaderTarget::Vertex => BLUR_DOWNSAMPLE_VERTEX_BYTES, - ShaderTarget::Fragment => BLUR_DOWNSAMPLE_FRAGMENT_BYTES, - }, - ShaderModule::Blur => match target { - ShaderTarget::Vertex => BLUR_VERTEX_BYTES, - ShaderTarget::Fragment => BLUR_FRAGMENT_BYTES, - }, - ShaderModule::BlurComposite => match target { - ShaderTarget::Vertex => BLUR_COMPOSITE_VERTEX_BYTES, - ShaderTarget::Fragment => BLUR_COMPOSITE_FRAGMENT_BYTES, - }, - }; - Self { inner: bytes } - } - } + #[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, + }, + ShaderModule::BlurDownsample => match target { + ShaderTarget::Vertex => BLUR_DOWNSAMPLE_VERTEX_BYTES, + ShaderTarget::Fragment => BLUR_DOWNSAMPLE_FRAGMENT_BYTES, + }, + ShaderModule::Blur => match target { + ShaderTarget::Vertex => BLUR_VERTEX_BYTES, + ShaderTarget::Fragment => BLUR_FRAGMENT_BYTES, + }, + ShaderModule::BlurComposite => match target { + ShaderTarget::Vertex => BLUR_COMPOSITE_VERTEX_BYTES, + ShaderTarget::Fragment => BLUR_COMPOSITE_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, - }; + #[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 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 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 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()); + 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, - ); + // 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 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()) - } - } + 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(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", - ShaderModule::BlurDownsample => "blur_downsample", - ShaderModule::Blur => "blur", - ShaderModule::BlurComposite => "blur_composite", - } - } - } + #[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", + ShaderModule::BlurDownsample => "blur_downsample", + ShaderModule::Blur => "blur", + ShaderModule::BlurComposite => "blur_composite", + } + } + } } mod nvidia { - use std::{ - ffi::CStr, - os::raw::{c_char, c_int, c_uint}, - }; + use std::{ + ffi::CStr, + os::raw::{c_char, c_int, c_uint}, + }; - use anyhow::Result; - use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - use crate::with_dll_library; + 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#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#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; + // 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"); + 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); + 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); + // 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, - ); + 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() - )) - }) - } + 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 std::os::raw::{c_char, c_int, c_void}; - use anyhow::Result; - use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - use crate::with_dll_library; + 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#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], - } + // 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, - } + #[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#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; + // 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"); + 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"))?; + 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 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 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); - } + 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() - }; + // 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() - }; + 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)) - }) - } + ags_deinitialize(context); + Ok(format!("{} ({})", software_version, driver_version)) + }) + } } mod dxgi { - use windows::{ - Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice}, - core::Interface, - }; + use windows::{ + Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice}, + core::Interface, + }; - 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 - )) - } + 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_windows/src/dispatcher.rs b/crates/gpui_windows/src/dispatcher.rs index 2b2bf402d2..94fcfe5c6a 100644 --- a/crates/gpui_windows/src/dispatcher.rs +++ b/crates/gpui_windows/src/dispatcher.rs @@ -1,180 +1,180 @@ use std::{ - sync::atomic::{AtomicBool, Ordering}, - thread::{ThreadId, current}, - time::{Duration, Instant}, + sync::atomic::{AtomicBool, Ordering}, + thread::{ThreadId, current}, + time::{Duration, Instant}, }; use anyhow::Context; use util::ResultExt; use windows::{ - System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, - }, - Win32::{ - Foundation::{LPARAM, WPARAM}, - Media::{timeBeginPeriod, timeEndPeriod}, - System::Threading::{GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_TIME_CRITICAL}, - UI::WindowsAndMessaging::PostMessageW, - }, + System::Threading::{ + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + }, + Win32::{ + Foundation::{LPARAM, WPARAM}, + Media::{timeBeginPeriod, timeEndPeriod}, + System::Threading::{GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_TIME_CRITICAL}, + UI::WindowsAndMessaging::PostMessageW, + }, }; use crate::{HWND, SafeHwnd, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD}; use gpui::{ - GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant, - TaskTiming, ThreadTaskTimings, TimerResolutionGuard, + GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant, + TaskTiming, ThreadTaskTimings, 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, + 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(); + 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), - } - } + WindowsDispatcher { + main_sender, + main_thread_id, + platform_window_handle, + validation_number, + wake_posted: AtomicBool::new(false), + } + } - fn dispatch_on_threadpool(&self, priority: WorkItemPriority, runnable: RunnableVariant) { - let handler = { - let mut task_wrapper = Some(runnable); - WorkItemHandler::new(move |_| { - let runnable = task_wrapper.take().unwrap(); - Self::execute_runnable(runnable); - Ok(()) - }) - }; + fn dispatch_on_threadpool(&self, priority: WorkItemPriority, runnable: RunnableVariant) { + let handler = { + let mut task_wrapper = Some(runnable); + WorkItemHandler::new(move |_| { + let runnable = task_wrapper.take().unwrap(); + Self::execute_runnable(runnable); + Ok(()) + }) + }; - ThreadPool::RunWithPriorityAsync(&handler, priority).log_err(); - } + ThreadPool::RunWithPriorityAsync(&handler, priority).log_err(); + } - fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) { - let handler = { - let mut task_wrapper = Some(runnable); - TimerElapsedHandler::new(move |_| { - let runnable = task_wrapper.take().unwrap(); - Self::execute_runnable(runnable); - Ok(()) - }) - }; - ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); - } + fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) { + let handler = { + let mut task_wrapper = Some(runnable); + TimerElapsedHandler::new(move |_| { + let runnable = task_wrapper.take().unwrap(); + Self::execute_runnable(runnable); + Ok(()) + }) + }; + ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + } - #[inline(always)] - pub(crate) fn execute_runnable(runnable: RunnableVariant) { - let start = Instant::now(); + #[inline(always)] + pub(crate) fn execute_runnable(runnable: RunnableVariant) { + let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - gpui::profiler::add_task_timing(timing); + let location = runnable.metadata().location; + let mut timing = TaskTiming { + location, + start, + end: None, + }; + gpui::profiler::add_task_timing(timing); - runnable.run(); + runnable.run(); - let end = Instant::now(); - timing.end = Some(end); + let end = Instant::now(); + timing.end = Some(end); - gpui::profiler::add_task_timing(timing); - } + gpui::profiler::add_task_timing(timing); + } } impl PlatformDispatcher for WindowsDispatcher { - fn get_all_timings(&self) -> Vec { - let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock(); - ThreadTaskTimings::convert(&global_thread_timings) - } + fn get_all_timings(&self) -> Vec { + let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock(); + ThreadTaskTimings::convert(&global_thread_timings) + } - fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings { - gpui::profiler::get_current_thread_task_timings() - } + fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings { + gpui::profiler::get_current_thread_task_timings() + } - fn is_main_thread(&self) -> bool { - current().id() == self.main_thread_id - } + 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 => WorkItemPriority::High, - Priority::Medium => WorkItemPriority::Normal, - Priority::Low => WorkItemPriority::Low, - }; - self.dispatch_on_threadpool(priority, runnable); - } + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + let priority = match priority { + Priority::RealtimeAudio => { + panic!("RealtimeAudio priority should use spawn_realtime, not dispatch") + } + Priority::High => WorkItemPriority::High, + Priority::Medium => WorkItemPriority::Normal, + Priority::Low => WorkItemPriority::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_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 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() }; + 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(); + // 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(); - }); - } + f(); + }); + } - fn increase_timer_resolution(&self) -> TimerResolutionGuard { - unsafe { - timeBeginPeriod(1); - } - util::defer(Box::new(|| unsafe { - timeEndPeriod(1); - })) - } + fn increase_timer_resolution(&self) -> TimerResolutionGuard { + unsafe { + timeBeginPeriod(1); + } + util::defer(Box::new(|| unsafe { + timeEndPeriod(1); + })) + } } diff --git a/crates/gpui_windows/src/display.rs b/crates/gpui_windows/src/display.rs index 3b81dc63a0..e436621289 100644 --- a/crates/gpui_windows/src/display.rs +++ b/crates/gpui_windows/src/display.rs @@ -4,15 +4,15 @@ use std::rc::Rc; use util::ResultExt; use uuid::Uuid; use windows::{ - Win32::{ - Foundation::*, - Graphics::Gdi::*, - UI::{ - HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, - WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI, - }, - }, - core::*, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + UI::{ + HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, + WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI, + }, + }, + core::*, }; use crate::logical_point; @@ -20,13 +20,13 @@ 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, + 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. @@ -34,171 +34,171 @@ 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(), - ); + 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, - }) - } + 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(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)) - } + 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)) + } - /// 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 - } - } + /// 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 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 - } + pub fn physical_bounds(&self) -> Bounds { + self.physical_bounds + } } impl PlatformDisplay for WindowsDisplay { - fn id(&self) -> DisplayId { - self.display_id - } + fn id(&self) -> DisplayId { + self.display_id + } - fn uuid(&self) -> anyhow::Result { - Ok(self.uuid) - } + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } - fn bounds(&self) -> Bounds { - self.bounds - } + fn bounds(&self) -> Bounds { + self.bounds + } - fn visible_bounds(&self) -> Bounds { - self.visible_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 + 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, + 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) + 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())) - } + 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) + 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) + 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_windows/src/events.rs b/crates/gpui_windows/src/events.rs index f8eeea76f9..be233f0736 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -4,18 +4,18 @@ use ::util::ResultExt; use anyhow::Context as _; use std::{rc::Rc, sync::atomic::Ordering}; use windows::{ - Win32::{ - Foundation::*, - Graphics::Gdi::*, - System::SystemServices::*, - UI::{ - Controls::*, - HiDpi::*, - Input::{Ime::*, KeyboardAndMouse::*}, - WindowsAndMessaging::*, - }, - }, - core::PCWSTR, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::SystemServices::*, + UI::{ + Controls::*, + HiDpi::*, + Input::{Ime::*, KeyboardAndMouse::*}, + WindowsAndMessaging::*, + }, + }, + core::PCWSTR, }; use crate::*; @@ -33,1647 +33,1643 @@ pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; impl WindowsWindowInner { - pub(crate) fn handle_msg( - self: &Rc, - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> LRESULT { - let handled = match msg { - // eagerly activate the window, so calls to `active_window` will work correctly - WM_MOUSEACTIVATE => { - unsafe { SetActiveWindow(handle).ok() }; - None - } - 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_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_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); - #[cfg(not(feature = "wgpu"))] - { - 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); - } - } - #[cfg(feature = "wgpu")] - { - if should_resize_renderer { - self.state - .renderer - .borrow_mut() - .update_drawable_size(device_size) - } - } - 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 { - // emulating winit to support dragging a window - // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/event_loop.rs#L1234-L1243 - if self.state.dragging.get() { - self.state.dragging.set(false); - let _ = unsafe { PostMessageW(Some(handle), WM_LBUTTONUP, WPARAM(0), LPARAM(0)) }; - } - - 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 { - if 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.is_movable || 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)); - if let Some(area) = area { - match area { - WindowControlArea::Drag => Some(HTCAPTION as _), - WindowControlArea::Close => return Some(HTCLOSE as _), - WindowControlArea::Max => return Some(HTMAXBUTTON as _), - WindowControlArea::Min => return Some(HTMINBUTTON as _), - } - } else { - None - } - } 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.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) => { - unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; - true - } - (HTMAXBUTTON, HTMAXBUTTON) => { - if self.state.is_maximized() { - unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; - } else { - unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; - } - 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 { - #[cfg(not(feature = "wgpu"))] - { - 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}"); - } - } - #[cfg(feature = "wgpu")] - { - _ = lparam; - if let Err(err) = self.state.renderer.borrow_mut().recover(&RawWindow { - hwnd: self.platform_window_handle, - }) { - 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_after_recovery.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 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_after_recovery.take(); - #[cfg(not(feature = "wgpu"))] - { - 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 - } + pub(crate) fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + // eagerly activate the window, so calls to `active_window` will work correctly + WM_MOUSEACTIVATE => { + unsafe { SetActiveWindow(handle).ok() }; + None + } + 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_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_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); + #[cfg(not(feature = "wgpu"))] + { + 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); + } + } + #[cfg(feature = "wgpu")] + { + if should_resize_renderer { + self.state + .renderer + .borrow_mut() + .update_drawable_size(device_size) + } + } + 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 { + // emulating winit to support dragging a window + // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/event_loop.rs#L1234-L1243 + if self.state.dragging.get() { + self.state.dragging.set(false); + let _ = unsafe { PostMessageW(Some(handle), WM_LBUTTONUP, WPARAM(0), LPARAM(0)) }; + } + + 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 { + if 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.is_movable || 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)); + if let Some(area) = area { + match area { + WindowControlArea::Drag => Some(HTCAPTION as _), + WindowControlArea::Close => return Some(HTCLOSE as _), + WindowControlArea::Max => return Some(HTMAXBUTTON as _), + WindowControlArea::Min => return Some(HTMINBUTTON as _), + } + } else { + None + } + } 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.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) => { + unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; + true + } + (HTMAXBUTTON, HTMAXBUTTON) => { + if self.state.is_maximized() { + unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; + } else { + unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; + } + 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 { + #[cfg(not(feature = "wgpu"))] + { + 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}"); + } + } + #[cfg(feature = "wgpu")] + { + _ = lparam; + if let Err(err) = self.state.renderer.borrow_mut().recover(&RawWindow { + hwnd: self.platform_window_handle, + }) { + 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_after_recovery.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 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_after_recovery.take(); + #[cfg(not(feature = "wgpu"))] + { + 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, + 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 }) - } + 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 - } + 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 drop(&mut self) { + unsafe { + ImmReleaseContext(self.hwnd, self.himc).ok().log_err(); + } + } } fn handle_key_event( - wparam: WPARAM, - lparam: LPARAM, - state: &WindowsWindowState, - f: F, + wparam: WPARAM, + lparam: LPARAM, + state: &WindowsWindowState, + f: F, ) -> Option where - F: FnOnce(Keystroke, bool) -> PlatformInput, + F: FnOnce(Keystroke, bool) -> PlatformInput, { - let virtual_key = VIRTUAL_KEY(wparam.loword()); - let modifiers = current_modifiers(); + 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)) - } - } + 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(), - ) + 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, + vkey: VIRTUAL_KEY, + lparam: LPARAM, + mut modifiers: Modifiers, ) -> Option<(Keystroke, bool)> { - let (key_char, prefer_character_input) = process_key(vkey, lparam.hiword()); + 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) - })?; + 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, - )) + 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 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, - ) - }; + 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); - } + 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()); + 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 result_c < 0 { + return (key_char, true); + } - if key_char.is_none() { - return (None, false); - } + 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; + // 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 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 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 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, - ) + 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 - } - } + 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 } + 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 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; - } + 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); + // 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 + 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 } + 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, - } + 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 } + let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0; + Capslock { on } } // there is some additional non-visible space when talking about window @@ -1681,36 +1677,34 @@ pub(crate) fn current_capslock() -> Capslock { // - 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 + 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 + 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(); - } + 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_windows/src/keyboard.rs b/crates/gpui_windows/src/keyboard.rs index 8164bc1564..e76ce2b7cf 100644 --- a/crates/gpui_windows/src/keyboard.rs +++ b/crates/gpui_windows/src/keyboard.rs @@ -1,371 +1,366 @@ 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, + 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, + KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, }; pub(crate) struct WindowsKeyboardLayout { - id: String, - name: String, + id: String, + name: String, } pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, + key_to_vkey: HashMap, + vkey_to_key: HashMap, + vkey_to_shifted: HashMap, } impl PlatformKeyboardLayout for WindowsKeyboardLayout { - fn id(&self) -> &str { - &self.id - } + fn id(&self) -> &str { + &self.id + } - fn name(&self) -> &str { - &self.name - } + 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 - ); - } + 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 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); - }; + 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() - }; + 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 - }; + let modifiers = Modifiers { + shift, + ..keystroke.modifiers + }; - KeybindingKeystroke::new(keystroke, modifiers, key) - } + KeybindingKeystroke::new(keystroke, modifiers, key) + } - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } + 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 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(), - } - } + 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, - } - } + 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() - } - } + 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, + 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) - } + 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; - } + 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)?; + // 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()) + 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 - ) + 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) + 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, + 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 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) }; + 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, - } + 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, - } + 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, + 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}; + use crate::WindowsKeyboardMapper; + use gpui::{Keystroke, Modifiers, PlatformKeyboardMapper}; - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); + #[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()); + // 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, 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()); + // 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()); - } + // 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_windows/src/platform.rs b/crates/gpui_windows/src/platform.rs index a0366be537..f2b93f62b7 100644 --- a/crates/gpui_windows/src/platform.rs +++ b/crates/gpui_windows/src/platform.rs @@ -1,12 +1,12 @@ use std::{ - cell::{Cell, RefCell}, - ffi::OsStr, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, + cell::{Cell, RefCell}, + ffi::OsStr, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, }; use ::util::{ResultExt, paths::SanitizedPath}; @@ -18,453 +18,453 @@ use smallvec::SmallVec; #[cfg(not(feature = "wgpu"))] use windows::Win32::Graphics::Direct3D11::ID3D11Device; use windows::{ - UI::ViewManagement::UISettings, - Win32::{ - Foundation::*, - Graphics::Gdi::*, - Security::Credentials::*, - System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*}, - UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, - }, - core::*, + UI::ViewManagement::UISettings, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + Security::Credentials::*, + System::{Com::*, LibraryLoader::*, Ole::*, 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, - #[cfg(not(feature = "wgpu"))] - 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, - disable_direct_composition: bool, + 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, + #[cfg(not(feature = "wgpu"))] + 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, + disable_direct_composition: bool, } 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, + 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, - #[cfg(not(feature = "wgpu"))] - directx_devices: RefCell>, + 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, + #[cfg(not(feature = "wgpu"))] + directx_devices: RefCell>, } #[derive(Default)] struct PlatformCallbacks { - open_urls: Cell)>>>, - quit: Cell>>, - reopen: Cell>>, - app_menu_action: Cell>>, - will_open_app_menu: Cell>>, - validate_app_menu_command: Cell bool>>>, - keyboard_layout_change: Cell>>, + open_urls: Cell)>>>, + quit: Cell>>, + reopen: Cell>>, + app_menu_action: Cell>>, + will_open_app_menu: Cell>>, + validate_app_menu_command: Cell bool>>>, + keyboard_layout_change: Cell>>, } impl WindowsPlatformState { - fn new(#[cfg(not(feature = "wgpu"))] directx_devices: Option) -> Self { - let callbacks = PlatformCallbacks::default(); - let jump_list = JumpList::new(); - let current_cursor = load_cursor(CursorStyle::Arrow); + fn new(#[cfg(not(feature = "wgpu"))] 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)), - #[cfg(not(feature = "wgpu"))] - directx_devices: RefCell::new(directx_devices), - menus: RefCell::new(Vec::new()), - } - } + Self { + callbacks, + jump_list: RefCell::new(jump_list), + current_cursor: Cell::new(current_cursor), + cursor_visible: Arc::new(AtomicBool::new(true)), + #[cfg(not(feature = "wgpu"))] + 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")?; - } - #[cfg(not(feature = "wgpu"))] - 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, - ) - }; - #[cfg(feature = "wgpu")] - let text_system = - Arc::new(gpui_wgpu::CosmicTextSystem::new("Segoe UI")) as Arc; + pub fn new(headless: bool) -> Result { + unsafe { + OleInitialize(None).context("unable to initialize Windows OLE")?; + } + #[cfg(not(feature = "wgpu"))] + 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, + ) + }; + #[cfg(feature = "wgpu")] + let text_system = + Arc::new(gpui_wgpu::CosmicTextSystem::new("Segoe UI")) as Arc; - 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())); + 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), - #[cfg(not(feature = "wgpu"))] - 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?; + 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), + #[cfg(not(feature = "wgpu"))] + 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?; - #[cfg(feature = "wgpu")] - let disable_direct_composition = true; - #[cfg(not(feature = "wgpu"))] - 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); + #[cfg(feature = "wgpu")] + let disable_direct_composition = true; + #[cfg(not(feature = "wgpu"))] + 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() - }; + 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, - #[cfg(not(feature = "wgpu"))] - direct_write_text_system, - disable_direct_composition, - drop_target_helper, - invalidate_devices: Arc::new(AtomicBool::new(false)), - }) - } + Ok(Self { + inner, + handle, + raw_window_handles, + headless, + icon, + background_executor, + foreground_executor, + text_system, + #[cfg(not(feature = "wgpu"))] + direct_write_text_system, + disable_direct_composition, + drop_target_helper, + invalidate_devices: Arc::new(AtomicBool::new(false)), + }) + } - 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())) - } + 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(); - }); - } + #[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, - #[cfg(not(feature = "wgpu"))] - directx_devices: self.inner.state.directx_devices.borrow().clone().unwrap(), - invalidate_devices: self.invalidate_devices.clone(), - } - } + 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, + #[cfg(not(feature = "wgpu"))] + directx_devices: self.inner.state.directx_devices.borrow().clone().unwrap(), + invalidate_devices: self.invalidate_devices.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 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 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 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) { - #[cfg(not(feature = "wgpu"))] - let Some(directx_devices) = self.inner.state.directx_devices.borrow().clone() else { - return; - }; - #[cfg(not(feature = "wgpu"))] - let Some(direct_write_text_system) = &self.direct_write_text_system else { - return; - }; - #[cfg(not(feature = "wgpu"))] - let mut directx_device = directx_devices; - #[cfg(not(feature = "wgpu"))] - let platform_window: SafeHwnd = self.handle.into(); - #[cfg(not(feature = "wgpu"))] - let validation_number = self.inner.validation_number; - let all_windows = Arc::downgrade(&self.raw_window_handles); - #[cfg(not(feature = "wgpu"))] - let text_system = Arc::downgrade(direct_write_text_system); - #[cfg(not(feature = "wgpu"))] - let invalidate_devices = self.invalidate_devices.clone(); + fn begin_vsync_thread(&self) { + #[cfg(not(feature = "wgpu"))] + let Some(directx_devices) = self.inner.state.directx_devices.borrow().clone() else { + return; + }; + #[cfg(not(feature = "wgpu"))] + let Some(direct_write_text_system) = &self.direct_write_text_system else { + return; + }; + #[cfg(not(feature = "wgpu"))] + let mut directx_device = directx_devices; + #[cfg(not(feature = "wgpu"))] + let platform_window: SafeHwnd = self.handle.into(); + #[cfg(not(feature = "wgpu"))] + let validation_number = self.inner.validation_number; + let all_windows = Arc::downgrade(&self.raw_window_handles); + #[cfg(not(feature = "wgpu"))] + let text_system = Arc::downgrade(direct_write_text_system); + #[cfg(not(feature = "wgpu"))] + 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(); - #[cfg(not(feature = "wgpu"))] - { - 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(); - } + std::thread::Builder::new() + .name("VSyncProvider".to_owned()) + .spawn(move || { + let vsync_provider = VSyncProvider::new(); + loop { + vsync_provider.wait_for_vsync(); + #[cfg(not(feature = "wgpu"))] + { + 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; - } + 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(()) + let result = unsafe { + SendMessageW( + msg.hwnd, + WM_GPUI_KEYDOWN, + Some(msg.wParam), + Some(msg.lParam), + ) + }; + (result.0 == 0).then_some(()) } impl Platform for WindowsPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } - fn text_system(&self) -> Arc { - self.text_system.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_layout(&self) -> Box { + Box::new( + WindowsKeyboardLayout::new() + .log_err() + .unwrap_or(WindowsKeyboardLayout::unknown()), + ) + } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } + 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_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 on_thermal_state_change(&self, _callback: Box) {} - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } - fn run(&self, on_finish_launching: Box) { - on_finish_launching(); - if !self.headless { - self.begin_vsync_thread(); - } + 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); - } - } - } + 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()); + self.inner + .with_callback(|callbacks| &callbacks.quit, |callback| callback()); - // Bypass the CRT exit logic, which runs atexit handlers before calling ExitProcess. - // aws-lc registers an atexit handler that intentionally acquires a lock without releasing it. - // aws-lc also has thread_local objects which acquire this lock in their destructor. - // Destructors for thread_locals run under the loader lock, so there is a race condition - // where, if a thread exits after atexit handlers have run, the TLS destructors will block - // indefinitely on this lock while holding the loader lock. Since ExitProcess also requires - // the loader lock, process teardown will deadlock. - unsafe { - windows::Win32::System::Threading::ExitProcess(0); - } - } + // Bypass the CRT exit logic, which runs atexit handlers before calling ExitProcess. + // aws-lc registers an atexit handler that intentionally acquires a lock without releasing it. + // aws-lc also has thread_local objects which acquire this lock in their destructor. + // Destructors for thread_locals run under the loader lock, so there is a race condition + // where, if a thread exits after atexit handlers have run, the TLS destructors will block + // indefinitely on this lock while holding the loader lock. Since ExitProcess also requires + // the loader lock, process teardown will deadlock. + unsafe { + windows::Win32::System::Threading::ExitProcess(0); + } + } - fn quit(&self) { - self.foreground_executor() - .spawn(async { unsafe { PostQuitMessage(0) } }) - .detach(); - } + fn quit(&self) { + self.foreground_executor() + .spawn(async { unsafe { PostQuitMessage(0) } }) + .detach(); + } - fn restart(&self, binary_path: Option) { - let pid = std::process::id(); - let Some(app_path) = binary_path.or(self.app_path().log_err()) else { - return; - }; - let script = format!( - r#" + fn restart(&self, binary_path: Option) { + let pid = std::process::id(); + let Some(app_path) = binary_path.or(self.app_path().log_err()) else { + return; + }; + let script = format!( + r#" $pidToWaitFor = {} $exePath = "{}" @@ -477,972 +477,972 @@ impl Platform for WindowsPlatform { Start-Sleep -Seconds 0.1 }} "#, - pid, - app_path.display(), - ); + pid, + app_path.display(), + ); - // 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 - self.foreground_executor - .spawn(async move { - #[allow( - clippy::disallowed_methods, - reason = "We are restarting ourselves, using std command thus is fine" - )] - let restart_process = - ::util::command::new_std_command(::util::shell::get_windows_system_shell()) - .arg("-command") - .arg(script) - .spawn(); + // 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 + self.foreground_executor + .spawn(async move { + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = + ::util::command::new_std_command(::util::shell::get_windows_system_shell()) + .arg("-command") + .arg(script) + .spawn(); - match restart_process { - Ok(_) => unsafe { PostQuitMessage(0) }, - Err(e) => log::error!("failed to spawn restart script: {:?}", e), - } - }) - .detach(); - } + 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 activate(&self, _ignoring_other_apps: bool) {} - fn hide(&self) {} + fn hide(&self) {} - // todo(windows) - fn hide_other_apps(&self) { - unimplemented!() - } + // todo(windows) + fn hide_other_apps(&self) { + unimplemented!() + } - // todo(windows) - fn unhide_other_apps(&self) { - unimplemented!() - } + // todo(windows) + fn unhide_other_apps(&self) { + unimplemented!() + } - fn displays(&self) -> Vec> { - WindowsDisplay::displays() - } + fn displays(&self) -> Vec> { + WindowsDisplay::displays() + } - fn primary_display(&self) -> Option> { - WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc) - } + 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 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) - } + #[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 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()); + 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)) - } + Ok(Box::new(window)) + } - fn window_appearance(&self) -> WindowAppearance { - system_appearance().log_err().unwrap_or_default() - } + 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 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 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(); + 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 - } + 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(); + 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 - } + rx + } - fn can_select_mixed_files_and_dirs(&self) -> bool { - // The FOS_PICKFOLDERS flag toggles between "only files" and "only folders". - false - } + 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 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 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) { - self.inner.state.callbacks.quit.set(Some(callback)); - } + fn on_quit(&self, callback: Box) { + self.inner.state.callbacks.quit.set(Some(callback)); + } - fn on_reopen(&self, callback: Box) { - self.inner.state.callbacks.reopen.set(Some(callback)); - } + fn on_reopen(&self, callback: Box) { + self.inner.state.callbacks.reopen.set(Some(callback)); + } - fn set_menus(&self, menus: Vec, _keymap: &Keymap) { - *self.inner.state.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect(); - } + fn set_menus(&self, menus: Vec, _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 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 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_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_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 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()?) - } + 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"); - } + // 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 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; - } + 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; - } - } - } + 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 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 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 write_to_clipboard(&self, item: ClipboardItem) { + write_to_clipboard(item); + } - fn read_from_clipboard(&self) -> Option { - read_from_clipboard() - } + fn read_from_clipboard(&self) -> Option { + read_from_clipboard() + } - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { - 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 write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + 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, - ) - }; + 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 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))) - } - }) - } + 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 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 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 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) - } + fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Task>> { + self.update_jump_list(menus, entries) + } } impl WindowsPlatformInner { - fn new(context: &mut PlatformWindowCreateContext) -> Result> { - #[cfg(not(feature = "wgpu"))] - let state = WindowsPlatformState::new(context.directx_devices.take()); - #[cfg(feature = "wgpu")] - let state = WindowsPlatformState::new(); - 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")?, - })) - } + fn new(context: &mut PlatformWindowCreateContext) -> Result> { + #[cfg(not(feature = "wgpu"))] + let state = WindowsPlatformState::new(context.directx_devices.take()); + #[cfg(feature = "wgpu")] + let state = WindowsPlatformState::new(); + 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)); - } - } + /// 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 => self.handle_gpui_events(msg, wparam, lparam), - _ => None, - }; - if let Some(result) = handled { - LRESULT(result) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } - } - } + 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 => self.handle_gpui_events(msg, wparam, lparam), + _ => 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 => { - #[cfg(not(feature = "wgpu"))] - return self.handle_device_lost(lparam); - #[cfg(feature = "wgpu")] - Some(0) - } - _ => unreachable!(), - } - } + 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 => { + #[cfg(not(feature = "wgpu"))] + return self.handle_device_lost(lparam); + #[cfg(feature = "wgpu")] + Some(0) + } + _ => unreachable!(), + } + } - 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); + 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() - } + lock.is_empty() + } - #[inline] - fn run_foreground_task(&self) -> Option { - const MAIN_TASK_TIMEOUT: u128 = 10; + #[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, - } - } + 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); + // 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, - } - } + WindowsDispatcher::execute_runnable(runnable); + } + _ => break 'tasks, + } + } - Some(0) - } + 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_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_keyboard_layout_change(&self) -> Option { + self.with_callback( + |callbacks| &callbacks.keyboard_layout_change, + |callback| callback(), + ); + Some(0) + } - #[cfg(not(feature = "wgpu"))] - 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()); + #[cfg(not(feature = "wgpu"))] + 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) - } + Some(0) + } } impl Drop for WindowsPlatform { - fn drop(&mut self) { - unsafe { - DestroyWindow(self.handle) - .context("Destroying platform window") - .log_err(); - OleUninitialize(); - } - } + fn drop(&mut self) { + unsafe { + 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, - #[cfg(not(feature = "wgpu"))] - 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, + 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, + #[cfg(not(feature = "wgpu"))] + 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, } struct PlatformWindowCreateContext { - inner: Option>>, - raw_window_handles: std::sync::Weak>>, - validation_number: usize, - main_sender: Option>, - main_receiver: Option>, - #[cfg(not(feature = "wgpu"))] - directx_devices: Option, - dispatcher: Option>, + inner: Option>>, + raw_window_handles: std::sync::Weak>>, + validation_number: usize, + main_sender: Option>, + main_receiver: Option>, + #[cfg(not(feature = "wgpu"))] + directx_devices: Option, + dispatcher: Option>, } 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(()) - } + 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 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 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 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)) - } - }) + 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, + options: PathPromptOptions, + window: Option, ) -> Result>> { - let folder_dialog: IFileOpenDialog = - unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_ALL)? }; + 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; - } + 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)?; + unsafe { + folder_dialog.SetOptions(dialog_options)?; - if let Some(prompt) = options.prompt { - let prompt: &str = &prompt; - folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?; - } + 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); - } - } + 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 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)); - } + 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)) + Ok(Some(paths)) } fn file_save_dialog( - directory: PathBuf, - suggested_name: Option, - window: Option, + 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 = SanitizedPath::new(&full_path); - let full_path_string = full_path.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() - }; - } + 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 = SanitizedPath::new(&full_path); + let full_path_string = full_path.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() - }; - } + 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))) + 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)) + 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()?) + let ui_settings = UISettings::new()?; + Ok(ui_settings.AutoHideScrollBars()?) } #[cfg(not(feature = "wgpu"))] 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 - } - } + let device_state = unsafe { device.GetDeviceRemovedReason() }; + match device_state { + Ok(_) => false, + Err(err) => { + log::error!("DirectX device lost detected: {:?}", err); + true + } + } } #[cfg(not(feature = "wgpu"))] 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, + 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)); + // 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."); + *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), - ); - } + 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(()) + 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) }; + 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, + 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 }; + 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, - ))); + 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) - } - }; - } + 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) } - }; + 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)) }; - } + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { drop(Box::from_raw(ptr)) }; + } - result + result } #[cfg(test)] mod tests { - use crate::{read_from_clipboard, write_to_clipboard}; - use gpui::ClipboardItem; + use crate::{read_from_clipboard, write_to_clipboard}; + use gpui::ClipboardItem; - #[test] - fn test_clipboard() { - let item = ClipboardItem::new_string("你好,我是张小白".to_string()); - write_to_clipboard(item.clone()); - assert_eq!(read_from_clipboard(), Some(item)); + #[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("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)); - } + 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_windows/src/system_settings.rs b/crates/gpui_windows/src/system_settings.rs index 53214f40de..ecf632df29 100644 --- a/crates/gpui_windows/src/system_settings.rs +++ b/crates/gpui_windows/src/system_settings.rs @@ -1,87 +1,87 @@ use std::{ - cell::Cell, - ffi::{c_uint, c_void}, + cell::Cell, + ffi::{c_uint, c_void}, }; use ::util::ResultExt; use windows::Win32::UI::WindowsAndMessaging::{ - SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_ACTION, - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW, + 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, + 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, + /// 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 - } + pub(crate) fn new() -> Self { + let mut settings = Self::default(); + settings.init(); + settings + } - fn init(&mut self) { - self.mouse_wheel_settings.update(); - } + 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(), - _ => {} - } - } + 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(); - } + 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(&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(), - ) - }; + 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); - } - } + 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(), - ) - }; + 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); - } - } + if result.log_err() != None && self.wheel_scroll_lines.get() != value { + self.wheel_scroll_lines.set(value); + } + } } diff --git a/crates/gpui_windows/src/util.rs b/crates/gpui_windows/src/util.rs index 04e26990a8..7e1bb85973 100644 --- a/crates/gpui_windows/src/util.rs +++ b/crates/gpui_windows/src/util.rs @@ -4,12 +4,12 @@ use ::util::ResultExt; #[cfg(not(feature = "wgpu"))] use anyhow::Context; use windows::{ - UI::{ - Color, - ViewManagement::{UIColorType, UISettings}, - }, - Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*}, - core::BOOL, + UI::{ + Color, + ViewManagement::{UIColorType, UISettings}, + }, + Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*}, + core::BOOL, }; #[cfg(not(feature = "wgpu"))] use windows::{Win32::System::LibraryLoader::LoadLibraryA, core::PCSTR}; @@ -18,175 +18,175 @@ 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; + 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 hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } - fn loword(&self) -> u16 { - (self.0 & 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_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } - fn signed_loword(&self) -> i16 { - (self.0 & 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 hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } - fn loword(&self) -> u16 { - (self.0 & 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_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } - fn signed_loword(&self) -> i16 { - (self.0 & 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 - } + #[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, + 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 - } + #[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) + 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() - })), - ) + 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(); - } + 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), - } + 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) - } + 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) + ((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128) } #[cfg(not(feature = "wgpu"))] pub(crate) fn with_dll_library(dll_name: PCSTR, f: F) -> Result where - F: FnOnce(HMODULE) -> Result, + 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 + 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_windows/src/vsync.rs b/crates/gpui_windows/src/vsync.rs index 73c32cf9b9..8547c2a1e0 100644 --- a/crates/gpui_windows/src/vsync.rs +++ b/crates/gpui_windows/src/vsync.rs @@ -1,81 +1,81 @@ use std::{ - sync::LazyLock, - time::{Duration, Instant}, + sync::LazyLock, + time::{Duration, Instant}, }; use anyhow::{Context, Result}; use util::ResultExt; use windows::Win32::{ - Foundation::HWND, - Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo}, - System::Performance::QueryPerformanceFrequency, + 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 + 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>, + 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 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); - } - } + 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) - } + 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) + let ticks_per_microsecond = ticks_per_second / 1_000_000; + Duration::from_micros(counts / ticks_per_microsecond) } diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index e8d5bca75b..90161a4248 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -1,13 +1,13 @@ #![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}, + cell::{Cell, RefCell}, + num::NonZeroIsize, + path::PathBuf, + rc::{Rc, Weak}, + str::FromStr, + sync::{Arc, Once, atomic::AtomicBool}, + time::{Duration, Instant}, }; use ::util::ResultExt; @@ -16,16 +16,16 @@ use futures::channel::oneshot::{self, Receiver}; 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::*, + 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; @@ -38,556 +38,554 @@ use gpui_wgpu::{WgpuRenderer, WgpuSurfaceConfig, wgpu}; pub(crate) struct WindowsWindow(pub Rc); impl std::ops::Deref for WindowsWindow { - type Target = WindowsWindowInner; + type Target = WindowsWindowInner; - fn deref(&self) -> &Self::Target { - &self.0 - } + 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 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 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, - #[cfg(feature = "wgpu")] - pub renderer: RefCell, - #[cfg(not(feature = "wgpu"))] - pub renderer: RefCell, - /// Set after a GPU device-lost recovery so the next `draw_window` call is - /// treated as a forced render. This guarantees the next frame both - /// re-enables drawing (via `mark_drawable`) and bypasses the GPUI view - /// cache, which would otherwise replay stale atlas tile references from - /// the previous frame and panic in `DirectXAtlasState::texture`. - pub force_render_after_recovery: Cell, + #[cfg(feature = "wgpu")] + pub renderer: RefCell, + #[cfg(not(feature = "wgpu"))] + pub renderer: RefCell, + /// Set after a GPU device-lost recovery so the next `draw_window` call is + /// treated as a forced render. This guarantees the next frame both + /// re-enables drawing (via `mark_drawable`) and bypasses the GPUI view + /// cache, which would otherwise replay stale atlas tile references from + /// the previous frame and panic in `DirectXAtlasState::texture`. + pub force_render_after_recovery: Cell, - pub click_state: ClickState, - pub current_cursor: Cell>, - /// Shared with [`WindowsPlatformState::cursor_visible`]. - pub cursor_visible: Arc, - pub nc_button_pressed: Cell>, - pub dragging: Cell, + pub click_state: ClickState, + pub current_cursor: Cell>, + /// Shared with [`WindowsPlatformState::cursor_visible`]. + pub cursor_visible: Arc, + pub nc_button_pressed: Cell>, + pub dragging: 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. - #[cfg(not(feature = "wgpu"))] - pub invalidate_devices: Arc, - fullscreen: Cell>, - initial_placement: Cell>, - hwnd: HWND, - pub(crate) a11y: RefCell>, + 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. + #[cfg(not(feature = "wgpu"))] + pub invalidate_devices: Arc, + 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) executor: ForegroundExecutor, - pub(crate) validation_number: usize, - pub(crate) main_receiver: PriorityQueueReceiver, - pub(crate) platform_window_handle: HWND, - pub(crate) parent_hwnd: Option, + 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) 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, - #[cfg(not(feature = "wgpu"))] directx_devices: &DirectXDevices, - window_params: &CREATESTRUCTW, - current_cursor: Option, - cursor_visible: Arc, - display: WindowsDisplay, - min_size: Option>, - appearance: WindowAppearance, - #[cfg(not(feature = "wgpu"))] disable_direct_composition: bool, - #[cfg(not(feature = "wgpu"))] invalidate_devices: Arc, - ) -> 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 physical_size = size( - DevicePixels(window_params.cx), - DevicePixels(window_params.cy), - ); - let logical_size = { 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; - #[cfg(feature = "wgpu")] - let renderer = WgpuRenderer::new( - Rc::new(RefCell::new(None)), - &RawWindow { hwnd }, - WgpuSurfaceConfig { - size: physical_size, - transparent: false, - preferred_present_mode: Some(wgpu::PresentMode::Mailbox), - }, - None, - None, - ) - .context("Creating Wgpu renderer")?; - #[cfg(not(feature = "wgpu"))] - 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; + fn new( + hwnd: HWND, + #[cfg(not(feature = "wgpu"))] directx_devices: &DirectXDevices, + window_params: &CREATESTRUCTW, + current_cursor: Option, + cursor_visible: Arc, + display: WindowsDisplay, + min_size: Option>, + appearance: WindowAppearance, + #[cfg(not(feature = "wgpu"))] disable_direct_composition: bool, + #[cfg(not(feature = "wgpu"))] invalidate_devices: Arc, + ) -> 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 physical_size = size( + DevicePixels(window_params.cx), + DevicePixels(window_params.cy), + ); + let logical_size = { 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; + #[cfg(feature = "wgpu")] + let renderer = WgpuRenderer::new( + Rc::new(RefCell::new(None)), + &RawWindow { hwnd }, + WgpuSurfaceConfig { + size: physical_size, + transparent: false, + preferred_present_mode: Some(wgpu::PresentMode::Mailbox), + }, + None, + None, + ) + .context("Creating Wgpu renderer")?; + #[cfg(not(feature = "wgpu"))] + 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")?; + 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_after_recovery: Cell::new(false), - click_state, - current_cursor: Cell::new(current_cursor), - cursor_visible, - nc_button_pressed: Cell::new(nc_button_pressed), - dragging: Cell::new(false), - display: Cell::new(display), - fullscreen: Cell::new(fullscreen), - initial_placement: Cell::new(initial_placement), - hwnd, - #[cfg(not(feature = "wgpu"))] - invalidate_devices, - direct_manipulation, - a11y: RefCell::new(None), - }) - } + 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_after_recovery: Cell::new(false), + click_state, + current_cursor: Cell::new(current_cursor), + cursor_visible, + nc_button_pressed: Cell::new(nc_button_pressed), + dragging: Cell::new(false), + display: Cell::new(display), + fullscreen: Cell::new(fullscreen), + initial_placement: Cell::new(initial_placement), + hwnd, + #[cfg(not(feature = "wgpu"))] + invalidate_devices, + direct_manipulation, + a11y: RefCell::new(None), + }) + } - #[inline] - pub(crate) fn is_fullscreen(&self) -> bool { - self.fullscreen.get().is_some() - } + #[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() - } + 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(), - } - } + 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, - ) - } + // 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(); + 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) - } - } + 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() - } + /// 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, - #[cfg(not(feature = "wgpu"))] - &context.directx_devices, - cs, - context.current_cursor, - context.cursor_visible.clone(), - context.display, - context.min_size, - context.appearance, - #[cfg(not(feature = "wgpu"))] - context.disable_direct_composition, - #[cfg(not(feature = "wgpu"))] - context.invalidate_devices.clone(), - )?; + fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { + let state = WindowsWindowState::new( + hwnd, + #[cfg(not(feature = "wgpu"))] + &context.directx_devices, + cs, + context.current_cursor, + context.cursor_visible.clone(), + context.display, + context.min_size, + context.appearance, + #[cfg(not(feature = "wgpu"))] + context.disable_direct_composition, + #[cfg(not(feature = "wgpu"))] + context.invalidate_devices.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, - 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, - })) - } + 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, + 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); + 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(); - } + 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(()) - } + 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 - } + 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>>, + 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, - 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, - #[cfg(not(feature = "wgpu"))] - disable_direct_composition: bool, - #[cfg(not(feature = "wgpu"))] - directx_devices: DirectXDevices, - #[cfg(not(feature = "wgpu"))] - invalidate_devices: Arc, - parent_hwnd: Option, + inner: Option>>, + handle: AnyWindowHandle, + hide_title_bar: bool, + display: WindowsDisplay, + is_movable: 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, + #[cfg(not(feature = "wgpu"))] + disable_direct_composition: bool, + #[cfg(not(feature = "wgpu"))] + directx_devices: DirectXDevices, + #[cfg(not(feature = "wgpu"))] + invalidate_devices: Arc, + parent_hwnd: Option, } impl WindowsWindow { - pub(crate) fn new( - handle: AnyWindowHandle, - params: WindowParams, - creation_info: WindowCreationInfo, - ) -> Result { - let WindowCreationInfo { - icon, - executor, - current_cursor, - cursor_visible, - drop_target_helper, - validation_number, - main_receiver, - platform_window_handle, - disable_direct_composition, - #[cfg(not(feature = "wgpu"))] - directx_devices, - invalidate_devices, - } = creation_info; - #[cfg(feature = "wgpu")] - { - _ = invalidate_devices; - } - 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(""), - ); + pub(crate) fn new( + handle: AnyWindowHandle, + params: WindowParams, + creation_info: WindowCreationInfo, + ) -> Result { + let WindowCreationInfo { + icon, + executor, + current_cursor, + cursor_visible, + drop_target_helper, + validation_number, + main_receiver, + platform_window_handle, + disable_direct_composition, + #[cfg(not(feature = "wgpu"))] + directx_devices, + invalidate_devices, + } = creation_info; + #[cfg(feature = "wgpu")] + { + _ = invalidate_devices; + } + 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, WINDOW_STYLE(0x0)) - } else { - let mut dwstyle = WS_SYSMENU; + let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { + (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0)) + } else { + let mut dwstyle = WS_SYSMENU; - if params.is_resizable { - dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX; - } + 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 - }; + 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; - } + (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, - min_size: params.window_min_size, - executor, - current_cursor, - cursor_visible, - drop_target_helper, - validation_number, - main_receiver, - platform_window_handle, - appearance, - #[cfg(not(feature = "wgpu"))] - disable_direct_composition, - #[cfg(not(feature = "wgpu"))] - directx_devices, - #[cfg(not(feature = "wgpu"))] - invalidate_devices, - 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 _), - ) - }; + 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, + min_size: params.window_min_size, + executor, + current_cursor, + cursor_visible, + drop_target_helper, + validation_number, + main_receiver, + platform_window_handle, + appearance, + #[cfg(not(feature = "wgpu"))] + disable_direct_composition, + #[cfg(not(feature = "wgpu"))] + directx_devices, + #[cfg(not(feature = "wgpu"))] + invalidate_devices, + 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(); + // 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.scale_factor.get(), - &this.state.border_offset, - )?; - if params.show { - unsafe { SetWindowPlacement(hwnd, &placement)? }; - } else { - this.state.initial_placement.set(Some(WindowOpenStatus { - placement, - state: WindowOpenState::Windowed, - })); - } + 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.scale_factor.get(), + &this.state.border_offset, + )?; + if params.show { + unsafe { SetWindowPlacement(hwnd, &placement)? }; + } else { + this.state.initial_placement.set(Some(WindowOpenStatus { + placement, + state: WindowOpenState::Windowed, + })); + } - Ok(Self(this)) - } + Ok(Self(this)) + } } #[cfg(feature = "wgpu")] #[derive(Debug, Clone, Copy)] pub(crate) struct RawWindow { - pub hwnd: HWND, + pub hwnd: HWND, } #[cfg(feature = "wgpu")] @@ -597,1163 +595,1163 @@ unsafe impl Sync for RawWindow {} #[cfg(feature = "wgpu")] impl rwh::HasWindowHandle for RawWindow { - fn window_handle(&self) -> std::result::Result, rwh::HandleError> { - let raw = rwh::Win32WindowHandle::new(unsafe { - NonZeroIsize::new_unchecked(self.hwnd.0 as isize) - }) - .into(); - Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) - } + fn window_handle(&self) -> std::result::Result, rwh::HandleError> { + let raw = rwh::Win32WindowHandle::new(unsafe { + NonZeroIsize::new_unchecked(self.hwnd.0 as isize) + }) + .into(); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) + } } #[cfg(feature = "wgpu")] impl rwh::HasDisplayHandle for RawWindow { - fn display_handle(&self) -> std::result::Result, rwh::HandleError> { - Ok(rwh::DisplayHandle::windows()) - } + fn display_handle(&self) -> std::result::Result, rwh::HandleError> { + Ok(rwh::DisplayHandle::windows()) + } } 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) }) - } + 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()) - } + 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(); - } + 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 bounds(&self) -> Bounds { + self.state.bounds() + } - fn is_maximized(&self) -> bool { - self.state.is_maximized() - } + fn is_maximized(&self) -> bool { + self.state.is_maximized() + } - fn window_bounds(&self) -> WindowBounds { - self.state.window_bounds() - } + 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() - } + /// 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); + 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(); - } + 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 scale_factor(&self) -> f32 { + self.state.scale_factor.get() + } - fn appearance(&self) -> WindowAppearance { - self.state.appearance.get() - } + fn appearance(&self) -> WindowAppearance { + self.state.appearance.get() + } - fn display(&self) -> Option> { - Some(Rc::new(self.state.display.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 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 modifiers(&self) -> Modifiers { + current_modifiers() + } - fn capslock(&self) -> Capslock { - current_capslock() - } + fn capslock(&self) -> Capslock { + current_capslock() + } - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.state.input_handler.set(Some(input_handler)); - } + 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 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(); + 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(); + 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(); + if let Some(clicked) = + button_id_map.iter().position(|&button_id| button_id == res) + { + let _ = done_tx.send(clicked); + } + } + }) + .detach(); - Some(done_rx) - } + 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(); + 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(); - } + 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(); - } + 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) }; + // 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(); - } + // todo(windows) + // crate `windows 0.56` reports true as Err + unsafe { SetForegroundWindow(hwnd).as_bool() }; + }) + .detach(); + } - fn is_active(&self) -> bool { - self.0.hwnd == unsafe { GetActiveWindow() } - } + fn is_active(&self) -> bool { + self.0.hwnd == unsafe { GetActiveWindow() } + } - fn is_hovered(&self) -> bool { - self.state.hovered.get() - } + fn is_hovered(&self) -> bool { + self.state.hovered.get() + } - fn background_appearance(&self) -> WindowBackgroundAppearance { - self.state.background_appearance.get() - } + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.state.background_appearance.get() + } - fn is_subpixel_rendering_supported(&self) -> bool { - true - } + 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_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; + 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); - } - } - } + // 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 minimize(&self) { + unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() }; + } - fn zoom(&self) { - let is_visible = unsafe { IsWindowVisible(self.0.hwnd).as_bool() }; - if !is_visible { - if let Some(mut status) = self.state.initial_placement.take() { - status.state = WindowOpenState::Maximized; - self.state.initial_placement.set(Some(status)); - } - return; - } + fn zoom(&self) { + let is_visible = unsafe { IsWindowVisible(self.0.hwnd).as_bool() }; + if !is_visible { + if let Some(mut status) = self.state.initial_placement.take() { + status.state = WindowOpenState::Maximized; + self.state.initial_placement.set(Some(status)); + } + return; + } - let window_operation = if self.is_maximized() { - SW_RESTORE - } else { - SW_MAXIMIZE - }; - unsafe { - ShowWindowAsync(self.0.hwnd, window_operation) - .ok() - .log_err(); - } - } + let window_operation = if self.is_maximized() { + SW_RESTORE + } else { + SW_MAXIMIZE + }; + unsafe { + ShowWindowAsync(self.0.hwnd, window_operation) + .ok() + .log_err(); + } + } - 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 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 is_fullscreen(&self) -> bool { + self.state.is_fullscreen() + } - fn start_window_move(&self) { - // winit does this by tracking whether the user is mouse-dragging - // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/window.rs#L241-L269 - // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/event_loop.rs#L1234-L1243 - self.state.dragging.set(true); + fn start_window_move(&self) { + // winit does this by tracking whether the user is mouse-dragging + // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/window.rs#L241-L269 + // https://github.com/rust-windowing/winit/blob/9674d8ceef6976326fe9583a81f2e684daac05d6/winit-win32/src/event_loop.rs#L1234-L1243 + self.state.dragging.set(true); - let cursor_pos = { - let mut pos = unsafe { std::mem::zeroed() }; - let _ = unsafe { GetCursorPos(&mut pos) }; - pos - }; - let points = POINTS { - x: cursor_pos.x as i16, - y: cursor_pos.y as i16, - }; + let cursor_pos = { + let mut pos = unsafe { std::mem::zeroed() }; + let _ = unsafe { GetCursorPos(&mut pos) }; + pos + }; + let points = POINTS { + x: cursor_pos.x as i16, + y: cursor_pos.y as i16, + }; - let _ = unsafe { ReleaseCapture() }; - let _ = unsafe { - PostMessageW( - Some(self.0.hwnd), - WM_NCLBUTTONDOWN, - WPARAM(HTCAPTION as usize), - LPARAM(&points as *const _ as isize), - ) - }; - } + let _ = unsafe { ReleaseCapture() }; + let _ = unsafe { + PostMessageW( + Some(self.0.hwnd), + WM_NCLBUTTONDOWN, + WPARAM(HTCAPTION as usize), + LPARAM(&points as *const _ as isize), + ) + }; + } - fn on_request_frame(&self, callback: Box) { - self.state.callbacks.request_frame.set(Some(callback)); - } + 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_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_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_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_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_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_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_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_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 on_appearance_changed(&self, callback: Box) { + self.0 + .state + .callbacks + .appearance_changed + .set(Some(callback)); + } - fn draw(&self, scene: &Scene) { - #[cfg(not(feature = "wgpu"))] - { - self.state - .renderer - .borrow_mut() - .draw(scene, self.state.background_appearance.get()) - .log_err(); - } + fn draw(&self, scene: &Scene) { + #[cfg(not(feature = "wgpu"))] + { + self.state + .renderer + .borrow_mut() + .draw(scene, self.state.background_appearance.get()) + .log_err(); + } - #[cfg(feature = "wgpu")] - { - let mut renderer = self.state.renderer.borrow_mut(); - if renderer.device_lost() { - match renderer.recover(&RawWindow { - hwnd: self.platform_window_handle, - }) { - Ok(()) => {} - Err(err) => { - log::warn!("GPU recovery failed, will retry on next frame: {err}"); - } - } + #[cfg(feature = "wgpu")] + { + let mut renderer = self.state.renderer.borrow_mut(); + if renderer.device_lost() { + match renderer.recover(&RawWindow { + hwnd: self.platform_window_handle, + }) { + Ok(()) => {} + Err(err) => { + log::warn!("GPU recovery failed, will retry on next frame: {err}"); + } + } - self.state.force_render_after_recovery.set(true); - return; - } - if !renderer.draw(scene) { - log::error!("failed to render scene"); - } + self.state.force_render_after_recovery.set(true); + return; + } + if !renderer.draw(scene) { + log::error!("failed to render scene"); + } - if renderer.needs_redraw() { - self.state.force_render_after_recovery.set(true); - } - } - } + if renderer.needs_redraw() { + self.state.force_render_after_recovery.set(true); + } + } + } - fn sprite_atlas(&self) -> Arc { - self.state.renderer.borrow().sprite_atlas().clone() - } + fn sprite_atlas(&self) -> Arc { + self.state.renderer.borrow().sprite_atlas().clone() + } - fn get_raw_handle(&self) -> HWND { - self.0.hwnd - } + fn get_raw_handle(&self) -> HWND { + self.0.hwnd + } - fn gpu_specs(&self) -> Option { - #[cfg(feature = "wgpu")] - return Some(self.state.renderer.borrow().gpu_specs()); - #[cfg(not(feature = "wgpu"))] - self.state.renderer.borrow().gpu_specs().log_err() - } + fn gpu_specs(&self) -> Option { + #[cfg(feature = "wgpu")] + return Some(self.state.renderer.borrow().gpu_specs()); + #[cfg(not(feature = "wgpu"))] + 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), - }; + 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); - } + 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 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; + 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 adapter = accesskit_windows::Adapter::new( + accesskit_windows::HWND(self.0.hwnd.0), + is_focused, + action_handler, + ); - let activation_handler = A11yActivationHandler { - callback: callbacks.activation, - }; + let activation_handler = A11yActivationHandler { + callback: callbacks.activation, + }; - *self.state.a11y.borrow_mut() = Some(A11yState { - adapter, - activation_handler, - }); - } + *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_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. - } + 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) adapter: accesskit_windows::Adapter, + pub(crate) activation_handler: A11yActivationHandler, } pub(crate) struct A11yActivationHandler { - callback: Box Option + Send + 'static>, + callback: Box Option + Send + 'static>, } impl accesskit::ActivationHandler for A11yActivationHandler { - fn request_initial_tree(&mut self) -> Option { - (self.callback)() - } + 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); - } + 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)); - } - } + 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 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); + 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(()) - } + 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); + 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(()) - } + 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); + 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(()) - } + 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, + 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); + 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), - } - } + 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); + /// 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() - } + 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)), - _ => {} - } - } + 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; + #[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() - } + 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, + 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, + 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, + 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, + 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(()) - } + 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, + placement: WINDOWPLACEMENT, + state: WindowOpenState, } #[derive(Clone, Copy)] enum WindowOpenState { - Maximized, - Fullscreen, - Windowed, + 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) }; - }); + 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, + 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) - } - }; - } + 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) } - }; + 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)) }; - } + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { drop(Box::from_raw(ptr)) }; + } - result + result } pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { - if hwnd.is_invalid() { - return None; - } + 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 - } + 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 + 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 - } + 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(()) + 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 + // 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, + 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), - } + 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, - scale_factor: f32, - border_offset: &WindowBorderOffset, + hwnd: HWND, + display: WindowsDisplay, + initial_bounds: Bounds, + scale_factor: f32, + 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() - }; - let bounds = bounds.to_device_pixels(scale_factor); - placement.rcNormalPosition = calculate_window_rect(bounds, border_offset); - Ok(placement) + 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() + }; + let bounds = bounds.to_device_pixels(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) }; + 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; - } + // 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, - ); + 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; - } - } + 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) }; + 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; - } + 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 set_window_composition_attribute: SetWindowCompositionAttributeType = - std::mem::transmute(GetProcAddress(user32, func_name)); - 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 _); - } - } + 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 set_window_composition_attribute: SetWindowCompositionAttributeType = + std::mem::transmute(GetProcAddress(user32, func_name)); + 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(); - } + 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; + 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_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 - ); - } + #[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_windows/src/wrapper.rs b/crates/gpui_windows/src/wrapper.rs index 60bbc433ca..50599e69ed 100644 --- a/crates/gpui_windows/src/wrapper.rs +++ b/crates/gpui_windows/src/wrapper.rs @@ -4,50 +4,50 @@ use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR}; #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { - raw: HCURSOR, + raw: HCURSOR, } unsafe impl Send for SafeCursor {} unsafe impl Sync for SafeCursor {} impl From for SafeCursor { - fn from(value: HCURSOR) -> Self { - SafeCursor { raw: value } - } + fn from(value: HCURSOR) -> Self { + SafeCursor { raw: value } + } } impl Deref for SafeCursor { - type Target = HCURSOR; + type Target = HCURSOR; - fn deref(&self) -> &Self::Target { - &self.raw - } + fn deref(&self) -> &Self::Target { + &self.raw + } } #[derive(Debug, Clone, Copy)] pub(crate) struct SafeHwnd { - raw: HWND, + raw: HWND, } impl SafeHwnd { - pub(crate) fn as_raw(&self) -> HWND { - self.raw - } + 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 } - } + fn from(value: HWND) -> Self { + SafeHwnd { raw: value } + } } impl Deref for SafeHwnd { - type Target = HWND; + type Target = HWND; - fn deref(&self) -> &Self::Target { - &self.raw - } + fn deref(&self) -> &Self::Target { + &self.raw + } } diff --git a/crates/gpui_zed_util/src/archive.rs b/crates/gpui_zed_util/src/archive.rs index 7fe43a25c3..4877c0ab75 100644 --- a/crates/gpui_zed_util/src/archive.rs +++ b/crates/gpui_zed_util/src/archive.rs @@ -8,376 +8,376 @@ use futures::{AsyncRead, io::BufReader}; #[cfg(any(unix, windows))] fn archive_path_is_normal(filename: &str) -> bool { - Path::new(filename).components().all(|c| { - matches!( - c, - std::path::Component::Normal(_) | std::path::Component::CurDir - ) - }) + Path::new(filename).components().all(|c| { + matches!( + c, + std::path::Component::Normal(_) | std::path::Component::CurDir + ) + }) } #[cfg(windows)] pub async fn extract_zip(destination: &Path, reader: R) -> Result<()> { - let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader)); + let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader)); - let destination = &destination - .canonicalize() - .unwrap_or_else(|_| destination.to_path_buf()); + let destination = &destination + .canonicalize() + .unwrap_or_else(|_| destination.to_path_buf()); - while let Some(mut item) = reader.next_with_entry().await? { - let entry_reader = item.reader_mut(); - let entry = entry_reader.entry(); - let filename = entry - .filename() - .as_str() - .context("reading zip entry file name")?; + while let Some(mut item) = reader.next_with_entry().await? { + let entry_reader = item.reader_mut(); + let entry = entry_reader.entry(); + let filename = entry + .filename() + .as_str() + .context("reading zip entry file name")?; - if !archive_path_is_normal(filename) { - reader = item.skip().await.context("reading next zip entry")?; - continue; - } + if !archive_path_is_normal(filename) { + reader = item.skip().await.context("reading next zip entry")?; + continue; + } - let path = destination.join(filename); + let path = destination.join(filename); - if entry - .dir() - .with_context(|| format!("reading zip entry metadata for path {path:?}"))? - { - std::fs::create_dir_all(&path) - .with_context(|| format!("creating directory {path:?}"))?; - } else { - let parent_dir = path - .parent() - .with_context(|| format!("no parent directory for {path:?}"))?; - std::fs::create_dir_all(parent_dir) - .with_context(|| format!("creating parent directory {parent_dir:?}"))?; - let mut file = smol::fs::File::create(&path) - .await - .with_context(|| format!("creating file {path:?}"))?; - futures::io::copy(entry_reader, &mut file) - .await - .with_context(|| format!("extracting into file {path:?}"))?; - } + if entry + .dir() + .with_context(|| format!("reading zip entry metadata for path {path:?}"))? + { + std::fs::create_dir_all(&path) + .with_context(|| format!("creating directory {path:?}"))?; + } else { + let parent_dir = path + .parent() + .with_context(|| format!("no parent directory for {path:?}"))?; + std::fs::create_dir_all(parent_dir) + .with_context(|| format!("creating parent directory {parent_dir:?}"))?; + let mut file = smol::fs::File::create(&path) + .await + .with_context(|| format!("creating file {path:?}"))?; + futures::io::copy(entry_reader, &mut file) + .await + .with_context(|| format!("extracting into file {path:?}"))?; + } - reader = item.skip().await.context("reading next zip entry")?; - } + reader = item.skip().await.context("reading next zip entry")?; + } - Ok(()) + Ok(()) } #[cfg(unix)] pub async fn extract_zip(destination: &Path, reader: R) -> Result<()> { - // Unix needs file permissions copied when extracting. - // This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used. - // `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`. - // - // A typical `reader` comes from a streaming network response, so cannot be sought right away, - // and reading the entire archive into the memory seems wasteful. - // - // So, save the stream into a temporary file first and then get it read with a seeking reader. - let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?); - futures::io::copy(&mut BufReader::new(reader), &mut file) - .await - .context("saving archive contents into the temporary file")?; - extract_seekable_zip(destination, file).await + // Unix needs file permissions copied when extracting. + // This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used. + // `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`. + // + // A typical `reader` comes from a streaming network response, so cannot be sought right away, + // and reading the entire archive into the memory seems wasteful. + // + // So, save the stream into a temporary file first and then get it read with a seeking reader. + let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?); + futures::io::copy(&mut BufReader::new(reader), &mut file) + .await + .context("saving archive contents into the temporary file")?; + extract_seekable_zip(destination, file).await } #[cfg(unix)] pub async fn extract_seekable_zip( - destination: &Path, - reader: R, + destination: &Path, + reader: R, ) -> Result<()> { - let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader)) - .await - .context("reading the zip archive")?; - let destination = &destination - .canonicalize() - .unwrap_or_else(|_| destination.to_path_buf()); - for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() { - let filename = entry - .filename() - .as_str() - .context("reading zip entry file name")?; + let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader)) + .await + .context("reading the zip archive")?; + let destination = &destination + .canonicalize() + .unwrap_or_else(|_| destination.to_path_buf()); + for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() { + let filename = entry + .filename() + .as_str() + .context("reading zip entry file name")?; - if !archive_path_is_normal(filename) { - continue; - } + if !archive_path_is_normal(filename) { + continue; + } - let path = destination.join(filename); + let path = destination.join(filename); - if entry - .dir() - .with_context(|| format!("reading zip entry metadata for path {path:?}"))? - { - std::fs::create_dir_all(&path) - .with_context(|| format!("creating directory {path:?}"))?; - } else { - let parent_dir = path - .parent() - .with_context(|| format!("no parent directory for {path:?}"))?; - std::fs::create_dir_all(parent_dir) - .with_context(|| format!("creating parent directory {parent_dir:?}"))?; - let mut file = smol::fs::File::create(&path) - .await - .with_context(|| format!("creating file {path:?}"))?; - let mut entry_reader = reader - .reader_with_entry(i) - .await - .with_context(|| format!("reading entry for path {path:?}"))?; - futures::io::copy(&mut entry_reader, &mut file) - .await - .with_context(|| format!("extracting into file {path:?}"))?; + if entry + .dir() + .with_context(|| format!("reading zip entry metadata for path {path:?}"))? + { + std::fs::create_dir_all(&path) + .with_context(|| format!("creating directory {path:?}"))?; + } else { + let parent_dir = path + .parent() + .with_context(|| format!("no parent directory for {path:?}"))?; + std::fs::create_dir_all(parent_dir) + .with_context(|| format!("creating parent directory {parent_dir:?}"))?; + let mut file = smol::fs::File::create(&path) + .await + .with_context(|| format!("creating file {path:?}"))?; + let mut entry_reader = reader + .reader_with_entry(i) + .await + .with_context(|| format!("reading entry for path {path:?}"))?; + futures::io::copy(&mut entry_reader, &mut file) + .await + .with_context(|| format!("extracting into file {path:?}"))?; - if let Some(perms) = entry.unix_permissions() - && perms != 0o000 - { - use std::os::unix::fs::PermissionsExt; - let permissions = std::fs::Permissions::from_mode(u32::from(perms)); - file.set_permissions(permissions) - .await - .with_context(|| format!("setting permissions for file {path:?}"))?; - } - } - } + if let Some(perms) = entry.unix_permissions() + && perms != 0o000 + { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(u32::from(perms)); + file.set_permissions(permissions) + .await + .with_context(|| format!("setting permissions for file {path:?}"))?; + } + } + } - Ok(()) + Ok(()) } #[cfg(test)] mod tests { - use async_zip::ZipEntryBuilder; - use async_zip::base::write::ZipFileWriter; - use futures::{AsyncSeek, AsyncWriteExt}; - use smol::io::Cursor; - use tempfile::TempDir; + use async_zip::ZipEntryBuilder; + use async_zip::base::write::ZipFileWriter; + use futures::{AsyncSeek, AsyncWriteExt}; + use smol::io::Cursor; + use tempfile::TempDir; - use super::*; + use super::*; - #[allow(unused_variables)] - async fn compress_zip(src_dir: &Path, dst: &Path, keep_file_permissions: bool) -> Result<()> { - let mut out = smol::fs::File::create(dst).await?; - let mut writer = ZipFileWriter::new(&mut out); + #[allow(unused_variables)] + async fn compress_zip(src_dir: &Path, dst: &Path, keep_file_permissions: bool) -> Result<()> { + let mut out = smol::fs::File::create(dst).await?; + let mut writer = ZipFileWriter::new(&mut out); - for entry in walkdir::WalkDir::new(src_dir) { - let entry = entry?; - let path = entry.path(); + for entry in walkdir::WalkDir::new(src_dir) { + let entry = entry?; + let path = entry.path(); - if path.is_dir() { - continue; - } + if path.is_dir() { + continue; + } - let relative_path = path.strip_prefix(src_dir)?; - let data = smol::fs::read(&path).await?; + let relative_path = path.strip_prefix(src_dir)?; + let data = smol::fs::read(&path).await?; - let filename = relative_path.display().to_string(); + let filename = relative_path.display().to_string(); - #[cfg(unix)] - { - let mut builder = - ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); - use std::os::unix::fs::PermissionsExt; - let metadata = std::fs::metadata(path)?; - let perms = keep_file_permissions.then(|| metadata.permissions().mode() as u16); - builder = builder.unix_permissions(perms.unwrap_or_default()); - writer.write_entry_whole(builder, &data).await?; - } - #[cfg(not(unix))] - { - let builder = - ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); - writer.write_entry_whole(builder, &data).await?; - } - } + #[cfg(unix)] + { + let mut builder = + ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); + use std::os::unix::fs::PermissionsExt; + let metadata = std::fs::metadata(path)?; + let perms = keep_file_permissions.then(|| metadata.permissions().mode() as u16); + builder = builder.unix_permissions(perms.unwrap_or_default()); + writer.write_entry_whole(builder, &data).await?; + } + #[cfg(not(unix))] + { + let builder = + ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); + writer.write_entry_whole(builder, &data).await?; + } + } - writer.close().await?; - out.flush().await?; - out.sync_all().await?; + writer.close().await?; + out.flush().await?; + out.sync_all().await?; - Ok(()) - } + Ok(()) + } - #[track_caller] - fn assert_file_content(path: &Path, content: &str) { - assert!(path.exists(), "file not found: {:?}", path); - let actual = std::fs::read_to_string(path).unwrap(); - assert_eq!(actual, content); - } + #[track_caller] + fn assert_file_content(path: &Path, content: &str) { + assert!(path.exists(), "file not found: {:?}", path); + let actual = std::fs::read_to_string(path).unwrap(); + assert_eq!(actual, content); + } - #[track_caller] - fn make_test_data() -> TempDir { - let dir = tempfile::tempdir().unwrap(); - let dst = dir.path(); + #[track_caller] + fn make_test_data() -> TempDir { + let dir = tempfile::tempdir().unwrap(); + let dst = dir.path(); - std::fs::write(dst.join("test"), "Hello world.").unwrap(); - std::fs::create_dir_all(dst.join("foo/bar")).unwrap(); - std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap(); - std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap(); - std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap(); + std::fs::write(dst.join("test"), "Hello world.").unwrap(); + std::fs::create_dir_all(dst.join("foo/bar")).unwrap(); + std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap(); + std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap(); + std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap(); - dir - } + dir + } - async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin { - let data = smol::fs::read(&path).await.unwrap(); - Cursor::new(data) - } + async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin { + let data = smol::fs::read(&path).await.unwrap(); + Cursor::new(data) + } - #[test] - fn test_extract_zip() { - let test_dir = make_test_data(); - let zip_file = test_dir.path().join("test.zip"); + #[test] + fn test_extract_zip() { + let test_dir = make_test_data(); + let zip_file = test_dir.path().join("test.zip"); - smol::block_on(async { - compress_zip(test_dir.path(), &zip_file, true) - .await - .unwrap(); - let reader = read_archive(&zip_file).await; + smol::block_on(async { + compress_zip(test_dir.path(), &zip_file, true) + .await + .unwrap(); + let reader = read_archive(&zip_file).await; - let dir = tempfile::tempdir().unwrap(); - let dst = dir.path(); - extract_zip(dst, reader).await.unwrap(); + let dir = tempfile::tempdir().unwrap(); + let dst = dir.path(); + extract_zip(dst, reader).await.unwrap(); - assert_file_content(&dst.join("test"), "Hello world."); - assert_file_content(&dst.join("foo/bar.txt"), "Foo bar."); - assert_file_content(&dst.join("foo/dar.md"), "Bar dar."); - assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界"); - }); - } + assert_file_content(&dst.join("test"), "Hello world."); + assert_file_content(&dst.join("foo/bar.txt"), "Foo bar."); + assert_file_content(&dst.join("foo/dar.md"), "Bar dar."); + assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界"); + }); + } - #[cfg(unix)] - #[test] - fn test_extract_zip_preserves_executable_permissions() { - use std::os::unix::fs::PermissionsExt; + #[cfg(unix)] + #[test] + fn test_extract_zip_preserves_executable_permissions() { + use std::os::unix::fs::PermissionsExt; - smol::block_on(async { - let test_dir = tempfile::tempdir().unwrap(); - let executable_path = test_dir.path().join("my_script"); + smol::block_on(async { + let test_dir = tempfile::tempdir().unwrap(); + let executable_path = test_dir.path().join("my_script"); - // Create an executable file - std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap(); - let mut perms = std::fs::metadata(&executable_path).unwrap().permissions(); - perms.set_mode(0o755); // rwxr-xr-x - std::fs::set_permissions(&executable_path, perms).unwrap(); + // Create an executable file + std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap(); + let mut perms = std::fs::metadata(&executable_path).unwrap().permissions(); + perms.set_mode(0o755); // rwxr-xr-x + std::fs::set_permissions(&executable_path, perms).unwrap(); - // Create zip - let zip_file = test_dir.path().join("test.zip"); - compress_zip(test_dir.path(), &zip_file, true) - .await - .unwrap(); + // Create zip + let zip_file = test_dir.path().join("test.zip"); + compress_zip(test_dir.path(), &zip_file, true) + .await + .unwrap(); - // Extract to new location - let extract_dir = tempfile::tempdir().unwrap(); - let reader = read_archive(&zip_file).await; - extract_zip(extract_dir.path(), reader).await.unwrap(); + // Extract to new location + let extract_dir = tempfile::tempdir().unwrap(); + let reader = read_archive(&zip_file).await; + extract_zip(extract_dir.path(), reader).await.unwrap(); - // Check permissions are preserved - let extracted_path = extract_dir.path().join("my_script"); - assert!(extracted_path.exists()); - let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions(); - assert_eq!(extracted_perms.mode() & 0o777, 0o755); - }); - } + // Check permissions are preserved + let extracted_path = extract_dir.path().join("my_script"); + assert!(extracted_path.exists()); + let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions(); + assert_eq!(extracted_perms.mode() & 0o777, 0o755); + }); + } - #[cfg(unix)] - #[test] - fn test_extract_zip_sets_default_permissions() { - use std::os::unix::fs::PermissionsExt; + #[cfg(unix)] + #[test] + fn test_extract_zip_sets_default_permissions() { + use std::os::unix::fs::PermissionsExt; - smol::block_on(async { - let test_dir = tempfile::tempdir().unwrap(); - let file_path = test_dir.path().join("my_script"); + smol::block_on(async { + let test_dir = tempfile::tempdir().unwrap(); + let file_path = test_dir.path().join("my_script"); - std::fs::write(&file_path, "#!/bin/bash\necho 'Hello'").unwrap(); - // The permissions will be shaped by the umask in the test environment - let original_perms = std::fs::metadata(&file_path).unwrap().permissions(); + std::fs::write(&file_path, "#!/bin/bash\necho 'Hello'").unwrap(); + // The permissions will be shaped by the umask in the test environment + let original_perms = std::fs::metadata(&file_path).unwrap().permissions(); - // Create zip - let zip_file = test_dir.path().join("test.zip"); - compress_zip(test_dir.path(), &zip_file, false) - .await - .unwrap(); + // Create zip + let zip_file = test_dir.path().join("test.zip"); + compress_zip(test_dir.path(), &zip_file, false) + .await + .unwrap(); - // Extract to new location - let extract_dir = tempfile::tempdir().unwrap(); - let reader = read_archive(&zip_file).await; - extract_zip(extract_dir.path(), reader).await.unwrap(); + // Extract to new location + let extract_dir = tempfile::tempdir().unwrap(); + let reader = read_archive(&zip_file).await; + extract_zip(extract_dir.path(), reader).await.unwrap(); - // Permissions were not stored, so will be whatever the umask generates - // by default for new files. This should match what we saw when we previously wrote - // the file. - let extracted_path = extract_dir.path().join("my_script"); - assert!(extracted_path.exists()); - let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions(); - assert_eq!( - extracted_perms.mode(), - original_perms.mode(), - "Expected matching Unix file mode for unzipped file without keep_file_permissions" - ); - assert_eq!( - extracted_perms, original_perms, - "Expected default set of permissions for unzipped file without keep_file_permissions" - ); - }); - } + // Permissions were not stored, so will be whatever the umask generates + // by default for new files. This should match what we saw when we previously wrote + // the file. + let extracted_path = extract_dir.path().join("my_script"); + assert!(extracted_path.exists()); + let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions(); + assert_eq!( + extracted_perms.mode(), + original_perms.mode(), + "Expected matching Unix file mode for unzipped file without keep_file_permissions" + ); + assert_eq!( + extracted_perms, original_perms, + "Expected default set of permissions for unzipped file without keep_file_permissions" + ); + }); + } - #[test] - fn test_archive_path_is_normal_rejects_traversal() { - assert!(!archive_path_is_normal("../parent.txt")); - assert!(!archive_path_is_normal("foo/../../grandparent.txt")); - assert!(!archive_path_is_normal("/tmp/absolute.txt")); + #[test] + fn test_archive_path_is_normal_rejects_traversal() { + assert!(!archive_path_is_normal("../parent.txt")); + assert!(!archive_path_is_normal("foo/../../grandparent.txt")); + assert!(!archive_path_is_normal("/tmp/absolute.txt")); - assert!(archive_path_is_normal("foo/bar.txt")); - assert!(archive_path_is_normal("foo/bar/baz.txt")); - assert!(archive_path_is_normal("./foo/bar.txt")); - assert!(archive_path_is_normal("normal.txt")); - } + assert!(archive_path_is_normal("foo/bar.txt")); + assert!(archive_path_is_normal("foo/bar/baz.txt")); + assert!(archive_path_is_normal("./foo/bar.txt")); + assert!(archive_path_is_normal("normal.txt")); + } - async fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Cursor> { - let mut buf = Cursor::new(Vec::new()); - let mut writer = ZipFileWriter::new(&mut buf); - for (name, data) in entries { - let builder = ZipEntryBuilder::new((*name).into(), async_zip::Compression::Stored); - writer.write_entry_whole(builder, data).await.unwrap(); - } - writer.close().await.unwrap(); - buf.set_position(0); - buf - } + async fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Cursor> { + let mut buf = Cursor::new(Vec::new()); + let mut writer = ZipFileWriter::new(&mut buf); + for (name, data) in entries { + let builder = ZipEntryBuilder::new((*name).into(), async_zip::Compression::Stored); + writer.write_entry_whole(builder, data).await.unwrap(); + } + writer.close().await.unwrap(); + buf.set_position(0); + buf + } - #[test] - fn test_extract_zip_skips_path_traversal_entries() { - smol::block_on(async { - let base_dir = tempfile::tempdir().unwrap(); - let extract_dir = base_dir.path().join("subdir"); - std::fs::create_dir_all(&extract_dir).unwrap(); + #[test] + fn test_extract_zip_skips_path_traversal_entries() { + smol::block_on(async { + let base_dir = tempfile::tempdir().unwrap(); + let extract_dir = base_dir.path().join("subdir"); + std::fs::create_dir_all(&extract_dir).unwrap(); - let absolute_target = base_dir.path().join("absolute.txt"); - let reader = build_zip_with_entries(&[ - ("normal.txt", b"normal file"), - ("subdir/nested.txt", b"nested file"), - ("../parent.txt", b"parent file"), - ("foo/../../grandparent.txt", b"grandparent file"), - (absolute_target.to_str().unwrap(), b"absolute file"), - ]) - .await; + let absolute_target = base_dir.path().join("absolute.txt"); + let reader = build_zip_with_entries(&[ + ("normal.txt", b"normal file"), + ("subdir/nested.txt", b"nested file"), + ("../parent.txt", b"parent file"), + ("foo/../../grandparent.txt", b"grandparent file"), + (absolute_target.to_str().unwrap(), b"absolute file"), + ]) + .await; - extract_zip(&extract_dir, reader).await.unwrap(); + extract_zip(&extract_dir, reader).await.unwrap(); - assert_file_content(&extract_dir.join("normal.txt"), "normal file"); - assert_file_content(&extract_dir.join("subdir/nested.txt"), "nested file"); + assert_file_content(&extract_dir.join("normal.txt"), "normal file"); + assert_file_content(&extract_dir.join("subdir/nested.txt"), "nested file"); - assert!( - !base_dir.path().join("parent.txt").exists(), - "parent traversal entry should have been skipped" - ); - assert!( - !base_dir.path().join("grandparent.txt").exists(), - "nested traversal entry should have been skipped" - ); - assert!( - !absolute_target.exists(), - "absolute path entry should have been skipped" - ); - }); - } + assert!( + !base_dir.path().join("parent.txt").exists(), + "parent traversal entry should have been skipped" + ); + assert!( + !base_dir.path().join("grandparent.txt").exists(), + "nested traversal entry should have been skipped" + ); + assert!( + !absolute_target.exists(), + "absolute path entry should have been skipped" + ); + }); + } } diff --git a/crates/gpui_zed_util/src/command.rs b/crates/gpui_zed_util/src/command.rs index a131d3c15b..2f2c2a3a60 100644 --- a/crates/gpui_zed_util/src/command.rs +++ b/crates/gpui_zed_util/src/command.rs @@ -12,21 +12,21 @@ pub use darwin::{Child, Command, Stdio}; const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32; pub fn new_command(program: impl AsRef) -> Command { - Command::new(program) + Command::new(program) } #[cfg(target_os = "windows")] pub fn new_std_command(program: impl AsRef) -> std::process::Command { - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt; - let mut command = std::process::Command::new(program); - command.creation_flags(CREATE_NO_WINDOW); - command + let mut command = std::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command } #[cfg(not(target_os = "windows"))] pub fn new_std_command(program: impl AsRef) -> std::process::Command { - std::process::Command::new(program) + std::process::Command::new(program) } #[cfg(not(target_os = "macos"))] @@ -41,100 +41,100 @@ pub struct Command(smol::process::Command); #[cfg(not(target_os = "macos"))] impl Command { - #[inline] - pub fn new(program: impl AsRef) -> Self { - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - let mut cmd = smol::process::Command::new(program); - cmd.creation_flags(CREATE_NO_WINDOW); - Self(cmd) - } - #[cfg(not(target_os = "windows"))] - Self(smol::process::Command::new(program)) - } + #[inline] + pub fn new(program: impl AsRef) -> Self { + #[cfg(target_os = "windows")] + { + use smol::process::windows::CommandExt; + let mut cmd = smol::process::Command::new(program); + cmd.creation_flags(CREATE_NO_WINDOW); + Self(cmd) + } + #[cfg(not(target_os = "windows"))] + Self(smol::process::Command::new(program)) + } - pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.0.arg(arg); - self - } + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.0.arg(arg); + self + } - pub fn args(&mut self, args: I) -> &mut Self - where - I: IntoIterator, - S: AsRef, - { - self.0.args(args); - self - } + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.0.args(args); + self + } - pub fn get_args(&self) -> impl Iterator { - self.0.get_args() - } + pub fn get_args(&self) -> impl Iterator { + self.0.get_args() + } - pub fn env(&mut self, key: impl AsRef, val: impl AsRef) -> &mut Self { - self.0.env(key, val); - self - } + pub fn env(&mut self, key: impl AsRef, val: impl AsRef) -> &mut Self { + self.0.env(key, val); + self + } - pub fn envs(&mut self, vars: I) -> &mut Self - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - self.0.envs(vars); - self - } + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.0.envs(vars); + self + } - pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { - self.0.env_remove(key); - self - } + pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { + self.0.env_remove(key); + self + } - pub fn env_clear(&mut self) -> &mut Self { - self.0.env_clear(); - self - } + pub fn env_clear(&mut self) -> &mut Self { + self.0.env_clear(); + self + } - pub fn current_dir(&mut self, dir: impl AsRef) -> &mut Self { - self.0.current_dir(dir); - self - } + pub fn current_dir(&mut self, dir: impl AsRef) -> &mut Self { + self.0.current_dir(dir); + self + } - pub fn stdin(&mut self, cfg: impl Into) -> &mut Self { - self.0.stdin(cfg.into()); - self - } + pub fn stdin(&mut self, cfg: impl Into) -> &mut Self { + self.0.stdin(cfg.into()); + self + } - pub fn stdout(&mut self, cfg: impl Into) -> &mut Self { - self.0.stdout(cfg.into()); - self - } + pub fn stdout(&mut self, cfg: impl Into) -> &mut Self { + self.0.stdout(cfg.into()); + self + } - pub fn stderr(&mut self, cfg: impl Into) -> &mut Self { - self.0.stderr(cfg.into()); - self - } + pub fn stderr(&mut self, cfg: impl Into) -> &mut Self { + self.0.stderr(cfg.into()); + self + } - pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { - self.0.kill_on_drop(kill_on_drop); - self - } + pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { + self.0.kill_on_drop(kill_on_drop); + self + } - pub fn spawn(&mut self) -> std::io::Result { - self.0.spawn() - } + pub fn spawn(&mut self) -> std::io::Result { + self.0.spawn() + } - pub async fn output(&mut self) -> std::io::Result { - self.0.output().await - } + pub async fn output(&mut self) -> std::io::Result { + self.0.output().await + } - pub async fn status(&mut self) -> std::io::Result { - self.0.status().await - } + pub async fn status(&mut self) -> std::io::Result { + self.0.status().await + } - pub fn get_program(&self) -> &OsStr { - self.0.get_program() - } + pub fn get_program(&self) -> &OsStr { + self.0.get_program() + } } diff --git a/crates/gpui_zed_util/src/command/darwin.rs b/crates/gpui_zed_util/src/command/darwin.rs index 1c31433a9c..fd9ee69302 100644 --- a/crates/gpui_zed_util/src/command/darwin.rs +++ b/crates/gpui_zed_util/src/command/darwin.rs @@ -1,5 +1,5 @@ use mach2::exception_types::{ - EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t, + EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t, }; use mach2::port::{MACH_PORT_NULL, mach_port_t}; use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t}; @@ -16,609 +16,609 @@ use std::ptr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Stdio { - /// A new pipe should be arranged to connect the parent and child processes. - #[default] - Piped, - /// The child inherits from the corresponding parent descriptor. - Inherit, - /// This stream will be ignored (redirected to `/dev/null`). - Null, + /// A new pipe should be arranged to connect the parent and child processes. + #[default] + Piped, + /// The child inherits from the corresponding parent descriptor. + Inherit, + /// This stream will be ignored (redirected to `/dev/null`). + Null, } impl Stdio { - pub fn piped() -> Self { - Self::Piped - } + pub fn piped() -> Self { + Self::Piped + } - pub fn inherit() -> Self { - Self::Inherit - } + pub fn inherit() -> Self { + Self::Inherit + } - pub fn null() -> Self { - Self::Null - } + pub fn null() -> Self { + Self::Null + } } unsafe extern "C" { - fn posix_spawnattr_setexceptionports_np( - attr: *mut libc::posix_spawnattr_t, - mask: exception_mask_t, - new_port: mach_port_t, - behavior: exception_behavior_t, - new_flavor: thread_state_flavor_t, - ) -> libc::c_int; + fn posix_spawnattr_setexceptionports_np( + attr: *mut libc::posix_spawnattr_t, + mask: exception_mask_t, + new_port: mach_port_t, + behavior: exception_behavior_t, + new_flavor: thread_state_flavor_t, + ) -> libc::c_int; - fn posix_spawn_file_actions_addchdir_np( - file_actions: *mut libc::posix_spawn_file_actions_t, - path: *const libc::c_char, - ) -> libc::c_int; + fn posix_spawn_file_actions_addchdir_np( + file_actions: *mut libc::posix_spawn_file_actions_t, + path: *const libc::c_char, + ) -> libc::c_int; - fn posix_spawn_file_actions_addinherit_np( - file_actions: *mut libc::posix_spawn_file_actions_t, - filedes: libc::c_int, - ) -> libc::c_int; + fn posix_spawn_file_actions_addinherit_np( + file_actions: *mut libc::posix_spawn_file_actions_t, + filedes: libc::c_int, + ) -> libc::c_int; - static environ: *const *mut libc::c_char; + static environ: *const *mut libc::c_char; } #[derive(Debug)] pub struct Command { - program: OsString, - args: Vec, - envs: BTreeMap>, - env_clear: bool, - current_dir: Option, - stdin_cfg: Option, - stdout_cfg: Option, - stderr_cfg: Option, - kill_on_drop: bool, + program: OsString, + args: Vec, + envs: BTreeMap>, + env_clear: bool, + current_dir: Option, + stdin_cfg: Option, + stdout_cfg: Option, + stderr_cfg: Option, + kill_on_drop: bool, } impl Command { - pub fn new(program: impl AsRef) -> Self { - Self { - program: program.as_ref().to_owned(), - args: Vec::new(), - envs: BTreeMap::new(), - env_clear: false, - current_dir: None, - stdin_cfg: None, - stdout_cfg: None, - stderr_cfg: None, - kill_on_drop: false, - } - } + pub fn new(program: impl AsRef) -> Self { + Self { + program: program.as_ref().to_owned(), + args: Vec::new(), + envs: BTreeMap::new(), + env_clear: false, + current_dir: None, + stdin_cfg: None, + stdout_cfg: None, + stderr_cfg: None, + kill_on_drop: false, + } + } - pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.args.push(arg.as_ref().to_owned()); - self - } + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.args.push(arg.as_ref().to_owned()); + self + } - pub fn args(&mut self, args: I) -> &mut Self - where - I: IntoIterator, - S: AsRef, - { - self.args - .extend(args.into_iter().map(|a| a.as_ref().to_owned())); - self - } + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.args + .extend(args.into_iter().map(|a| a.as_ref().to_owned())); + self + } - pub fn get_args(&self) -> impl Iterator { - self.args.iter().map(|s| s.as_os_str()) - } + pub fn get_args(&self) -> impl Iterator { + self.args.iter().map(|s| s.as_os_str()) + } - pub fn env(&mut self, key: impl AsRef, val: impl AsRef) -> &mut Self { - self.envs - .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned())); - self - } + pub fn env(&mut self, key: impl AsRef, val: impl AsRef) -> &mut Self { + self.envs + .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned())); + self + } - pub fn envs(&mut self, vars: I) -> &mut Self - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - for (key, val) in vars { - self.envs - .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned())); - } - self - } + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + for (key, val) in vars { + self.envs + .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned())); + } + self + } - pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { - let key = key.as_ref().to_owned(); - if self.env_clear { - self.envs.remove(&key); - } else { - self.envs.insert(key, None); - } - self - } + pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { + let key = key.as_ref().to_owned(); + if self.env_clear { + self.envs.remove(&key); + } else { + self.envs.insert(key, None); + } + self + } - pub fn env_clear(&mut self) -> &mut Self { - self.env_clear = true; - self.envs.clear(); - self - } + pub fn env_clear(&mut self) -> &mut Self { + self.env_clear = true; + self.envs.clear(); + self + } - pub fn current_dir(&mut self, dir: impl AsRef) -> &mut Self { - self.current_dir = Some(dir.as_ref().to_owned()); - self - } + pub fn current_dir(&mut self, dir: impl AsRef) -> &mut Self { + self.current_dir = Some(dir.as_ref().to_owned()); + self + } - pub fn stdin(&mut self, cfg: Stdio) -> &mut Self { - self.stdin_cfg = Some(cfg); - self - } + pub fn stdin(&mut self, cfg: Stdio) -> &mut Self { + self.stdin_cfg = Some(cfg); + self + } - pub fn stdout(&mut self, cfg: Stdio) -> &mut Self { - self.stdout_cfg = Some(cfg); - self - } + pub fn stdout(&mut self, cfg: Stdio) -> &mut Self { + self.stdout_cfg = Some(cfg); + self + } - pub fn stderr(&mut self, cfg: Stdio) -> &mut Self { - self.stderr_cfg = Some(cfg); - self - } + pub fn stderr(&mut self, cfg: Stdio) -> &mut Self { + self.stderr_cfg = Some(cfg); + self + } - pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { - self.kill_on_drop = kill_on_drop; - self - } + pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { + self.kill_on_drop = kill_on_drop; + self + } - pub fn spawn(&mut self) -> io::Result { - let current_dir = self - .current_dir - .as_deref() - .unwrap_or_else(|| Path::new(".")); + pub fn spawn(&mut self) -> io::Result { + let current_dir = self + .current_dir + .as_deref() + .unwrap_or_else(|| Path::new(".")); - // Optimization: if no environment modifications were requested, pass None - // to spawn_posix so it uses the `environ` global directly, avoiding a - // full copy of the environment. This matches std::process::Command behavior. - let envs = if self.env_clear || !self.envs.is_empty() { - let mut result = BTreeMap::::new(); - if !self.env_clear { - for (key, val) in std::env::vars_os() { - result.insert(key, val); - } - } - for (key, maybe_val) in &self.envs { - if let Some(val) = maybe_val { - result.insert(key.clone(), val.clone()); - } else { - result.remove(key); - } - } - Some(result.into_iter().collect::>()) - } else { - None - }; + // Optimization: if no environment modifications were requested, pass None + // to spawn_posix so it uses the `environ` global directly, avoiding a + // full copy of the environment. This matches std::process::Command behavior. + let envs = if self.env_clear || !self.envs.is_empty() { + let mut result = BTreeMap::::new(); + if !self.env_clear { + for (key, val) in std::env::vars_os() { + result.insert(key, val); + } + } + for (key, maybe_val) in &self.envs { + if let Some(val) = maybe_val { + result.insert(key.clone(), val.clone()); + } else { + result.remove(key); + } + } + Some(result.into_iter().collect::>()) + } else { + None + }; - spawn_posix_spawn(SpawnOptions { - program: &self.program, - args: &self.args, - current_dir, - envs: envs.as_deref(), - stdin_cfg: self.stdin_cfg.unwrap_or_default(), - stdout_cfg: self.stdout_cfg.unwrap_or_default(), - stderr_cfg: self.stderr_cfg.unwrap_or_default(), - kill_on_drop: self.kill_on_drop, - }) - } + spawn_posix_spawn(SpawnOptions { + program: &self.program, + args: &self.args, + current_dir, + envs: envs.as_deref(), + stdin_cfg: self.stdin_cfg.unwrap_or_default(), + stdout_cfg: self.stdout_cfg.unwrap_or_default(), + stderr_cfg: self.stderr_cfg.unwrap_or_default(), + kill_on_drop: self.kill_on_drop, + }) + } - pub async fn output(&mut self) -> io::Result { - self.stdin_cfg.get_or_insert(Stdio::null()); - self.stdout_cfg.get_or_insert(Stdio::piped()); - self.stderr_cfg.get_or_insert(Stdio::piped()); + pub async fn output(&mut self) -> io::Result { + self.stdin_cfg.get_or_insert(Stdio::null()); + self.stdout_cfg.get_or_insert(Stdio::piped()); + self.stderr_cfg.get_or_insert(Stdio::piped()); - let child = self.spawn()?; - child.output().await - } + let child = self.spawn()?; + child.output().await + } - pub async fn status(&mut self) -> io::Result { - let mut child = self.spawn()?; - child.status().await - } + pub async fn status(&mut self) -> io::Result { + let mut child = self.spawn()?; + child.status().await + } - pub fn get_program(&self) -> &OsStr { - self.program.as_os_str() - } + pub fn get_program(&self) -> &OsStr { + self.program.as_os_str() + } } #[derive(Debug)] pub struct Child { - pid: libc::pid_t, - pub stdin: Option>, - pub stdout: Option>, - pub stderr: Option>, - kill_on_drop: bool, - status: Option, + pid: libc::pid_t, + pub stdin: Option>, + pub stdout: Option>, + pub stderr: Option>, + kill_on_drop: bool, + status: Option, } impl Drop for Child { - fn drop(&mut self) { - if self.kill_on_drop && self.status.is_none() { - let _ = self.kill(); - } - } + fn drop(&mut self) { + if self.kill_on_drop && self.status.is_none() { + let _ = self.kill(); + } + } } impl Child { - pub fn id(&self) -> u32 { - self.pid as u32 - } + pub fn id(&self) -> u32 { + self.pid as u32 + } - pub fn kill(&mut self) -> io::Result<()> { - let result = unsafe { libc::kill(self.pid, libc::SIGKILL) }; - if result == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } + pub fn kill(&mut self) -> io::Result<()> { + let result = unsafe { libc::kill(self.pid, libc::SIGKILL) }; + if result == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } - pub fn try_status(&mut self) -> io::Result> { - if let Some(status) = self.status { - return Ok(Some(status)); - } + pub fn try_status(&mut self) -> io::Result> { + if let Some(status) = self.status { + return Ok(Some(status)); + } - let mut status: libc::c_int = 0; - let result = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) }; + let mut status: libc::c_int = 0; + let result = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) }; - if result == -1 { - Err(io::Error::last_os_error()) - } else if result == 0 { - Ok(None) - } else { - let exit_status = ExitStatus::from_raw(status); - self.status = Some(exit_status); - Ok(Some(exit_status)) - } - } + if result == -1 { + Err(io::Error::last_os_error()) + } else if result == 0 { + Ok(None) + } else { + let exit_status = ExitStatus::from_raw(status); + self.status = Some(exit_status); + Ok(Some(exit_status)) + } + } - pub fn status( - &mut self, - ) -> impl std::future::Future> + Send + 'static { - self.stdin.take(); + pub fn status( + &mut self, + ) -> impl std::future::Future> + Send + 'static { + self.stdin.take(); - let pid = self.pid; - let cached_status = self.status; + let pid = self.pid; + let cached_status = self.status; - async move { - if let Some(status) = cached_status { - return Ok(status); - } + async move { + if let Some(status) = cached_status { + return Ok(status); + } - smol::unblock(move || { - let mut status: libc::c_int = 0; - let result = unsafe { libc::waitpid(pid, &mut status, 0) }; - if result == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(ExitStatus::from_raw(status)) - } - }) - .await - } - } + smol::unblock(move || { + let mut status: libc::c_int = 0; + let result = unsafe { libc::waitpid(pid, &mut status, 0) }; + if result == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(ExitStatus::from_raw(status)) + } + }) + .await + } + } - pub async fn output(mut self) -> io::Result { - use futures_lite::AsyncReadExt; + pub async fn output(mut self) -> io::Result { + use futures_lite::AsyncReadExt; - let status = self.status(); + let status = self.status(); - let stdout = self.stdout.take(); - let stdout_future = async move { - let mut data = Vec::new(); - if let Some(mut stdout) = stdout { - stdout.read_to_end(&mut data).await?; - } - io::Result::Ok(data) - }; + let stdout = self.stdout.take(); + let stdout_future = async move { + let mut data = Vec::new(); + if let Some(mut stdout) = stdout { + stdout.read_to_end(&mut data).await?; + } + io::Result::Ok(data) + }; - let stderr = self.stderr.take(); - let stderr_future = async move { - let mut data = Vec::new(); - if let Some(mut stderr) = stderr { - stderr.read_to_end(&mut data).await?; - } - io::Result::Ok(data) - }; + let stderr = self.stderr.take(); + let stderr_future = async move { + let mut data = Vec::new(); + if let Some(mut stderr) = stderr { + stderr.read_to_end(&mut data).await?; + } + io::Result::Ok(data) + }; - let (stdout_data, stderr_data) = - futures_lite::future::try_zip(stdout_future, stderr_future).await?; - let status = status.await?; + let (stdout_data, stderr_data) = + futures_lite::future::try_zip(stdout_future, stderr_future).await?; + let status = status.await?; - Ok(Output { - status, - stdout: stdout_data, - stderr: stderr_data, - }) - } + Ok(Output { + status, + stdout: stdout_data, + stderr: stderr_data, + }) + } } struct SpawnOptions<'a> { - program: &'a OsStr, - args: &'a [OsString], - current_dir: &'a Path, - envs: Option<&'a [(OsString, OsString)]>, - stdin_cfg: Stdio, - stdout_cfg: Stdio, - stderr_cfg: Stdio, - kill_on_drop: bool, + program: &'a OsStr, + args: &'a [OsString], + current_dir: &'a Path, + envs: Option<&'a [(OsString, OsString)]>, + stdin_cfg: Stdio, + stdout_cfg: Stdio, + stderr_cfg: Stdio, + kill_on_drop: bool, } fn spawn_posix_spawn(options: SpawnOptions<'_>) -> io::Result { - let SpawnOptions { - program, - args, - current_dir, - envs, - stdin_cfg, - stdout_cfg, - stderr_cfg, - kill_on_drop, - } = options; - let program_cstr = CString::new(program.as_bytes()).map_err(|_| invalid_input_error())?; + let SpawnOptions { + program, + args, + current_dir, + envs, + stdin_cfg, + stdout_cfg, + stderr_cfg, + kill_on_drop, + } = options; + let program_cstr = CString::new(program.as_bytes()).map_err(|_| invalid_input_error())?; - let current_dir_cstr = - CString::new(current_dir.as_os_str().as_bytes()).map_err(|_| invalid_input_error())?; + let current_dir_cstr = + CString::new(current_dir.as_os_str().as_bytes()).map_err(|_| invalid_input_error())?; - let mut argv_cstrs = vec![program_cstr.clone()]; - for arg in args { - let cstr = CString::new(arg.as_bytes()).map_err(|_| invalid_input_error())?; - argv_cstrs.push(cstr); - } - let mut argv_ptrs: Vec<*mut libc::c_char> = argv_cstrs - .iter() - .map(|s| s.as_ptr() as *mut libc::c_char) - .collect(); - argv_ptrs.push(ptr::null_mut()); + let mut argv_cstrs = vec![program_cstr.clone()]; + for arg in args { + let cstr = CString::new(arg.as_bytes()).map_err(|_| invalid_input_error())?; + argv_cstrs.push(cstr); + } + let mut argv_ptrs: Vec<*mut libc::c_char> = argv_cstrs + .iter() + .map(|s| s.as_ptr() as *mut libc::c_char) + .collect(); + argv_ptrs.push(ptr::null_mut()); - let envp: Vec = if let Some(envs) = envs { - envs.iter() - .map(|(key, value)| { - let mut env_str = key.as_bytes().to_vec(); - env_str.push(b'='); - env_str.extend_from_slice(value.as_bytes()); - CString::new(env_str) - }) - .collect::, _>>() - .map_err(|_| invalid_input_error())? - } else { - Vec::new() - }; - let mut envp_ptrs: Vec<*mut libc::c_char> = envp - .iter() - .map(|s| s.as_ptr() as *mut libc::c_char) - .collect(); - envp_ptrs.push(ptr::null_mut()); + let envp: Vec = if let Some(envs) = envs { + envs.iter() + .map(|(key, value)| { + let mut env_str = key.as_bytes().to_vec(); + env_str.push(b'='); + env_str.extend_from_slice(value.as_bytes()); + CString::new(env_str) + }) + .collect::, _>>() + .map_err(|_| invalid_input_error())? + } else { + Vec::new() + }; + let mut envp_ptrs: Vec<*mut libc::c_char> = envp + .iter() + .map(|s| s.as_ptr() as *mut libc::c_char) + .collect(); + envp_ptrs.push(ptr::null_mut()); - let (stdin_read, stdin_write) = match stdin_cfg { - Stdio::Piped => { - let (r, w) = create_pipe()?; - (Some(r), Some(w)) - } - Stdio::Null => { - let fd = open_dev_null(libc::O_RDONLY)?; - (Some(fd), None) - } - Stdio::Inherit => (None, None), - }; + let (stdin_read, stdin_write) = match stdin_cfg { + Stdio::Piped => { + let (r, w) = create_pipe()?; + (Some(r), Some(w)) + } + Stdio::Null => { + let fd = open_dev_null(libc::O_RDONLY)?; + (Some(fd), None) + } + Stdio::Inherit => (None, None), + }; - let (stdout_read, stdout_write) = match stdout_cfg { - Stdio::Piped => { - let (r, w) = create_pipe()?; - (Some(r), Some(w)) - } - Stdio::Null => { - let fd = open_dev_null(libc::O_WRONLY)?; - (None, Some(fd)) - } - Stdio::Inherit => (None, None), - }; + let (stdout_read, stdout_write) = match stdout_cfg { + Stdio::Piped => { + let (r, w) = create_pipe()?; + (Some(r), Some(w)) + } + Stdio::Null => { + let fd = open_dev_null(libc::O_WRONLY)?; + (None, Some(fd)) + } + Stdio::Inherit => (None, None), + }; - let (stderr_read, stderr_write) = match stderr_cfg { - Stdio::Piped => { - let (r, w) = create_pipe()?; - (Some(r), Some(w)) - } - Stdio::Null => { - let fd = open_dev_null(libc::O_WRONLY)?; - (None, Some(fd)) - } - Stdio::Inherit => (None, None), - }; + let (stderr_read, stderr_write) = match stderr_cfg { + Stdio::Piped => { + let (r, w) = create_pipe()?; + (Some(r), Some(w)) + } + Stdio::Null => { + let fd = open_dev_null(libc::O_WRONLY)?; + (None, Some(fd)) + } + Stdio::Inherit => (None, None), + }; - let mut attr: libc::posix_spawnattr_t = ptr::null_mut(); - let mut file_actions: libc::posix_spawn_file_actions_t = ptr::null_mut(); + let mut attr: libc::posix_spawnattr_t = ptr::null_mut(); + let mut file_actions: libc::posix_spawn_file_actions_t = ptr::null_mut(); - unsafe { - cvt_nz(libc::posix_spawnattr_init(&mut attr))?; - cvt_nz(libc::posix_spawn_file_actions_init(&mut file_actions))?; + unsafe { + cvt_nz(libc::posix_spawnattr_init(&mut attr))?; + cvt_nz(libc::posix_spawn_file_actions_init(&mut file_actions))?; - cvt_nz(libc::posix_spawnattr_setflags( - &mut attr, - libc::POSIX_SPAWN_CLOEXEC_DEFAULT as libc::c_short, - ))?; + cvt_nz(libc::posix_spawnattr_setflags( + &mut attr, + libc::POSIX_SPAWN_CLOEXEC_DEFAULT as libc::c_short, + ))?; - cvt_nz(posix_spawnattr_setexceptionports_np( - &mut attr, - EXC_MASK_ALL, - MACH_PORT_NULL, - EXCEPTION_DEFAULT as exception_behavior_t, - THREAD_STATE_NONE, - ))?; + cvt_nz(posix_spawnattr_setexceptionports_np( + &mut attr, + EXC_MASK_ALL, + MACH_PORT_NULL, + EXCEPTION_DEFAULT as exception_behavior_t, + THREAD_STATE_NONE, + ))?; - cvt_nz(posix_spawn_file_actions_addchdir_np( - &mut file_actions, - current_dir_cstr.as_ptr(), - ))?; + cvt_nz(posix_spawn_file_actions_addchdir_np( + &mut file_actions, + current_dir_cstr.as_ptr(), + ))?; - if let Some(fd) = stdin_read { - cvt_nz(libc::posix_spawn_file_actions_adddup2( - &mut file_actions, - fd, - libc::STDIN_FILENO, - ))?; - cvt_nz(posix_spawn_file_actions_addinherit_np( - &mut file_actions, - libc::STDIN_FILENO, - ))?; - } + if let Some(fd) = stdin_read { + cvt_nz(libc::posix_spawn_file_actions_adddup2( + &mut file_actions, + fd, + libc::STDIN_FILENO, + ))?; + cvt_nz(posix_spawn_file_actions_addinherit_np( + &mut file_actions, + libc::STDIN_FILENO, + ))?; + } - if let Some(fd) = stdout_write { - cvt_nz(libc::posix_spawn_file_actions_adddup2( - &mut file_actions, - fd, - libc::STDOUT_FILENO, - ))?; - cvt_nz(posix_spawn_file_actions_addinherit_np( - &mut file_actions, - libc::STDOUT_FILENO, - ))?; - } + if let Some(fd) = stdout_write { + cvt_nz(libc::posix_spawn_file_actions_adddup2( + &mut file_actions, + fd, + libc::STDOUT_FILENO, + ))?; + cvt_nz(posix_spawn_file_actions_addinherit_np( + &mut file_actions, + libc::STDOUT_FILENO, + ))?; + } - if let Some(fd) = stderr_write { - cvt_nz(libc::posix_spawn_file_actions_adddup2( - &mut file_actions, - fd, - libc::STDERR_FILENO, - ))?; - cvt_nz(posix_spawn_file_actions_addinherit_np( - &mut file_actions, - libc::STDERR_FILENO, - ))?; - } + if let Some(fd) = stderr_write { + cvt_nz(libc::posix_spawn_file_actions_adddup2( + &mut file_actions, + fd, + libc::STDERR_FILENO, + ))?; + cvt_nz(posix_spawn_file_actions_addinherit_np( + &mut file_actions, + libc::STDERR_FILENO, + ))?; + } - let mut pid: libc::pid_t = 0; + let mut pid: libc::pid_t = 0; - let spawn_result = libc::posix_spawnp( - &mut pid, - program_cstr.as_ptr(), - &file_actions, - &attr, - argv_ptrs.as_ptr(), - if envs.is_some() { - envp_ptrs.as_ptr() - } else { - environ - }, - ); + let spawn_result = libc::posix_spawnp( + &mut pid, + program_cstr.as_ptr(), + &file_actions, + &attr, + argv_ptrs.as_ptr(), + if envs.is_some() { + envp_ptrs.as_ptr() + } else { + environ + }, + ); - libc::posix_spawnattr_destroy(&mut attr); - libc::posix_spawn_file_actions_destroy(&mut file_actions); + libc::posix_spawnattr_destroy(&mut attr); + libc::posix_spawn_file_actions_destroy(&mut file_actions); - if let Some(fd) = stdin_read { - libc::close(fd); - } - if let Some(fd) = stdout_write { - libc::close(fd); - } - if let Some(fd) = stderr_write { - libc::close(fd); - } + if let Some(fd) = stdin_read { + libc::close(fd); + } + if let Some(fd) = stdout_write { + libc::close(fd); + } + if let Some(fd) = stderr_write { + libc::close(fd); + } - cvt_nz(spawn_result)?; + cvt_nz(spawn_result)?; - Ok(Child { - pid, - stdin: stdin_write.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), - stdout: stdout_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), - stderr: stderr_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), - kill_on_drop, - status: None, - }) - } + Ok(Child { + pid, + stdin: stdin_write.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), + stdout: stdout_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), + stderr: stderr_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))), + kill_on_drop, + status: None, + }) + } } fn create_pipe() -> io::Result<(libc::c_int, libc::c_int)> { - let mut fds: [libc::c_int; 2] = [0; 2]; - unsafe { - let result = libc::pipe(fds.as_mut_ptr()); - if result == -1 { - let error = io::Error::last_os_error(); - return Err(error); - } + let mut fds: [libc::c_int; 2] = [0; 2]; + unsafe { + let result = libc::pipe(fds.as_mut_ptr()); + if result == -1 { + let error = io::Error::last_os_error(); + return Err(error); + } - // Set close-on-exec on both ends of the pipe. - // - // Without this, unrelated spawns elsewhere in the process (e.g. - // `smol::process` or `async_process`, which on Apple platforms use - // `posix_spawn` *without* `POSIX_SPAWN_CLOEXEC_DEFAULT`) would inherit - // these file descriptors and keep the pipes open even after we drop our - // side. - for &fd in &fds { - let result = libc::ioctl(fd, libc::FIOCLEX); - if result == -1 { - let error = io::Error::last_os_error(); - libc::close(fds[0]); - libc::close(fds[1]); - return Err(error); - } - } + // Set close-on-exec on both ends of the pipe. + // + // Without this, unrelated spawns elsewhere in the process (e.g. + // `smol::process` or `async_process`, which on Apple platforms use + // `posix_spawn` *without* `POSIX_SPAWN_CLOEXEC_DEFAULT`) would inherit + // these file descriptors and keep the pipes open even after we drop our + // side. + for &fd in &fds { + let result = libc::ioctl(fd, libc::FIOCLEX); + if result == -1 { + let error = io::Error::last_os_error(); + libc::close(fds[0]); + libc::close(fds[1]); + return Err(error); + } + } - Ok((fds[0], fds[1])) - } + Ok((fds[0], fds[1])) + } } fn open_dev_null(flags: libc::c_int) -> io::Result { - // Set close-on-exec for this pipe, for the same reason as in `create_pipe`. - let fd = unsafe { - libc::open( - c"/dev/null".as_ptr() as *const libc::c_char, - flags | libc::O_CLOEXEC, - ) - }; - if fd == -1 { - return Err(io::Error::last_os_error()); - } - Ok(fd) + // Set close-on-exec for this pipe, for the same reason as in `create_pipe`. + let fd = unsafe { + libc::open( + c"/dev/null".as_ptr() as *const libc::c_char, + flags | libc::O_CLOEXEC, + ) + }; + if fd == -1 { + return Err(io::Error::last_os_error()); + } + Ok(fd) } /// Zero means `Ok()`, all other values are treated as raw OS errors. Does not look at `errno`. /// Mirrored after Rust's std `cvt_nz` function. fn cvt_nz(error: libc::c_int) -> io::Result<()> { - if error == 0 { - Ok(()) - } else { - Err(io::Error::from_raw_os_error(error)) - } + if error == 0 { + Ok(()) + } else { + Err(io::Error::from_raw_os_error(error)) + } } fn invalid_input_error() -> io::Error { - io::Error::new( - io::ErrorKind::InvalidInput, - "invalid argument: path or argument contains null byte", - ) + io::Error::new( + io::ErrorKind::InvalidInput, + "invalid argument: path or argument contains null byte", + ) } #[cfg(test)] mod tests { - use super::*; - use futures_lite::AsyncWriteExt; + use super::*; + use futures_lite::AsyncWriteExt; - // Verifies that pipes returned by `create_pipe` aren't visible to unrelated - // child processes spawned via `std::process::Command`. On macOS, `std` - // uses `posix_spawn` without `POSIX_SPAWN_CLOEXEC_DEFAULT`, so any - // non-CLOEXEC fd in the parent leaks into the child. Without - // `FD_CLOEXEC` on our pipe fds, an unrelated spawn (a terminal, the crash - // handler, etc.) running concurrently with a piped git child would hold - // git's stdin write end open and deadlock the git child on `read()`. - #[test] - fn test_create_pipe_not_inherited_by_unrelated_spawn() { - let (read_fd, write_fd) = create_pipe().expect("create_pipe failed"); + // Verifies that pipes returned by `create_pipe` aren't visible to unrelated + // child processes spawned via `std::process::Command`. On macOS, `std` + // uses `posix_spawn` without `POSIX_SPAWN_CLOEXEC_DEFAULT`, so any + // non-CLOEXEC fd in the parent leaks into the child. Without + // `FD_CLOEXEC` on our pipe fds, an unrelated spawn (a terminal, the crash + // handler, etc.) running concurrently with a piped git child would hold + // git's stdin write end open and deadlock the git child on `read()`. + #[test] + fn test_create_pipe_not_inherited_by_unrelated_spawn() { + let (read_fd, write_fd) = create_pipe().expect("create_pipe failed"); - // Probe with the exact fds returned by `create_pipe` (no dup), since - // duping with `F_DUPFD` would lose CLOEXEC and `F_DUPFD_CLOEXEC` would - // unconditionally set it, either of which would defeat the test. - #[allow(clippy::disallowed_methods)] - let output = std::process::Command::new("/bin/sh") - .arg("-c") - .arg(format!( - "for fd in {read_fd} {write_fd}; do \ + // Probe with the exact fds returned by `create_pipe` (no dup), since + // duping with `F_DUPFD` would lose CLOEXEC and `F_DUPFD_CLOEXEC` would + // unconditionally set it, either of which would defeat the test. + #[allow(clippy::disallowed_methods)] + let output = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(format!( + "for fd in {read_fd} {write_fd}; do \ if [ -e /dev/fd/$fd ]; then \ echo $fd WAS INHERITED; \ else \ @@ -626,290 +626,290 @@ mod tests { fi; \ done; \ echo DONE" - )) - .output() - .expect("failed to spawn sh"); + )) + .output() + .expect("failed to spawn sh"); - let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - unsafe { - libc::close(read_fd); - libc::close(write_fd); - } + unsafe { + libc::close(read_fd); + libc::close(write_fd); + } - assert_eq!( - stdout, - format!("{read_fd} WAS NOT INHERITED\n{write_fd} WAS NOT INHERITED\nDONE\n") - ); - } + assert_eq!( + stdout, + format!("{read_fd} WAS NOT INHERITED\n{write_fd} WAS NOT INHERITED\nDONE\n") + ); + } - #[test] - fn test_spawn_echo() { - smol::block_on(async { - let output = Command::new("/bin/echo") - .args(["-n", "hello world"]) - .output() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_echo() { + smol::block_on(async { + let output = Command::new("/bin/echo") + .args(["-n", "hello world"]) + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(output.stdout, b"hello world"); - }); - } + assert!(output.status.success()); + assert_eq!(output.stdout, b"hello world"); + }); + } - #[test] - fn test_spawn_cat_stdin() { - smol::block_on(async { - let mut child = Command::new("/bin/cat") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn"); + #[test] + fn test_spawn_cat_stdin() { + smol::block_on(async { + let mut child = Command::new("/bin/cat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to spawn"); - if let Some(ref mut stdin) = child.stdin { - stdin - .write_all(b"hello from stdin") - .await - .expect("failed to write"); - stdin.close().await.expect("failed to close"); - } - drop(child.stdin.take()); + if let Some(ref mut stdin) = child.stdin { + stdin + .write_all(b"hello from stdin") + .await + .expect("failed to write"); + stdin.close().await.expect("failed to close"); + } + drop(child.stdin.take()); - let output = child.output().await.expect("failed to get output"); - assert!(output.status.success()); - assert_eq!(output.stdout, b"hello from stdin"); - }); - } + let output = child.output().await.expect("failed to get output"); + assert!(output.status.success()); + assert_eq!(output.stdout, b"hello from stdin"); + }); + } - #[test] - fn test_spawn_stderr() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "echo error >&2"]) - .output() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_stderr() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "echo error >&2"]) + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(output.stderr, b"error\n"); - }); - } + assert!(output.status.success()); + assert_eq!(output.stderr, b"error\n"); + }); + } - #[test] - fn test_spawn_exit_code() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "exit 42"]) - .output() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_exit_code() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "exit 42"]) + .output() + .await + .expect("failed to run command"); - assert!(!output.status.success()); - assert_eq!(output.status.code(), Some(42)); - }); - } + assert!(!output.status.success()); + assert_eq!(output.status.code(), Some(42)); + }); + } - #[test] - fn test_spawn_current_dir() { - smol::block_on(async { - let output = Command::new("/bin/pwd") - .current_dir("/tmp") - .output() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_current_dir() { + smol::block_on(async { + let output = Command::new("/bin/pwd") + .current_dir("/tmp") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - let pwd = String::from_utf8_lossy(&output.stdout); - assert!(pwd.trim() == "/tmp" || pwd.trim() == "/private/tmp"); - }); - } + assert!(output.status.success()); + let pwd = String::from_utf8_lossy(&output.stdout); + assert!(pwd.trim() == "/tmp" || pwd.trim() == "/private/tmp"); + }); + } - #[test] - fn test_spawn_env() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "echo $MY_TEST_VAR"]) - .env("MY_TEST_VAR", "test_value") - .output() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_env() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "echo $MY_TEST_VAR"]) + .env("MY_TEST_VAR", "test_value") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value"); - }); - } + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value"); + }); + } - #[test] - fn test_spawn_status() { - smol::block_on(async { - let status = Command::new("/usr/bin/true") - .status() - .await - .expect("failed to run command"); + #[test] + fn test_spawn_status() { + smol::block_on(async { + let status = Command::new("/usr/bin/true") + .status() + .await + .expect("failed to run command"); - assert!(status.success()); + assert!(status.success()); - let status = Command::new("/usr/bin/false") - .status() - .await - .expect("failed to run command"); + let status = Command::new("/usr/bin/false") + .status() + .await + .expect("failed to run command"); - assert!(!status.success()); - }); - } + assert!(!status.success()); + }); + } - #[test] - fn test_env_remove_removes_set_env() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "echo ${MY_VAR:-unset}"]) - .env("MY_VAR", "set_value") - .env_remove("MY_VAR") - .output() - .await - .expect("failed to run command"); + #[test] + fn test_env_remove_removes_set_env() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "echo ${MY_VAR:-unset}"]) + .env("MY_VAR", "set_value") + .env_remove("MY_VAR") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); - }); - } + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); + }); + } - #[test] - fn test_env_remove_removes_inherited_env() { - smol::block_on(async { - // SAFETY: This test is single-threaded and we clean up the var at the end - unsafe { std::env::set_var("TEST_INHERITED_VAR", "inherited_value") }; + #[test] + fn test_env_remove_removes_inherited_env() { + smol::block_on(async { + // SAFETY: This test is single-threaded and we clean up the var at the end + unsafe { std::env::set_var("TEST_INHERITED_VAR", "inherited_value") }; - let output = Command::new("/bin/sh") - .args(["-c", "echo ${TEST_INHERITED_VAR:-unset}"]) - .env_remove("TEST_INHERITED_VAR") - .output() - .await - .expect("failed to run command"); + let output = Command::new("/bin/sh") + .args(["-c", "echo ${TEST_INHERITED_VAR:-unset}"]) + .env_remove("TEST_INHERITED_VAR") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); - // SAFETY: Cleaning up test env var - unsafe { std::env::remove_var("TEST_INHERITED_VAR") }; - }); - } + // SAFETY: Cleaning up test env var + unsafe { std::env::remove_var("TEST_INHERITED_VAR") }; + }); + } - #[test] - fn test_env_after_env_remove() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "echo ${MY_VAR:-unset}"]) - .env_remove("MY_VAR") - .env("MY_VAR", "new_value") - .output() - .await - .expect("failed to run command"); + #[test] + fn test_env_after_env_remove() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "echo ${MY_VAR:-unset}"]) + .env_remove("MY_VAR") + .env("MY_VAR", "new_value") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "new_value"); - }); - } + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "new_value"); + }); + } - #[test] - fn test_env_remove_after_env_clear() { - smol::block_on(async { - let output = Command::new("/bin/sh") - .args(["-c", "echo ${MY_VAR:-unset}"]) - .env_clear() - .env("MY_VAR", "set_value") - .env_remove("MY_VAR") - .output() - .await - .expect("failed to run command"); + #[test] + fn test_env_remove_after_env_clear() { + smol::block_on(async { + let output = Command::new("/bin/sh") + .args(["-c", "echo ${MY_VAR:-unset}"]) + .env_clear() + .env("MY_VAR", "set_value") + .env_remove("MY_VAR") + .output() + .await + .expect("failed to run command"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); - }); - } + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset"); + }); + } - #[test] - fn test_stdio_null_stdin() { - smol::block_on(async { - let child = Command::new("/bin/cat") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn"); + #[test] + fn test_stdio_null_stdin() { + smol::block_on(async { + let child = Command::new("/bin/cat") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to spawn"); - let output = child.output().await.expect("failed to get output"); - assert!(output.status.success()); - assert!( - output.stdout.is_empty(), - "stdin from /dev/null should produce no output from cat" - ); - }); - } + let output = child.output().await.expect("failed to get output"); + assert!(output.status.success()); + assert!( + output.stdout.is_empty(), + "stdin from /dev/null should produce no output from cat" + ); + }); + } - #[test] - fn test_stdio_null_stdout() { - smol::block_on(async { - let mut child = Command::new("/bin/echo") - .args(["hello"]) - .stdout(Stdio::null()) - .spawn() - .expect("failed to spawn"); + #[test] + fn test_stdio_null_stdout() { + smol::block_on(async { + let mut child = Command::new("/bin/echo") + .args(["hello"]) + .stdout(Stdio::null()) + .spawn() + .expect("failed to spawn"); - assert!( - child.stdout.is_none(), - "stdout should be None when Stdio::null() is used" - ); + assert!( + child.stdout.is_none(), + "stdout should be None when Stdio::null() is used" + ); - let status = child.status().await.expect("failed to get status"); - assert!(status.success()); - }); - } + let status = child.status().await.expect("failed to get status"); + assert!(status.success()); + }); + } - #[test] - fn test_stdio_null_stderr() { - smol::block_on(async { - let mut child = Command::new("/bin/sh") - .args(["-c", "echo error >&2"]) - .stderr(Stdio::null()) - .spawn() - .expect("failed to spawn"); + #[test] + fn test_stdio_null_stderr() { + smol::block_on(async { + let mut child = Command::new("/bin/sh") + .args(["-c", "echo error >&2"]) + .stderr(Stdio::null()) + .spawn() + .expect("failed to spawn"); - assert!( - child.stderr.is_none(), - "stderr should be None when Stdio::null() is used" - ); + assert!( + child.stderr.is_none(), + "stderr should be None when Stdio::null() is used" + ); - let status = child.status().await.expect("failed to get status"); - assert!(status.success()); - }); - } + let status = child.status().await.expect("failed to get status"); + assert!(status.success()); + }); + } - #[test] - fn test_stdio_piped_stdin() { - smol::block_on(async { - let mut child = Command::new("/bin/cat") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn"); + #[test] + fn test_stdio_piped_stdin() { + smol::block_on(async { + let mut child = Command::new("/bin/cat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to spawn"); - assert!( - child.stdin.is_some(), - "stdin should be Some when Stdio::piped() is used" - ); + assert!( + child.stdin.is_some(), + "stdin should be Some when Stdio::piped() is used" + ); - if let Some(ref mut stdin) = child.stdin { - stdin - .write_all(b"piped input") - .await - .expect("failed to write"); - stdin.close().await.expect("failed to close"); - } - drop(child.stdin.take()); + if let Some(ref mut stdin) = child.stdin { + stdin + .write_all(b"piped input") + .await + .expect("failed to write"); + stdin.close().await.expect("failed to close"); + } + drop(child.stdin.take()); - let output = child.output().await.expect("failed to get output"); - assert!(output.status.success()); - assert_eq!(output.stdout, b"piped input"); - }); - } + let output = child.output().await.expect("failed to get output"); + assert!(output.status.success()); + assert_eq!(output.stdout, b"piped input"); + }); + } } diff --git a/crates/gpui_zed_util/src/disambiguate.rs b/crates/gpui_zed_util/src/disambiguate.rs index 8a64af4496..c88e6fe101 100644 --- a/crates/gpui_zed_util/src/disambiguate.rs +++ b/crates/gpui_zed_util/src/disambiguate.rs @@ -12,191 +12,191 @@ use std::hash::Hash; /// an item reaches its fixed point, it is assumed it will no longer change and /// will no longer be checked for collisions. pub fn compute_disambiguation_details( - items: &[T], - get_description: impl Fn(&T, usize) -> D, + items: &[T], + get_description: impl Fn(&T, usize) -> D, ) -> Vec where - D: Eq + Hash + Clone, + D: Eq + Hash + Clone, { - let mut details = vec![0usize; items.len()]; - let mut descriptions: HashMap> = HashMap::default(); - let mut current_descriptions: Vec = - items.iter().map(|item| get_description(item, 0)).collect(); + let mut details = vec![0usize; items.len()]; + let mut descriptions: HashMap> = HashMap::default(); + let mut current_descriptions: Vec = + items.iter().map(|item| get_description(item, 0)).collect(); - loop { - let mut any_collisions = false; + loop { + let mut any_collisions = false; - for (index, (item, &detail)) in items.iter().zip(&details).enumerate() { - if detail > 0 { - let new_description = get_description(item, detail); - if new_description == current_descriptions[index] { - continue; - } - current_descriptions[index] = new_description; - } - descriptions - .entry(current_descriptions[index].clone()) - .or_default() - .push(index); - } + for (index, (item, &detail)) in items.iter().zip(&details).enumerate() { + if detail > 0 { + let new_description = get_description(item, detail); + if new_description == current_descriptions[index] { + continue; + } + current_descriptions[index] = new_description; + } + descriptions + .entry(current_descriptions[index].clone()) + .or_default() + .push(index); + } - for (_, indices) in descriptions.drain() { - if indices.len() > 1 { - any_collisions = true; - for index in indices { - details[index] += 1; - } - } - } + for (_, indices) in descriptions.drain() { + if indices.len() > 1 { + any_collisions = true; + for index in indices { + details[index] += 1; + } + } + } - if !any_collisions { - break; - } - } + if !any_collisions { + break; + } + } - details + details } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_no_conflicts() { - let items = vec!["alpha", "beta", "gamma"]; - let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); - assert_eq!(details, vec![0, 0, 0]); - } + #[test] + fn test_no_conflicts() { + let items = vec!["alpha", "beta", "gamma"]; + let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); + assert_eq!(details, vec![0, 0, 0]); + } - #[test] - fn test_simple_two_way_conflict() { - // Two items with the same base name but different parents. - let items = vec![("src/foo.rs", "foo.rs"), ("lib/foo.rs", "foo.rs")]; - let details = compute_disambiguation_details(&items, |item, detail| match detail { - 0 => item.1.to_string(), - _ => item.0.to_string(), - }); - assert_eq!(details, vec![1, 1]); - } + #[test] + fn test_simple_two_way_conflict() { + // Two items with the same base name but different parents. + let items = vec![("src/foo.rs", "foo.rs"), ("lib/foo.rs", "foo.rs")]; + let details = compute_disambiguation_details(&items, |item, detail| match detail { + 0 => item.1.to_string(), + _ => item.0.to_string(), + }); + assert_eq!(details, vec![1, 1]); + } - #[test] - fn test_three_way_conflict() { - let items = vec![ - ("foo.rs", "a/foo.rs"), - ("foo.rs", "b/foo.rs"), - ("foo.rs", "c/foo.rs"), - ]; - let details = compute_disambiguation_details(&items, |item, detail| match detail { - 0 => item.0.to_string(), - _ => item.1.to_string(), - }); - assert_eq!(details, vec![1, 1, 1]); - } + #[test] + fn test_three_way_conflict() { + let items = vec![ + ("foo.rs", "a/foo.rs"), + ("foo.rs", "b/foo.rs"), + ("foo.rs", "c/foo.rs"), + ]; + let details = compute_disambiguation_details(&items, |item, detail| match detail { + 0 => item.0.to_string(), + _ => item.1.to_string(), + }); + assert_eq!(details, vec![1, 1, 1]); + } - #[test] - fn test_deeper_conflict() { - // At detail 0, all three show "file.rs". - // At detail 1, items 0 and 1 both show "src/file.rs", item 2 shows "lib/file.rs". - // At detail 2, item 0 shows "a/src/file.rs", item 1 shows "b/src/file.rs". - let items = vec![ - vec!["file.rs", "src/file.rs", "a/src/file.rs"], - vec!["file.rs", "src/file.rs", "b/src/file.rs"], - vec!["file.rs", "lib/file.rs", "x/lib/file.rs"], - ]; - let details = compute_disambiguation_details(&items, |item, detail| { - let clamped = detail.min(item.len() - 1); - item[clamped].to_string() - }); - assert_eq!(details, vec![2, 2, 1]); - } + #[test] + fn test_deeper_conflict() { + // At detail 0, all three show "file.rs". + // At detail 1, items 0 and 1 both show "src/file.rs", item 2 shows "lib/file.rs". + // At detail 2, item 0 shows "a/src/file.rs", item 1 shows "b/src/file.rs". + let items = vec![ + vec!["file.rs", "src/file.rs", "a/src/file.rs"], + vec!["file.rs", "src/file.rs", "b/src/file.rs"], + vec!["file.rs", "lib/file.rs", "x/lib/file.rs"], + ]; + let details = compute_disambiguation_details(&items, |item, detail| { + let clamped = detail.min(item.len() - 1); + item[clamped].to_string() + }); + assert_eq!(details, vec![2, 2, 1]); + } - #[test] - fn test_mixed_conflicting_and_unique() { - let items = vec![ - ("src/foo.rs", "foo.rs"), - ("lib/foo.rs", "foo.rs"), - ("src/bar.rs", "bar.rs"), - ]; - let details = compute_disambiguation_details(&items, |item, detail| match detail { - 0 => item.1.to_string(), - _ => item.0.to_string(), - }); - assert_eq!(details, vec![1, 1, 0]); - } + #[test] + fn test_mixed_conflicting_and_unique() { + let items = vec![ + ("src/foo.rs", "foo.rs"), + ("lib/foo.rs", "foo.rs"), + ("src/bar.rs", "bar.rs"), + ]; + let details = compute_disambiguation_details(&items, |item, detail| match detail { + 0 => item.1.to_string(), + _ => item.0.to_string(), + }); + assert_eq!(details, vec![1, 1, 0]); + } - #[test] - fn test_identical_items_terminates() { - // All items return the same description at every detail level. - // The algorithm must terminate rather than looping forever. - let items = vec!["same", "same", "same"]; - let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); - // After bumping to 1, the description doesn't change from level 0, - // so the items are skipped and the loop terminates. - assert_eq!(details, vec![1, 1, 1]); - } + #[test] + fn test_identical_items_terminates() { + // All items return the same description at every detail level. + // The algorithm must terminate rather than looping forever. + let items = vec!["same", "same", "same"]; + let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); + // After bumping to 1, the description doesn't change from level 0, + // so the items are skipped and the loop terminates. + assert_eq!(details, vec![1, 1, 1]); + } - #[test] - fn test_single_item() { - let items = vec!["only"]; - let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); - assert_eq!(details, vec![0]); - } + #[test] + fn test_single_item() { + let items = vec!["only"]; + let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); + assert_eq!(details, vec![0]); + } - #[test] - fn test_empty_input() { - let items: Vec<&str> = vec![]; - let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); - let expected: Vec = vec![]; - assert_eq!(details, expected); - } + #[test] + fn test_empty_input() { + let items: Vec<&str> = vec![]; + let details = compute_disambiguation_details(&items, |item, _detail| item.to_string()); + let expected: Vec = vec![]; + assert_eq!(details, expected); + } - #[test] - fn test_duplicate_paths_from_multiple_groups() { - use std::path::Path; + #[test] + fn test_duplicate_paths_from_multiple_groups() { + use std::path::Path; - // Simulates the sidebar scenario: a path like /Users/rtfeldman/code/zed - // appears in two project groups (e.g. "zed" alone and "zed, roc"). - // After deduplication, only unique paths should be disambiguated. - // - // Paths: - // /Users/rtfeldman/code/worktrees/zed/focal-arrow/zed (group 1) - // /Users/rtfeldman/code/zed (group 2) - // /Users/rtfeldman/code/zed (group 3, same path as group 2) - // /Users/rtfeldman/code/roc (group 3) - // - // A naive flat_map collects duplicates. The duplicate /code/zed entries - // collide with each other and drive the detail to the full path. - // The fix is to deduplicate before disambiguating. + // Simulates the sidebar scenario: a path like /Users/rtfeldman/code/zed + // appears in two project groups (e.g. "zed" alone and "zed, roc"). + // After deduplication, only unique paths should be disambiguated. + // + // Paths: + // /Users/rtfeldman/code/worktrees/zed/focal-arrow/zed (group 1) + // /Users/rtfeldman/code/zed (group 2) + // /Users/rtfeldman/code/zed (group 3, same path as group 2) + // /Users/rtfeldman/code/roc (group 3) + // + // A naive flat_map collects duplicates. The duplicate /code/zed entries + // collide with each other and drive the detail to the full path. + // The fix is to deduplicate before disambiguating. - fn path_suffix(path: &Path, detail: usize) -> String { - let mut components: Vec<_> = path - .components() - .rev() - .filter_map(|c| match c { - std::path::Component::Normal(s) => Some(s.to_string_lossy()), - _ => None, - }) - .take(detail + 1) - .collect(); - components.reverse(); - components.join("/") - } + fn path_suffix(path: &Path, detail: usize) -> String { + let mut components: Vec<_> = path + .components() + .rev() + .filter_map(|c| match c { + std::path::Component::Normal(s) => Some(s.to_string_lossy()), + _ => None, + }) + .take(detail + 1) + .collect(); + components.reverse(); + components.join("/") + } - let all_paths: Vec<&Path> = vec![ - Path::new("/Users/rtfeldman/code/worktrees/zed/focal-arrow/zed"), - Path::new("/Users/rtfeldman/code/zed"), - Path::new("/Users/rtfeldman/code/roc"), - ]; + let all_paths: Vec<&Path> = vec![ + Path::new("/Users/rtfeldman/code/worktrees/zed/focal-arrow/zed"), + Path::new("/Users/rtfeldman/code/zed"), + Path::new("/Users/rtfeldman/code/roc"), + ]; - let details = - compute_disambiguation_details(&all_paths, |path, detail| path_suffix(path, detail)); + let details = + compute_disambiguation_details(&all_paths, |path, detail| path_suffix(path, detail)); - // focal-arrow/zed and code/zed both end in "zed", so they need detail 1. - // "roc" is unique at detail 0. - assert_eq!(details, vec![1, 1, 0]); + // focal-arrow/zed and code/zed both end in "zed", so they need detail 1. + // "roc" is unique at detail 0. + assert_eq!(details, vec![1, 1, 0]); - assert_eq!(path_suffix(all_paths[0], details[0]), "focal-arrow/zed"); - assert_eq!(path_suffix(all_paths[1], details[1]), "code/zed"); - assert_eq!(path_suffix(all_paths[2], details[2]), "roc"); - } + assert_eq!(path_suffix(all_paths[0], details[0]), "focal-arrow/zed"); + assert_eq!(path_suffix(all_paths[1], details[1]), "code/zed"); + assert_eq!(path_suffix(all_paths[2], details[2]), "roc"); + } } diff --git a/crates/gpui_zed_util/src/fs.rs b/crates/gpui_zed_util/src/fs.rs index 60aab4a2e7..8d16f124ae 100644 --- a/crates/gpui_zed_util/src/fs.rs +++ b/crates/gpui_zed_util/src/fs.rs @@ -7,99 +7,99 @@ use std::path::{Path, PathBuf}; /// Removes all files and directories matching the given predicate pub async fn remove_matching(dir: &Path, predicate: F) where - F: Fn(&Path) -> bool, + F: Fn(&Path) -> bool, { - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if predicate(entry_path.as_path()) - && let Ok(metadata) = fs::metadata(&entry_path).await - { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if predicate(entry_path.as_path()) + && let Ok(metadata) = fs::metadata(&entry_path).await + { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } } pub async fn collect_matching(dir: &Path, predicate: F) -> Vec where - F: Fn(&Path) -> bool, + F: Fn(&Path) -> bool, { - let mut matching = vec![]; + let mut matching = vec![]; - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() - && predicate(entry.path().as_path()) - { - matching.push(entry.path()); - } - } - } + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() + && predicate(entry.path().as_path()) + { + matching.push(entry.path()); + } + } + } - matching + matching } pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option where - F: Fn(&str) -> bool, + F: Fn(&str) -> bool, { - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); - if let Some(file_name) = entry_path - .file_name() - .map(|file_name| file_name.to_string_lossy()) - && predicate(&file_name) - { - return Some(entry_path); - } - } - } - } + if let Some(file_name) = entry_path + .file_name() + .map(|file_name| file_name.to_string_lossy()) + && predicate(&file_name) + { + return Some(entry_path); + } + } + } + } - None + None } pub async fn move_folder_files_to_folder>( - source_path: P, - target_path: P, + source_path: P, + target_path: P, ) -> Result<()> { - if !target_path.as_ref().is_dir() { - bail!("Folder not found or is not a directory"); - } + if !target_path.as_ref().is_dir() { + bail!("Folder not found or is not a directory"); + } - let mut entries = fs::read_dir(source_path.as_ref()).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - let old_path = entry.path(); - let new_path = target_path.as_ref().join(entry.file_name()); + let mut entries = fs::read_dir(source_path.as_ref()).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + let old_path = entry.path(); + let new_path = target_path.as_ref().join(entry.file_name()); - fs::rename(&old_path, &new_path).await?; - } + fs::rename(&old_path, &new_path).await?; + } - fs::remove_dir(source_path).await?; + fs::remove_dir(source_path).await?; - Ok(()) + Ok(()) } #[cfg(unix)] /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { - fs::set_permissions( - path, - ::from_mode(0o755), - ) - .await + fs::set_permissions( + path, + ::from_mode(0o755), + ) + .await } #[cfg(not(unix))] @@ -107,5 +107,5 @@ pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> { - Ok(()) + Ok(()) } diff --git a/crates/gpui_zed_util/src/markdown.rs b/crates/gpui_zed_util/src/markdown.rs index e42ce13b59..289ec6d3c8 100644 --- a/crates/gpui_zed_util/src/markdown.rs +++ b/crates/gpui_zed_util/src/markdown.rs @@ -2,59 +2,59 @@ use std::fmt::{Display, Formatter}; /// Generates a URL-friendly slug from heading text (e.g. "Hello World" → "hello-world"). pub fn generate_heading_slug(text: &str) -> String { - text.trim() - .chars() - .filter_map(|c| { - if c.is_alphanumeric() || c == '-' || c == '_' { - Some(c.to_lowercase().next().unwrap_or(c)) - } else if c == ' ' { - Some('-') - } else { - None - } - }) - .collect() + text.trim() + .chars() + .filter_map(|c| { + if c.is_alphanumeric() || c == '-' || c == '_' { + Some(c.to_lowercase().next().unwrap_or(c)) + } else if c == ' ' { + Some('-') + } else { + None + } + }) + .collect() } /// Returns true if the URL starts with a URI scheme (RFC 3986 §3.1). fn has_uri_scheme(url: &str) -> bool { - let mut chars = url.chars(); - match chars.next() { - Some(c) if c.is_ascii_alphabetic() => {} - _ => return false, - } - for c in chars { - if c == ':' { - return true; - } - if !(c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') { - return false; - } - } - false + let mut chars = url.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() => {} + _ => return false, + } + for c in chars { + if c == ':' { + return true; + } + if !(c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') { + return false; + } + } + false } /// Splits a relative URL into its path and `#fragment` parts. /// Absolute URLs are returned as-is with no fragment. pub fn split_local_url_fragment(url: &str) -> (&str, Option<&str>) { - if has_uri_scheme(url) { - return (url, None); - } - match url.find('#') { - Some(pos) => { - let path = &url[..pos]; - let fragment = &url[pos + 1..]; - ( - path, - if fragment.is_empty() { - None - } else { - Some(fragment) - }, - ) - } - None => (url, None), - } + if has_uri_scheme(url) { + return (url, None); + } + match url.find('#') { + Some(pos) => { + let path = &url[..pos]; + let fragment = &url[pos + 1..]; + ( + path, + if fragment.is_empty() { + None + } else { + Some(fragment) + }, + ) + } + None => (url, None), + } } /// Indicates that the wrapped `String` is markdown text. @@ -62,9 +62,9 @@ pub fn split_local_url_fragment(url: &str) -> (&str, Option<&str>) { pub struct MarkdownString(pub String); impl Display for MarkdownString { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } } /// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow @@ -98,15 +98,15 @@ pub struct MarkdownInlineCode<'a>(pub &'a str); /// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed. pub struct MarkdownCodeBlock<'a> { - pub tag: &'a str, - pub text: &'a str, + pub tag: &'a str, + pub text: &'a str, } impl Display for MarkdownEscaped<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - let mut start_of_unescaped = None; - for (ix, c) in self.0.char_indices() { - match c { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + let mut start_of_unescaped = None; + for (ix, c) in self.0.char_indices() { + match c { // Always escaped. '\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' | // TODO: these only need to be escaped when they are the first non-whitespace @@ -154,94 +154,94 @@ impl Display for MarkdownEscaped<'_> { } } } - } - if let Some(start_of_unescaped) = start_of_unescaped { - write!(formatter, "{}", &self.0[start_of_unescaped..])?; - } - Ok(()) - } + } + if let Some(start_of_unescaped) = start_of_unescaped { + write!(formatter, "{}", &self.0[start_of_unescaped..])?; + } + Ok(()) + } } impl Display for MarkdownInlineCode<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - // Apache License 2.0, same as this crate. - // - // Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications: - // - // * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating - // `Code` events parsed by pulldown-cmark. - // - // https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290 + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + // Apache License 2.0, same as this crate. + // + // Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications: + // + // * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating + // `Code` events parsed by pulldown-cmark. + // + // https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290 - let mut all_whitespace = true; - let text = self - .0 - .chars() - .map(|c| { - if c.is_whitespace() { - ' ' - } else { - all_whitespace = false; - c - } - }) - .collect::(); + let mut all_whitespace = true; + let text = self + .0 + .chars() + .map(|c| { + if c.is_whitespace() { + ' ' + } else { + all_whitespace = false; + c + } + }) + .collect::(); - // When inline code has leading and trailing ' ' characters, additional space is needed - // to escape it, unless all characters are space. - if all_whitespace { - write!(formatter, "`{text}`") - } else { - // More backticks are needed to delimit the inline code than the maximum number of - // backticks in a consecutive run. - let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1); - let space = match text.as_bytes() { - &[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick. - &[b' ', .., b' '] => " ", // Space needed to escape inner space. - _ => "", // No space needed. - }; - write!(formatter, "{backticks}{space}{text}{space}{backticks}") - } - } + // When inline code has leading and trailing ' ' characters, additional space is needed + // to escape it, unless all characters are space. + if all_whitespace { + write!(formatter, "`{text}`") + } else { + // More backticks are needed to delimit the inline code than the maximum number of + // backticks in a consecutive run. + let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1); + let space = match text.as_bytes() { + &[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick. + &[b' ', .., b' '] => " ", // Space needed to escape inner space. + _ => "", // No space needed. + }; + write!(formatter, "{backticks}{space}{text}{space}{backticks}") + } + } } impl Display for MarkdownCodeBlock<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - let tag = self.tag; - let text = self.text; - let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1)); - write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n") - } + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + let tag = self.tag; + let text = self.text; + let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1)); + write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n") + } } // Copied from `pulldown-cmark-to-cmark-20.0.0` with changed names. // https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L1063 // Apache License 2.0, same as this code. fn count_max_consecutive_chars(text: &str, search: char) -> usize { - let mut in_search_chars = false; - let mut max_count = 0; - let mut cur_count = 0; + let mut in_search_chars = false; + let mut max_count = 0; + let mut cur_count = 0; - for ch in text.chars() { - if ch == search { - cur_count += 1; - in_search_chars = true; - } else if in_search_chars { - max_count = max_count.max(cur_count); - cur_count = 0; - in_search_chars = false; - } - } - max_count.max(cur_count) + for ch in text.chars() { + if ch == search { + cur_count += 1; + in_search_chars = true; + } else if in_search_chars { + max_count = max_count.max(cur_count); + cur_count = 0; + in_search_chars = false; + } + } + max_count.max(cur_count) } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_markdown_escaped() { - let input = r#" + #[test] + fn test_markdown_escaped() { + let input = r#" # Heading Another heading @@ -266,7 +266,7 @@ mod tests { HTML entity:   "#; - let expected = r#" + let expected = r#" \# Heading Another heading @@ -291,86 +291,86 @@ mod tests { HTML entity: \  "#; - assert_eq!(MarkdownEscaped(input).to_string(), expected); - } + assert_eq!(MarkdownEscaped(input).to_string(), expected); + } - #[test] - fn test_markdown_inline_code() { - assert_eq!(MarkdownInlineCode(" ").to_string(), "` `"); - assert_eq!(MarkdownInlineCode("text").to_string(), "`text`"); - assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `"); - assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `"); - assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``"); - assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```"); - assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``"); - assert_eq!( - MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(), - "``some `text` no leading or trailing backticks``" - ); - } + #[test] + fn test_markdown_inline_code() { + assert_eq!(MarkdownInlineCode(" ").to_string(), "` `"); + assert_eq!(MarkdownInlineCode("text").to_string(), "`text`"); + assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `"); + assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `"); + assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``"); + assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```"); + assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``"); + assert_eq!( + MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(), + "``some `text` no leading or trailing backticks``" + ); + } - #[test] - fn test_count_max_consecutive_chars() { - assert_eq!( - count_max_consecutive_chars("``a```b``", '`'), - 3, - "the highest seen consecutive segment of backticks counts" - ); - assert_eq!( - count_max_consecutive_chars("```a``b`", '`'), - 3, - "it can't be downgraded later" - ); - } + #[test] + fn test_count_max_consecutive_chars() { + assert_eq!( + count_max_consecutive_chars("``a```b``", '`'), + 3, + "the highest seen consecutive segment of backticks counts" + ); + assert_eq!( + count_max_consecutive_chars("```a``b`", '`'), + 3, + "it can't be downgraded later" + ); + } - #[test] - fn test_split_local_url_fragment() { - assert_eq!(split_local_url_fragment("#heading"), ("", Some("heading"))); - assert_eq!( - split_local_url_fragment("./file.md#heading"), - ("./file.md", Some("heading")) - ); - assert_eq!(split_local_url_fragment("./file.md"), ("./file.md", None)); - assert_eq!( - split_local_url_fragment("https://example.com#frag"), - ("https://example.com#frag", None) - ); - assert_eq!( - split_local_url_fragment("mailto:user@example.com"), - ("mailto:user@example.com", None) - ); - assert_eq!(split_local_url_fragment("#"), ("", None)); - assert_eq!( - split_local_url_fragment("../other.md#section"), - ("../other.md", Some("section")) - ); - assert_eq!( - split_local_url_fragment("123:not-a-scheme#frag"), - ("123:not-a-scheme", Some("frag")) - ); - } + #[test] + fn test_split_local_url_fragment() { + assert_eq!(split_local_url_fragment("#heading"), ("", Some("heading"))); + assert_eq!( + split_local_url_fragment("./file.md#heading"), + ("./file.md", Some("heading")) + ); + assert_eq!(split_local_url_fragment("./file.md"), ("./file.md", None)); + assert_eq!( + split_local_url_fragment("https://example.com#frag"), + ("https://example.com#frag", None) + ); + assert_eq!( + split_local_url_fragment("mailto:user@example.com"), + ("mailto:user@example.com", None) + ); + assert_eq!(split_local_url_fragment("#"), ("", None)); + assert_eq!( + split_local_url_fragment("../other.md#section"), + ("../other.md", Some("section")) + ); + assert_eq!( + split_local_url_fragment("123:not-a-scheme#frag"), + ("123:not-a-scheme", Some("frag")) + ); + } - #[test] - fn test_generate_heading_slug() { - assert_eq!(generate_heading_slug("Hello World"), "hello-world"); - assert_eq!(generate_heading_slug("Hello World"), "hello--world"); - assert_eq!(generate_heading_slug("Hello-World"), "hello-world"); - assert_eq!( - generate_heading_slug("Some **bold** text"), - "some-bold-text" - ); - assert_eq!(generate_heading_slug("Let's try with Ü"), "lets-try-with-ü"); - assert_eq!( - generate_heading_slug("heading with 123 numbers"), - "heading-with-123-numbers" - ); - assert_eq!( - generate_heading_slug("What about (parens)?"), - "what-about-parens" - ); - assert_eq!( - generate_heading_slug(" leading spaces "), - "leading-spaces" - ); - } + #[test] + fn test_generate_heading_slug() { + assert_eq!(generate_heading_slug("Hello World"), "hello-world"); + assert_eq!(generate_heading_slug("Hello World"), "hello--world"); + assert_eq!(generate_heading_slug("Hello-World"), "hello-world"); + assert_eq!( + generate_heading_slug("Some **bold** text"), + "some-bold-text" + ); + assert_eq!(generate_heading_slug("Let's try with Ü"), "lets-try-with-ü"); + assert_eq!( + generate_heading_slug("heading with 123 numbers"), + "heading-with-123-numbers" + ); + assert_eq!( + generate_heading_slug("What about (parens)?"), + "what-about-parens" + ); + assert_eq!( + generate_heading_slug(" leading spaces "), + "leading-spaces" + ); + } } diff --git a/crates/gpui_zed_util/src/path_list.rs b/crates/gpui_zed_util/src/path_list.rs index af99f4c657..bc4d0d81db 100644 --- a/crates/gpui_zed_util/src/path_list.rs +++ b/crates/gpui_zed_util/src/path_list.rs @@ -1,7 +1,7 @@ use std::{ - hash::{Hash, Hasher}, - path::{Path, PathBuf}, - sync::Arc, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, + sync::Arc, }; use crate::paths::SanitizedPath; @@ -16,218 +16,218 @@ use serde::{Deserialize, Serialize}; /// The paths can be retrieved in the original order using `ordered_paths()`. #[derive(Default, Debug, Clone)] pub struct PathList { - /// The paths, in lexicographic order. - paths: Arc<[PathBuf]>, - /// The order in which the paths were provided. - /// - /// See `ordered_paths()` for a way to get the paths in the original order. - order: Arc<[usize]>, + /// The paths, in lexicographic order. + paths: Arc<[PathBuf]>, + /// The order in which the paths were provided. + /// + /// See `ordered_paths()` for a way to get the paths in the original order. + order: Arc<[usize]>, } impl PartialEq for PathList { - fn eq(&self, other: &Self) -> bool { - self.paths == other.paths - } + fn eq(&self, other: &Self) -> bool { + self.paths == other.paths + } } impl Eq for PathList {} impl Hash for PathList { - fn hash(&self, state: &mut H) { - self.paths.hash(state); - } + fn hash(&self, state: &mut H) { + self.paths.hash(state); + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializedPathList { - pub paths: String, - pub order: String, + pub paths: String, + pub order: String, } impl PathList { - pub fn new>(paths: &[P]) -> Self { - let mut indexed_paths: Vec<(usize, PathBuf)> = paths - .iter() - .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::new(path).into())) - .collect(); - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); - let paths = indexed_paths - .into_iter() - .map(|e| e.1) - .collect::>() - .into(); - Self { order, paths } - } + pub fn new>(paths: &[P]) -> Self { + let mut indexed_paths: Vec<(usize, PathBuf)> = paths + .iter() + .enumerate() + .map(|(ix, path)| (ix, SanitizedPath::new(path).into())) + .collect(); + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); + let paths = indexed_paths + .into_iter() + .map(|e| e.1) + .collect::>() + .into(); + Self { order, paths } + } - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } + pub fn is_empty(&self) -> bool { + self.paths.is_empty() + } - /// Returns a new `PathList` with the given path removed. - pub fn without_path(&self, path_to_remove: &Path) -> PathList { - let paths: Vec = self - .ordered_paths() - .filter(|p| p.as_path() != path_to_remove) - .cloned() - .collect(); - PathList::new(&paths) - } + /// Returns a new `PathList` with the given path removed. + pub fn without_path(&self, path_to_remove: &Path) -> PathList { + let paths: Vec = self + .ordered_paths() + .filter(|p| p.as_path() != path_to_remove) + .cloned() + .collect(); + PathList::new(&paths) + } - /// Get the paths in lexicographic order. - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } + /// Get the paths in lexicographic order. + pub fn paths(&self) -> &[PathBuf] { + self.paths.as_ref() + } - /// Get the paths in the lexicographic order. - pub fn paths_owned(&self) -> Arc<[PathBuf]> { - self.paths.clone() - } + /// Get the paths in the lexicographic order. + pub fn paths_owned(&self) -> Arc<[PathBuf]> { + self.paths.clone() + } - /// Get the order in which the paths were provided. - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } + /// Get the order in which the paths were provided. + pub fn order(&self) -> &[usize] { + self.order.as_ref() + } - /// Get the paths in the original order. - pub fn ordered_paths(&self) -> impl Iterator { - self.order - .iter() - .zip(self.paths.iter()) - .sorted_by_key(|(i, _)| **i) - .map(|(_, path)| path) - } + /// Get the paths in the original order. + pub fn ordered_paths(&self) -> impl Iterator { + self.order + .iter() + .zip(self.paths.iter()) + .sorted_by_key(|(i, _)| **i) + .map(|(_, path)| path) + } - pub fn is_lexicographically_ordered(&self) -> bool { - self.order.iter().enumerate().all(|(i, &j)| i == j) - } + pub fn is_lexicographically_ordered(&self) -> bool { + self.order.iter().enumerate().all(|(i, &j)| i == j) + } - pub fn deserialize(serialized: &SerializedPathList) -> Self { - let mut paths: Vec = if serialized.paths.is_empty() { - Vec::new() - } else { - serialized.paths.split('\n').map(PathBuf::from).collect() - }; + pub fn deserialize(serialized: &SerializedPathList) -> Self { + let mut paths: Vec = if serialized.paths.is_empty() { + Vec::new() + } else { + serialized.paths.split('\n').map(PathBuf::from).collect() + }; - let mut order: Vec = serialized - .order - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); + let mut order: Vec = serialized + .order + .split(',') + .filter_map(|s| s.parse().ok()) + .collect(); - if !paths.is_sorted() || order.len() != paths.len() { - order = (0..paths.len()).collect(); - paths.sort(); - } + if !paths.is_sorted() || order.len() != paths.len() { + order = (0..paths.len()).collect(); + paths.sort(); + } - Self { - paths: paths.into(), - order: order.into(), - } - } + Self { + paths: paths.into(), + order: order.into(), + } + } - pub fn serialize(&self) -> SerializedPathList { - use std::fmt::Write as _; + pub fn serialize(&self) -> SerializedPathList { + use std::fmt::Write as _; - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } + let mut paths = String::new(); + for path in self.paths.iter() { + if !paths.is_empty() { + paths.push('\n'); + } + paths.push_str(&path.to_string_lossy()); + } - let mut order = String::new(); - for ix in self.order.iter() { - if !order.is_empty() { - order.push(','); - } - write!(&mut order, "{}", *ix).unwrap(); - } - SerializedPathList { paths, order } - } + let mut order = String::new(); + for ix in self.order.iter() { + if !order.is_empty() { + order.push(','); + } + write!(&mut order, "{}", *ix).unwrap(); + } + SerializedPathList { paths, order } + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_path_list() { - let list1 = PathList::new(&["a/d", "a/c"]); - let list2 = PathList::new(&["a/c", "a/d"]); + #[test] + fn test_path_list() { + let list1 = PathList::new(&["a/d", "a/c"]); + let list2 = PathList::new(&["a/c", "a/d"]); - assert_eq!(list1.paths(), list2.paths(), "paths differ"); - assert_eq!(list1.order(), &[1, 0], "list1 order incorrect"); - assert_eq!(list2.order(), &[0, 1], "list2 order incorrect"); + assert_eq!(list1.paths(), list2.paths(), "paths differ"); + assert_eq!(list1.order(), &[1, 0], "list1 order incorrect"); + assert_eq!(list2.order(), &[0, 1], "list2 order incorrect"); - // Same paths in different order are equal (order is display-only). - assert_eq!( - list1, list2, - "same paths with different order should be equal" - ); + // Same paths in different order are equal (order is display-only). + assert_eq!( + list1, list2, + "same paths with different order should be equal" + ); - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1, "list1 deserialization failed"); + let list1_deserialized = PathList::deserialize(&list1.serialize()); + assert_eq!(list1_deserialized, list1, "list1 deserialization failed"); - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2, "list2 deserialization failed"); + let list2_deserialized = PathList::deserialize(&list2.serialize()); + assert_eq!(list2_deserialized, list2, "list2 deserialization failed"); - assert_eq!( - list1.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/d"), &PathBuf::from("a/c")], - "list1 ordered paths incorrect" - ); - assert_eq!( - list2.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/c"), &PathBuf::from("a/d")], - "list2 ordered paths incorrect" - ); - } + assert_eq!( + list1.ordered_paths().collect_array().unwrap(), + [&PathBuf::from("a/d"), &PathBuf::from("a/c")], + "list1 ordered paths incorrect" + ); + assert_eq!( + list2.ordered_paths().collect_array().unwrap(), + [&PathBuf::from("a/c"), &PathBuf::from("a/d")], + "list2 ordered paths incorrect" + ); + } - #[test] - fn test_path_list_ordering() { - let list = PathList::new(&["b", "a", "c"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[1, 0, 2]); - assert!(!list.is_lexicographically_ordered()); + #[test] + fn test_path_list_ordering() { + let list = PathList::new(&["b", "a", "c"]); + assert_eq!( + list.paths(), + &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] + ); + assert_eq!(list.order(), &[1, 0, 2]); + assert!(!list.is_lexicographically_ordered()); - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); + let serialized = list.serialize(); + let deserialized = PathList::deserialize(&serialized); + assert_eq!(deserialized, list); - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("a"), - &PathBuf::from("c") - ] - ); + assert_eq!( + deserialized.ordered_paths().collect_array().unwrap(), + [ + &PathBuf::from("b"), + &PathBuf::from("a"), + &PathBuf::from("c") + ] + ); - let list = PathList::new(&["b", "c", "a"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[2, 0, 1]); - assert!(!list.is_lexicographically_ordered()); + let list = PathList::new(&["b", "c", "a"]); + assert_eq!( + list.paths(), + &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] + ); + assert_eq!(list.order(), &[2, 0, 1]); + assert!(!list.is_lexicographically_ordered()); - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); + let serialized = list.serialize(); + let deserialized = PathList::deserialize(&serialized); + assert_eq!(deserialized, list); - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("c"), - &PathBuf::from("a"), - ] - ); - } + assert_eq!( + deserialized.ordered_paths().collect_array().unwrap(), + [ + &PathBuf::from("b"), + &PathBuf::from("c"), + &PathBuf::from("a"), + ] + ); + } } diff --git a/crates/gpui_zed_util/src/paths.rs b/crates/gpui_zed_util/src/paths.rs index 86e459cff7..ebdd84dab1 100644 --- a/crates/gpui_zed_util/src/paths.rs +++ b/crates/gpui_zed_util/src/paths.rs @@ -10,9 +10,9 @@ use std::mem; use std::path::StripPrefixError; use std::sync::Arc; use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - sync::LazyLock, + ffi::OsStr, + path::{Path, PathBuf}, + sync::LazyLock, }; use crate::rel_path::RelPath; @@ -20,179 +20,179 @@ use crate::rel_path::RelPathBuf; /// Returns the path to the user's home directory. pub fn home_dir() -> &'static PathBuf { - static HOME_DIR: std::sync::OnceLock = std::sync::OnceLock::new(); - HOME_DIR.get_or_init(|| { - if cfg!(any(test, feature = "test-support")) { - if cfg!(target_os = "macos") { - PathBuf::from("/Users/zed") - } else if cfg!(target_os = "windows") { - PathBuf::from("C:\\Users\\zed") - } else { - PathBuf::from("/home/zed") - } - } else { - dirs::home_dir().expect("failed to determine home directory") - } - }) + static HOME_DIR: std::sync::OnceLock = std::sync::OnceLock::new(); + HOME_DIR.get_or_init(|| { + if cfg!(any(test, feature = "test-support")) { + if cfg!(target_os = "macos") { + PathBuf::from("/Users/zed") + } else if cfg!(target_os = "windows") { + PathBuf::from("C:\\Users\\zed") + } else { + PathBuf::from("/home/zed") + } + } else { + dirs::home_dir().expect("failed to determine home directory") + } + }) } pub trait PathExt { - /// Compacts a given file path by replacing the user's home directory - /// prefix with a tilde (`~`). - /// - /// # Returns - /// - /// * A `PathBuf` containing the compacted file path. If the input path - /// does not have the user's home directory prefix, or if we are not on - /// Linux or macOS, the original path is returned unchanged. - fn compact(&self) -> PathBuf; + /// Compacts a given file path by replacing the user's home directory + /// prefix with a tilde (`~`). + /// + /// # Returns + /// + /// * A `PathBuf` containing the compacted file path. If the input path + /// does not have the user's home directory prefix, or if we are not on + /// Linux or macOS, the original path is returned unchanged. + fn compact(&self) -> PathBuf; - /// Returns a file's extension or, if the file is hidden, its name without the leading dot - fn extension_or_hidden_file_name(&self) -> Option<&str>; + /// Returns a file's extension or, if the file is hidden, its name without the leading dot + fn extension_or_hidden_file_name(&self) -> Option<&str>; - fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result - where - Self: From<&'a Path>, - { - #[cfg(target_family = "wasm")] - { - std::str::from_utf8(bytes) - .map(Path::new) - .map(Into::into) - .map_err(Into::into) - } - #[cfg(unix)] - { - use std::os::unix::prelude::OsStrExt; - Ok(Self::from(Path::new(OsStr::from_bytes(bytes)))) - } - #[cfg(windows)] - { - use anyhow::Context; - use tendril::fmt::{Format, WTF8}; - WTF8::validate(bytes) - .then(|| { - // Safety: bytes are valid WTF-8 sequence. - Self::from(Path::new(unsafe { - OsStr::from_encoded_bytes_unchecked(bytes) - })) - }) - .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}")) - } - } + fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result + where + Self: From<&'a Path>, + { + #[cfg(target_family = "wasm")] + { + std::str::from_utf8(bytes) + .map(Path::new) + .map(Into::into) + .map_err(Into::into) + } + #[cfg(unix)] + { + use std::os::unix::prelude::OsStrExt; + Ok(Self::from(Path::new(OsStr::from_bytes(bytes)))) + } + #[cfg(windows)] + { + use anyhow::Context; + use tendril::fmt::{Format, WTF8}; + WTF8::validate(bytes) + .then(|| { + // Safety: bytes are valid WTF-8 sequence. + Self::from(Path::new(unsafe { + OsStr::from_encoded_bytes_unchecked(bytes) + })) + }) + .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}")) + } + } - /// Converts a local path to one that can be used inside of WSL. - /// Returns `None` if the path cannot be converted into a WSL one (network share). - fn local_to_wsl(&self) -> Option; + /// Converts a local path to one that can be used inside of WSL. + /// Returns `None` if the path cannot be converted into a WSL one (network share). + fn local_to_wsl(&self) -> Option; - /// Returns a file's "full" joined collection of extensions, in the case where a file does not - /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx) - /// - /// Will provide back the extensions joined together such as tar.gz or stories.tsx - fn multiple_extensions(&self) -> Option; + /// Returns a file's "full" joined collection of extensions, in the case where a file does not + /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx) + /// + /// Will provide back the extensions joined together such as tar.gz or stories.tsx + fn multiple_extensions(&self) -> Option; - /// Try to make a shell-safe representation of the path. - #[cfg(not(target_family = "wasm"))] - fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result; + /// Try to make a shell-safe representation of the path. + #[cfg(not(target_family = "wasm"))] + fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result; } impl> PathExt for T { - fn compact(&self) -> PathBuf { - #[cfg(target_family = "wasm")] - { - self.as_ref().to_path_buf() - } - #[cfg(not(target_family = "wasm"))] - if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { - match self.as_ref().strip_prefix(home_dir().as_path()) { - Ok(relative_path) => { - let mut shortened_path = PathBuf::new(); - shortened_path.push("~"); - shortened_path.push(relative_path); - shortened_path - } - Err(_) => self.as_ref().to_path_buf(), - } - } else { - self.as_ref().to_path_buf() - } - } + fn compact(&self) -> PathBuf { + #[cfg(target_family = "wasm")] + { + self.as_ref().to_path_buf() + } + #[cfg(not(target_family = "wasm"))] + if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { + match self.as_ref().strip_prefix(home_dir().as_path()) { + Ok(relative_path) => { + let mut shortened_path = PathBuf::new(); + shortened_path.push("~"); + shortened_path.push(relative_path); + shortened_path + } + Err(_) => self.as_ref().to_path_buf(), + } + } else { + self.as_ref().to_path_buf() + } + } - fn extension_or_hidden_file_name(&self) -> Option<&str> { - let path = self.as_ref(); - let file_name = path.file_name()?.to_str()?; - if file_name.starts_with('.') { - return file_name.strip_prefix('.'); - } + fn extension_or_hidden_file_name(&self) -> Option<&str> { + let path = self.as_ref(); + let file_name = path.file_name()?.to_str()?; + if file_name.starts_with('.') { + return file_name.strip_prefix('.'); + } - path.extension() - .and_then(|e| e.to_str()) - .or_else(|| path.file_stem()?.to_str()) - } + path.extension() + .and_then(|e| e.to_str()) + .or_else(|| path.file_stem()?.to_str()) + } - fn local_to_wsl(&self) -> Option { - // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths - // todo: ideally rework them..? - let mut new_path = std::ffi::OsString::new(); - for component in self.as_ref().components() { - match component { - std::path::Component::Prefix(prefix) => { - let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase(); - let drive_letter = drive_letter.strip_suffix(':')?; + fn local_to_wsl(&self) -> Option { + // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths + // todo: ideally rework them..? + let mut new_path = std::ffi::OsString::new(); + for component in self.as_ref().components() { + match component { + std::path::Component::Prefix(prefix) => { + let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase(); + let drive_letter = drive_letter.strip_suffix(':')?; - new_path.push(format!("/mnt/{}", drive_letter)); - } - std::path::Component::RootDir => {} - std::path::Component::CurDir => { - new_path.push("/."); - } - std::path::Component::ParentDir => { - new_path.push("/.."); - } - std::path::Component::Normal(os_str) => { - new_path.push("/"); - new_path.push(os_str); - } - } - } + new_path.push(format!("/mnt/{}", drive_letter)); + } + std::path::Component::RootDir => {} + std::path::Component::CurDir => { + new_path.push("/."); + } + std::path::Component::ParentDir => { + new_path.push("/.."); + } + std::path::Component::Normal(os_str) => { + new_path.push("/"); + new_path.push(os_str); + } + } + } - Some(new_path.into()) - } + Some(new_path.into()) + } - fn multiple_extensions(&self) -> Option { - let path = self.as_ref(); - let file_name = path.file_name()?.to_str()?; + fn multiple_extensions(&self) -> Option { + let path = self.as_ref(); + let file_name = path.file_name()?.to_str()?; - let parts: Vec<&str> = file_name - .split('.') - // Skip the part with the file name extension - .skip(1) - .collect(); + let parts: Vec<&str> = file_name + .split('.') + // Skip the part with the file name extension + .skip(1) + .collect(); - if parts.len() < 2 { - return None; - } + if parts.len() < 2 { + return None; + } - Some(parts.into_iter().join(".")) - } + Some(parts.into_iter().join(".")) + } - #[cfg(not(target_family = "wasm"))] - fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result { - use anyhow::Context; - let path_str = self - .as_ref() - .to_str() - .with_context(|| "Path contains invalid UTF-8")?; - shell_kind - .try_quote(path_str) - .as_deref() - .map(ToOwned::to_owned) - .context("Failed to quote path") - } + #[cfg(not(target_family = "wasm"))] + fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result { + use anyhow::Context; + let path_str = self + .as_ref() + .to_str() + .with_context(|| "Path contains invalid UTF-8")?; + shell_kind + .try_quote(path_str) + .as_deref() + .map(ToOwned::to_owned) + .context("Failed to quote path") + } } pub fn path_ends_with(base: &Path, suffix: &Path) -> bool { - strip_path_suffix(base, suffix).is_some() + strip_path_suffix(base, suffix).is_some() } /// Case-insensitive ASCII comparison of a path component to a literal @@ -203,26 +203,26 @@ pub fn path_ends_with(base: &Path, suffix: &Path) -> bool { /// unusual casing. Callers should restrict `name` to ASCII; for ASCII /// inputs `eq_ignore_ascii_case` is safe and stable across platforms. pub fn component_matches_ignore_ascii_case(component: &OsStr, name: &str) -> bool { - component - .to_str() - .is_some_and(|s| s.eq_ignore_ascii_case(name)) + component + .to_str() + .is_some_and(|s| s.eq_ignore_ascii_case(name)) } pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> { - if let Some(remainder) = base - .as_os_str() - .as_encoded_bytes() - .strip_suffix(suffix.as_os_str().as_encoded_bytes()) - && remainder - .last() - .is_none_or(|last_byte| std::path::is_separator(*last_byte as char)) - { - let os_str = unsafe { - OsStr::from_encoded_bytes_unchecked(&remainder[0..remainder.len().saturating_sub(1)]) - }; - return Some(Path::new(os_str)); - } - None + if let Some(remainder) = base + .as_os_str() + .as_encoded_bytes() + .strip_suffix(suffix.as_os_str().as_encoded_bytes()) + && remainder + .last() + .is_none_or(|last_byte| std::path::is_separator(*last_byte as char)) + { + let os_str = unsafe { + OsStr::from_encoded_bytes_unchecked(&remainder[0..remainder.len().saturating_sub(1)]) + }; + return Some(Path::new(os_str)); + } + None } /// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On @@ -232,353 +232,353 @@ pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> pub struct SanitizedPath(Path); impl SanitizedPath { - pub fn new + ?Sized>(path: &T) -> &Self { - #[cfg(not(target_os = "windows"))] - return Self::unchecked_new(path.as_ref()); + pub fn new + ?Sized>(path: &T) -> &Self { + #[cfg(not(target_os = "windows"))] + return Self::unchecked_new(path.as_ref()); - #[cfg(target_os = "windows")] - return Self::unchecked_new(dunce::simplified(path.as_ref())); - } + #[cfg(target_os = "windows")] + return Self::unchecked_new(dunce::simplified(path.as_ref())); + } - pub fn unchecked_new + ?Sized>(path: &T) -> &Self { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) } - } + pub fn unchecked_new + ?Sized>(path: &T) -> &Self { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) } + } - pub fn from_arc(path: Arc) -> Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - #[cfg(not(target_os = "windows"))] - return unsafe { mem::transmute::, Arc>(path) }; + pub fn from_arc(path: Arc) -> Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + #[cfg(not(target_os = "windows"))] + return unsafe { mem::transmute::, Arc>(path) }; - #[cfg(target_os = "windows")] - { - let simplified = dunce::simplified(path.as_ref()); - if simplified == path.as_ref() { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::, Arc>(path) } - } else { - Self::unchecked_new(simplified).into() - } - } - } + #[cfg(target_os = "windows")] + { + let simplified = dunce::simplified(path.as_ref()); + if simplified == path.as_ref() { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::, Arc>(path) } + } else { + Self::unchecked_new(simplified).into() + } + } + } - pub fn new_arc + ?Sized>(path: &T) -> Arc { - Self::new(path).into() - } + pub fn new_arc + ?Sized>(path: &T) -> Arc { + Self::new(path).into() + } - pub fn cast_arc(path: Arc) -> Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::, Arc>(path) } - } + pub fn cast_arc(path: Arc) -> Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::, Arc>(path) } + } - pub fn cast_arc_ref(path: &Arc) -> &Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::<&Arc, &Arc>(path) } - } + pub fn cast_arc_ref(path: &Arc) -> &Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::<&Arc, &Arc>(path) } + } - pub fn starts_with(&self, prefix: &Self) -> bool { - self.0.starts_with(&prefix.0) - } + pub fn starts_with(&self, prefix: &Self) -> bool { + self.0.starts_with(&prefix.0) + } - pub fn as_path(&self) -> &Path { - &self.0 - } + pub fn as_path(&self) -> &Path { + &self.0 + } - pub fn file_name(&self) -> Option<&std::ffi::OsStr> { - self.0.file_name() - } + pub fn file_name(&self) -> Option<&std::ffi::OsStr> { + self.0.file_name() + } - pub fn extension(&self) -> Option<&std::ffi::OsStr> { - self.0.extension() - } + pub fn extension(&self) -> Option<&std::ffi::OsStr> { + self.0.extension() + } - pub fn join>(&self, path: P) -> PathBuf { - self.0.join(path) - } + pub fn join>(&self, path: P) -> PathBuf { + self.0.join(path) + } - pub fn parent(&self) -> Option<&Self> { - self.0.parent().map(Self::unchecked_new) - } + pub fn parent(&self) -> Option<&Self> { + self.0.parent().map(Self::unchecked_new) + } - pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> { - self.0.strip_prefix(base.as_path()) - } + pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> { + self.0.strip_prefix(base.as_path()) + } - pub fn to_str(&self) -> Option<&str> { - self.0.to_str() - } + pub fn to_str(&self) -> Option<&str> { + self.0.to_str() + } - pub fn to_path_buf(&self) -> PathBuf { - self.0.to_path_buf() - } + pub fn to_path_buf(&self) -> PathBuf { + self.0.to_path_buf() + } } impl std::fmt::Debug for SanitizedPath { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, formatter) - } + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.0, formatter) + } } impl Display for SanitizedPath { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.display()) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } } impl From<&SanitizedPath> for Arc { - fn from(sanitized_path: &SanitizedPath) -> Self { - let path: Arc = sanitized_path.0.into(); - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute(path) } - } + fn from(sanitized_path: &SanitizedPath) -> Self { + let path: Arc = sanitized_path.0.into(); + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute(path) } + } } impl From<&SanitizedPath> for PathBuf { - fn from(sanitized_path: &SanitizedPath) -> Self { - sanitized_path.as_path().into() - } + fn from(sanitized_path: &SanitizedPath) -> Self { + sanitized_path.as_path().into() + } } impl AsRef for SanitizedPath { - fn as_ref(&self) -> &Path { - &self.0 - } + fn as_ref(&self) -> &Path { + &self.0 + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PathStyle { - Posix, - Windows, + Posix, + Windows, } impl PathStyle { - #[cfg(target_os = "windows")] - pub const fn local() -> Self { - PathStyle::Windows - } + #[cfg(target_os = "windows")] + pub const fn local() -> Self { + PathStyle::Windows + } - #[cfg(not(target_os = "windows"))] - pub const fn local() -> Self { - PathStyle::Posix - } + #[cfg(not(target_os = "windows"))] + pub const fn local() -> Self { + PathStyle::Posix + } - #[inline] - pub fn primary_separator(&self) -> &'static str { - match self { - PathStyle::Posix => "/", - PathStyle::Windows => "\\", - } - } + #[inline] + pub fn primary_separator(&self) -> &'static str { + match self { + PathStyle::Posix => "/", + PathStyle::Windows => "\\", + } + } - pub fn separators(&self) -> &'static [&'static str] { - match self { - PathStyle::Posix => &["/"], - PathStyle::Windows => &["\\", "/"], - } - } + pub fn separators(&self) -> &'static [&'static str] { + match self { + PathStyle::Posix => &["/"], + PathStyle::Windows => &["\\", "/"], + } + } - pub fn separators_ch(&self) -> &'static [char] { - match self { - PathStyle::Posix => &['/'], - PathStyle::Windows => &['\\', '/'], - } - } + pub fn separators_ch(&self) -> &'static [char] { + match self { + PathStyle::Posix => &['/'], + PathStyle::Windows => &['\\', '/'], + } + } - pub fn is_absolute(&self, path_like: &str) -> bool { - path_like.starts_with('/') - || *self == PathStyle::Windows - && (path_like.starts_with('\\') - || path_like - .chars() - .next() - .is_some_and(|c| c.is_ascii_alphabetic()) - && path_like[1..] - .strip_prefix(':') - .is_some_and(|path| path.starts_with('/') || path.starts_with('\\'))) - } + pub fn is_absolute(&self, path_like: &str) -> bool { + path_like.starts_with('/') + || *self == PathStyle::Windows + && (path_like.starts_with('\\') + || path_like + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic()) + && path_like[1..] + .strip_prefix(':') + .is_some_and(|path| path.starts_with('/') || path.starts_with('\\'))) + } - pub fn is_windows(&self) -> bool { - *self == PathStyle::Windows - } + pub fn is_windows(&self) -> bool { + *self == PathStyle::Windows + } - pub fn is_posix(&self) -> bool { - *self == PathStyle::Posix - } + pub fn is_posix(&self) -> bool { + *self == PathStyle::Posix + } - pub fn join(self, left: impl AsRef, right: impl AsRef) -> Option { - let right = right.as_ref().to_str()?; - if is_absolute(right, self) { - return None; - } - let left = left.as_ref().to_str()?; - if left.is_empty() { - Some(right.into()) - } else { - Some(format!( - "{left}{}{right}", - if left.ends_with(self.primary_separator()) { - "" - } else { - self.primary_separator() - } - )) - } - } + pub fn join(self, left: impl AsRef, right: impl AsRef) -> Option { + let right = right.as_ref().to_str()?; + if is_absolute(right, self) { + return None; + } + let left = left.as_ref().to_str()?; + if left.is_empty() { + Some(right.into()) + } else { + Some(format!( + "{left}{}{right}", + if left.ends_with(self.primary_separator()) { + "" + } else { + self.primary_separator() + } + )) + } + } - pub fn join_path( - self, - left: impl AsRef, - right: impl AsRef, - ) -> anyhow::Result { - let left = left - .as_ref() - .to_str() - .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?; - let right = right.as_ref(); - let right_string = right - .to_str() - .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?; - let joined = self - .join(left, right_string) - .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?; - Ok(PathBuf::from(self.normalize(&joined))) - } + pub fn join_path( + self, + left: impl AsRef, + right: impl AsRef, + ) -> anyhow::Result { + let left = left + .as_ref() + .to_str() + .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?; + let right = right.as_ref(); + let right_string = right + .to_str() + .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?; + let joined = self + .join(left, right_string) + .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?; + Ok(PathBuf::from(self.normalize(&joined))) + } - pub fn normalize(self, path_like: &str) -> String { - match self { - PathStyle::Windows => crate::normalize_path(Path::new(path_like)) - .to_string_lossy() - .into_owned(), - PathStyle::Posix => { - let is_absolute = path_like.starts_with('/'); - let remainder = if is_absolute { - path_like.trim_start_matches('/') - } else { - path_like - }; + pub fn normalize(self, path_like: &str) -> String { + match self { + PathStyle::Windows => crate::normalize_path(Path::new(path_like)) + .to_string_lossy() + .into_owned(), + PathStyle::Posix => { + let is_absolute = path_like.starts_with('/'); + let remainder = if is_absolute { + path_like.trim_start_matches('/') + } else { + path_like + }; - let mut components = Vec::new(); - for component in remainder.split(self.separators_ch()) { - match component { - "" | "." => {} - ".." => { - if components - .last() - .is_some_and(|component| *component != "..") - { - components.pop(); - } else if !is_absolute { - components.push(component); - } - } - component => components.push(component), - } - } + let mut components = Vec::new(); + for component in remainder.split(self.separators_ch()) { + match component { + "" | "." => {} + ".." => { + if components + .last() + .is_some_and(|component| *component != "..") + { + components.pop(); + } else if !is_absolute { + components.push(component); + } + } + component => components.push(component), + } + } - let normalized = components.join(self.primary_separator()); - if is_absolute && normalized.is_empty() { - "/".to_string() - } else if is_absolute { - format!("/{normalized}") - } else { - normalized - } - } - } - } + let normalized = components.join(self.primary_separator()); + if is_absolute && normalized.is_empty() { + "/".to_string() + } else if is_absolute { + format!("/{normalized}") + } else { + normalized + } + } + } + } - pub fn split(self, path_like: &str) -> (Option<&str>, &str) { - let Some(pos) = path_like.rfind(self.primary_separator()) else { - return (None, path_like); - }; - let filename_start = pos + self.primary_separator().len(); - ( - Some(&path_like[..filename_start]), - &path_like[filename_start..], - ) - } + pub fn split(self, path_like: &str) -> (Option<&str>, &str) { + let Some(pos) = path_like.rfind(self.primary_separator()) else { + return (None, path_like); + }; + let filename_start = pos + self.primary_separator().len(); + ( + Some(&path_like[..filename_start]), + &path_like[filename_start..], + ) + } - pub fn strip_prefix<'a>( - &self, - child: &'a Path, - parent: &'a Path, - ) -> Option> { - let parent = parent.to_str()?; - if parent.is_empty() { - return RelPath::new(child, *self).ok(); - } - let parent = self - .separators() - .iter() - .find_map(|sep| parent.strip_suffix(sep)) - .unwrap_or(parent); - let child = child.to_str()?; + pub fn strip_prefix<'a>( + &self, + child: &'a Path, + parent: &'a Path, + ) -> Option> { + let parent = parent.to_str()?; + if parent.is_empty() { + return RelPath::new(child, *self).ok(); + } + let parent = self + .separators() + .iter() + .find_map(|sep| parent.strip_suffix(sep)) + .unwrap_or(parent); + let child = child.to_str()?; - // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:") - let stripped = if self.is_windows() - && child.as_bytes().get(1) == Some(&b':') - && parent.as_bytes().get(1) == Some(&b':') - && child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0]) - { - child[2..].strip_prefix(&parent[2..])? - } else { - child.strip_prefix(parent)? - }; - if let Some(relative) = self - .separators() - .iter() - .find_map(|sep| stripped.strip_prefix(sep)) - { - RelPath::new(relative.as_ref(), *self).ok() - } else if stripped.is_empty() { - Some(Cow::Borrowed(RelPath::empty())) - } else { - None - } - } + // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:") + let stripped = if self.is_windows() + && child.as_bytes().get(1) == Some(&b':') + && parent.as_bytes().get(1) == Some(&b':') + && child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0]) + { + child[2..].strip_prefix(&parent[2..])? + } else { + child.strip_prefix(parent)? + }; + if let Some(relative) = self + .separators() + .iter() + .find_map(|sep| stripped.strip_prefix(sep)) + { + RelPath::new(relative.as_ref(), *self).ok() + } else if stripped.is_empty() { + Some(Cow::Borrowed(RelPath::empty())) + } else { + None + } + } } #[derive(Debug, Clone)] pub struct RemotePathBuf { - style: PathStyle, - string: String, + style: PathStyle, + string: String, } impl RemotePathBuf { - pub fn new(string: String, style: PathStyle) -> Self { - Self { style, string } - } + pub fn new(string: String, style: PathStyle) -> Self { + Self { style, string } + } - pub fn from_str(path: &str, style: PathStyle) -> Self { - Self::new(path.to_string(), style) - } + pub fn from_str(path: &str, style: PathStyle) -> Self { + Self::new(path.to_string(), style) + } - pub fn path_style(&self) -> PathStyle { - self.style - } + pub fn path_style(&self) -> PathStyle { + self.style + } - pub fn to_proto(&self) -> String { - self.string.clone() - } + pub fn to_proto(&self) -> String { + self.string.clone() + } } impl Display for RemotePathBuf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.string) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) + } } pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool { - path_like.starts_with('/') - || path_style == PathStyle::Windows - && (path_like.starts_with('\\') - || path_like - .chars() - .next() - .is_some_and(|c| c.is_ascii_alphabetic()) - && path_like[1..] - .strip_prefix(':') - .is_some_and(|path| path.starts_with('/') || path.starts_with('\\'))) + path_like.starts_with('/') + || path_style == PathStyle::Windows + && (path_like.starts_with('\\') + || path_like + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic()) + && path_like[1..] + .strip_prefix(':') + .is_some_and(|path| path.starts_with('/') || path.starts_with('\\'))) } #[derive(Debug, PartialEq)] @@ -588,9 +588,9 @@ pub struct NormalizeError; impl Error for NormalizeError {} impl std::fmt::Display for NormalizeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("parent reference `..` points outside of base directory") - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("parent reference `..` points outside of base directory") + } } /// Copied from stdlib where it's unstable. @@ -610,51 +610,51 @@ impl std::fmt::Display for NormalizeError { /// [`path::absolute`](absolute) is an alternative that preserves `..`. /// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem. pub fn normalize_lexically(path: &Path) -> Result { - use std::path::Component; + use std::path::Component; - let mut lexical = PathBuf::new(); - let mut iter = path.components().peekable(); + let mut lexical = PathBuf::new(); + let mut iter = path.components().peekable(); - // Find the root, if any, and add it to the lexical path. - // Here we treat the Windows path "C:\" as a single "root" even though - // `components` splits it into two: (Prefix, RootDir). - let root = match iter.peek() { - Some(Component::ParentDir) => return Err(NormalizeError), - Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => { - lexical.push(p); - iter.next(); - lexical.as_os_str().len() - } - Some(Component::Prefix(prefix)) => { - lexical.push(prefix.as_os_str()); - iter.next(); - if let Some(p @ Component::RootDir) = iter.peek() { - lexical.push(p); - iter.next(); - } - lexical.as_os_str().len() - } - None => return Ok(PathBuf::new()), - Some(Component::Normal(_)) => 0, - }; + // Find the root, if any, and add it to the lexical path. + // Here we treat the Windows path "C:\" as a single "root" even though + // `components` splits it into two: (Prefix, RootDir). + let root = match iter.peek() { + Some(Component::ParentDir) => return Err(NormalizeError), + Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => { + lexical.push(p); + iter.next(); + lexical.as_os_str().len() + } + Some(Component::Prefix(prefix)) => { + lexical.push(prefix.as_os_str()); + iter.next(); + if let Some(p @ Component::RootDir) = iter.peek() { + lexical.push(p); + iter.next(); + } + lexical.as_os_str().len() + } + None => return Ok(PathBuf::new()), + Some(Component::Normal(_)) => 0, + }; - for component in iter { - match component { - Component::RootDir => unreachable!(), - Component::Prefix(_) => return Err(NormalizeError), - Component::CurDir => continue, - Component::ParentDir => { - // It's an error if ParentDir causes us to go above the "root". - if lexical.as_os_str().len() == root { - return Err(NormalizeError); - } else { - lexical.pop(); - } - } - Component::Normal(path) => lexical.push(path), - } - } - Ok(lexical) + for component in iter { + match component { + Component::RootDir => unreachable!(), + Component::Prefix(_) => return Err(NormalizeError), + Component::CurDir => continue, + Component::ParentDir => { + // It's an error if ParentDir causes us to go above the "root". + if lexical.as_os_str().len() == root { + return Err(NormalizeError); + } else { + lexical.pop(); + } + } + Component::Normal(path) => lexical.push(path), + } + } + Ok(lexical) } /// A delimiter to use in `path_query:row_number:column_number` strings parsing. @@ -686,319 +686,324 @@ const ROW_COL_CAPTURE_REGEX: &str = r"(?xs) /// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct PathWithPosition { - pub path: PathBuf, - pub row: Option, - // Absent if row is absent. - pub column: Option, + pub path: PathBuf, + pub row: Option, + // Absent if row is absent. + pub column: Option, } impl PathWithPosition { - /// Returns a PathWithPosition from a path. - pub fn from_path(path: PathBuf) -> Self { - Self { - path, - row: None, - column: None, - } - } + /// Returns a PathWithPosition from a path. + pub fn from_path(path: PathBuf) -> Self { + Self { + path, + row: None, + column: None, + } + } - /// Parses a string that possibly has `:row:column` or `(row, column)` suffix. - /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools - /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. - /// If the suffix parsing fails, the whole string is parsed as a path. - /// - /// Be mindful that `test_file:10:1:` is a valid posix filename. - /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename. - /// - /// # Examples - /// - /// ``` - /// # use util::paths::PathWithPosition; - /// # use std::path::PathBuf; - /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition { - /// path: PathBuf::from("test_file"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition { - /// path: PathBuf::from("test_file"), - /// row: Some(10), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: Some(2), - /// }); - /// ``` - /// - /// # Expected parsing results when encounter ill-formatted inputs. - /// ``` - /// # use util::paths::PathWithPosition; - /// # use std::path::PathBuf; - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:a"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:a:b"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: Some(2), - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:1"), - /// row: Some(2), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:1"), - /// row: Some(2), - /// column: Some(3), - /// }); - /// ``` - pub fn parse_str(s: &str) -> Self { - let trimmed = s.trim(); - let path = Path::new(trimmed); - let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str() - else { - return Self { - path: Path::new(s).to_path_buf(), - row: None, - column: None, - }; - }; - if maybe_file_name_with_row_col.is_empty() { - return Self { - path: Path::new(s).to_path_buf(), - row: None, - column: None, - }; - } + /// Parses a string that possibly has `:row:column` or `(row, column)` suffix. + /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools + /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. + /// If the suffix parsing fails, the whole string is parsed as a path. + /// + /// Be mindful that `test_file:10:1:` is a valid posix filename. + /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename. + /// + /// # Examples + /// + /// ``` + /// # use util::paths::PathWithPosition; + /// # use std::path::PathBuf; + /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition { + /// path: PathBuf::from("test_file"), + /// row: None, + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition { + /// path: PathBuf::from("test_file"), + /// row: Some(10), + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: None, + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: Some(1), + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: Some(1), + /// column: Some(2), + /// }); + /// ``` + /// + /// # Expected parsing results when encounter ill-formatted inputs. + /// ``` + /// # use util::paths::PathWithPosition; + /// # use std::path::PathBuf; + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition { + /// path: PathBuf::from("test_file.rs:a"), + /// row: None, + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition { + /// path: PathBuf::from("test_file.rs:a:b"), + /// row: None, + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: None, + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: Some(1), + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: Some(1), + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition { + /// path: PathBuf::from("test_file.rs"), + /// row: Some(1), + /// column: Some(2), + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition { + /// path: PathBuf::from("test_file.rs:1"), + /// row: Some(2), + /// column: None, + /// }); + /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition { + /// path: PathBuf::from("test_file.rs:1"), + /// row: Some(2), + /// column: Some(3), + /// }); + /// ``` + pub fn parse_str(s: &str) -> Self { + let trimmed = s.trim(); + let path = Path::new(trimmed); + let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str() + else { + return Self { + path: Path::new(s).to_path_buf(), + row: None, + column: None, + }; + }; + if maybe_file_name_with_row_col.is_empty() { + return Self { + path: Path::new(s).to_path_buf(), + row: None, + column: None, + }; + } - // Let's avoid repeated init cost on this. It is subject to thread contention, but - // so far this code isn't called from multiple hot paths. Getting contention here - // in the future seems unlikely. - static SUFFIX_RE: LazyLock = - LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap()); - match SUFFIX_RE - .captures(maybe_file_name_with_row_col) - .map(|caps| caps.extract()) - { - Some((_, [file_name, maybe_row, maybe_column])) => { - let row = maybe_row.parse::().ok(); - let column = maybe_column.parse::().ok(); + // Let's avoid repeated init cost on this. It is subject to thread contention, but + // so far this code isn't called from multiple hot paths. Getting contention here + // in the future seems unlikely. + static SUFFIX_RE: LazyLock = + LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap()); + match SUFFIX_RE + .captures(maybe_file_name_with_row_col) + .map(|caps| caps.extract()) + { + Some((_, [file_name, maybe_row, maybe_column])) => { + let row = maybe_row.parse::().ok(); + let column = maybe_column.parse::().ok(); - let (_, suffix) = trimmed.split_once(file_name).unwrap(); - let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()]; + let (_, suffix) = trimmed.split_once(file_name).unwrap(); + let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()]; - Self { - path: Path::new(path_without_suffix).to_path_buf(), - row, - column, - } - } - None => { - // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only, - // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too. - // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead. - let delimiter = ':'; - let mut path_parts = s - .rsplitn(3, delimiter) - .collect::>() - .into_iter() - .rev() - .fuse(); - let mut path_string = path_parts.next().expect("rsplitn should have the rest of the string as its last parameter that we reversed").to_owned(); - let mut row = None; - let mut column = None; - if let Some(maybe_row) = path_parts.next() { - if let Ok(parsed_row) = maybe_row.parse::() { - row = Some(parsed_row); - if let Some(parsed_column) = path_parts - .next() - .and_then(|maybe_col| maybe_col.parse::().ok()) - { - column = Some(parsed_column); - } - } else { - path_string.push(delimiter); - path_string.push_str(maybe_row); - } - } - for split in path_parts { - path_string.push(delimiter); - path_string.push_str(split); - } + Self { + path: Path::new(path_without_suffix).to_path_buf(), + row, + column, + } + } + None => { + // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only, + // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too. + // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead. + let delimiter = ':'; + let mut path_parts = s + .rsplitn(3, delimiter) + .collect::>() + .into_iter() + .rev() + .fuse(); + let mut path_string = path_parts + .next() + .expect( + "rsplitn should have the rest of the string as its last parameter that we reversed", + ) + .to_owned(); + let mut row = None; + let mut column = None; + if let Some(maybe_row) = path_parts.next() { + if let Ok(parsed_row) = maybe_row.parse::() { + row = Some(parsed_row); + if let Some(parsed_column) = path_parts + .next() + .and_then(|maybe_col| maybe_col.parse::().ok()) + { + column = Some(parsed_column); + } + } else { + path_string.push(delimiter); + path_string.push_str(maybe_row); + } + } + for split in path_parts { + path_string.push(delimiter); + path_string.push_str(split); + } - Self { - path: PathBuf::from(path_string), - row, - column, - } - } - } - } + Self { + path: PathBuf::from(path_string), + row, + column, + } + } + } + } - pub fn map_path( - self, - mapping: impl FnOnce(PathBuf) -> Result, - ) -> Result { - Ok(PathWithPosition { - path: mapping(self.path)?, - row: self.row, - column: self.column, - }) - } + pub fn map_path( + self, + mapping: impl FnOnce(PathBuf) -> Result, + ) -> Result { + Ok(PathWithPosition { + path: mapping(self.path)?, + row: self.row, + column: self.column, + }) + } - pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String { - let path_string = path_to_string(&self.path); - if let Some(row) = self.row { - if let Some(column) = self.column { - format!("{path_string}:{row}:{column}") - } else { - format!("{path_string}:{row}") - } - } else { - path_string - } - } + pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String { + let path_string = path_to_string(&self.path); + if let Some(row) = self.row { + if let Some(column) = self.column { + format!("{path_string}:{row}:{column}") + } else { + format!("{path_string}:{row}") + } + } else { + path_string + } + } } #[derive(Clone)] pub struct PathMatcher { - sources: Vec<(String, RelPathBuf, /*trailing separator*/ bool)>, - glob: GlobSet, - path_style: PathStyle, + sources: Vec<(String, RelPathBuf, /*trailing separator*/ bool)>, + glob: GlobSet, + path_style: PathStyle, } impl std::fmt::Debug for PathMatcher { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PathMatcher") - .field("sources", &self.sources) - .field("path_style", &self.path_style) - .finish() - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PathMatcher") + .field("sources", &self.sources) + .field("path_style", &self.path_style) + .finish() + } } impl PartialEq for PathMatcher { - fn eq(&self, other: &Self) -> bool { - self.sources.eq(&other.sources) - } + fn eq(&self, other: &Self) -> bool { + self.sources.eq(&other.sources) + } } impl Eq for PathMatcher {} impl PathMatcher { - pub fn new( - globs: impl IntoIterator>, - path_style: PathStyle, - ) -> Result { - let globs = globs - .into_iter() - .map(|as_str| { - GlobBuilder::new(as_str.as_ref()) - .backslash_escape(path_style.is_posix()) - .build() - }) - .collect::, _>>()?; - let sources = globs - .iter() - .filter_map(|glob| { - let glob = glob.glob(); - Some(( - glob.to_string(), - RelPath::new(glob.as_ref(), path_style) - .ok() - .map(std::borrow::Cow::into_owned)?, - glob.ends_with(path_style.separators_ch()), - )) - }) - .collect(); - let mut glob_builder = GlobSetBuilder::new(); - for single_glob in globs { - glob_builder.add(single_glob); - } - let glob = glob_builder.build()?; - Ok(PathMatcher { - glob, - sources, - path_style, - }) - } + pub fn new( + globs: impl IntoIterator>, + path_style: PathStyle, + ) -> Result { + let globs = globs + .into_iter() + .map(|as_str| { + GlobBuilder::new(as_str.as_ref()) + .backslash_escape(path_style.is_posix()) + .build() + }) + .collect::, _>>()?; + let sources = globs + .iter() + .filter_map(|glob| { + let glob = glob.glob(); + Some(( + glob.to_string(), + RelPath::new(glob.as_ref(), path_style) + .ok() + .map(std::borrow::Cow::into_owned)?, + glob.ends_with(path_style.separators_ch()), + )) + }) + .collect(); + let mut glob_builder = GlobSetBuilder::new(); + for single_glob in globs { + glob_builder.add(single_glob); + } + let glob = glob_builder.build()?; + Ok(PathMatcher { + glob, + sources, + path_style, + }) + } - pub fn sources(&self) -> impl Iterator + Clone { - self.sources.iter().map(|(source, ..)| source.as_str()) - } + pub fn sources(&self) -> impl Iterator + Clone { + self.sources.iter().map(|(source, ..)| source.as_str()) + } - pub fn is_match>(&self, other: P) -> bool { - let other = other.as_ref(); - if self - .sources - .iter() - .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source)) - { - return true; - } - let other_path = other.display(self.path_style); + pub fn is_match>(&self, other: P) -> bool { + let other = other.as_ref(); + if self + .sources + .iter() + .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source)) + { + return true; + } + let other_path = other.display(self.path_style); - if self.glob.is_match(&*other_path) { - return true; - } + if self.glob.is_match(&*other_path) { + return true; + } - self.glob - .is_match(other_path.into_owned() + self.path_style.primary_separator()) - } + self.glob + .is_match(other_path.into_owned() + self.path_style.primary_separator()) + } - pub fn is_match_std_path>(&self, other: P) -> bool { - let other = other.as_ref(); - if self.sources.iter().any(|(_, source, _)| { - other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path()) - }) { - return true; - } - self.glob.is_match(other) - } + pub fn is_match_std_path>(&self, other: P) -> bool { + let other = other.as_ref(); + if self.sources.iter().any(|(_, source, _)| { + other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path()) + }) { + return true; + } + self.glob.is_match(other) + } } impl Default for PathMatcher { - fn default() -> Self { - Self { - path_style: PathStyle::local(), - glob: GlobSet::empty(), - sources: vec![], - } - } + fn default() -> Self { + Self { + path_style: PathStyle::local(), + glob: GlobSet::empty(), + sources: vec![], + } + } } /// Compares two sequences of consecutive digits for natural sorting. @@ -1036,58 +1041,58 @@ impl Default for PathMatcher { /// The function advances both iterators past their respective numeric sequences, /// regardless of the comparison result. fn compare_numeric_segments( - a_iter: &mut std::iter::Peekable, - b_iter: &mut std::iter::Peekable, + a_iter: &mut std::iter::Peekable, + b_iter: &mut std::iter::Peekable, ) -> Ordering where - I: Iterator, + I: Iterator, { - // Collect all consecutive digits into strings - let mut a_num_str = String::new(); - let mut b_num_str = String::new(); + // Collect all consecutive digits into strings + let mut a_num_str = String::new(); + let mut b_num_str = String::new(); - while let Some(&c) = a_iter.peek() { - if !c.is_ascii_digit() { - break; - } + while let Some(&c) = a_iter.peek() { + if !c.is_ascii_digit() { + break; + } - a_num_str.push(c); - a_iter.next(); - } + a_num_str.push(c); + a_iter.next(); + } - while let Some(&c) = b_iter.peek() { - if !c.is_ascii_digit() { - break; - } + while let Some(&c) = b_iter.peek() { + if !c.is_ascii_digit() { + break; + } - b_num_str.push(c); - b_iter.next(); - } + b_num_str.push(c); + b_iter.next(); + } - // First compare lengths (handle leading zeros) - match a_num_str.len().cmp(&b_num_str.len()) { - Ordering::Equal => { - // Same length, compare digit by digit - match a_num_str.cmp(&b_num_str) { - Ordering::Equal => Ordering::Equal, - ordering => ordering, - } - } + // First compare lengths (handle leading zeros) + match a_num_str.len().cmp(&b_num_str.len()) { + Ordering::Equal => { + // Same length, compare digit by digit + match a_num_str.cmp(&b_num_str) { + Ordering::Equal => Ordering::Equal, + ordering => ordering, + } + } - // Different lengths but same value means leading zeros - ordering => { - // Try parsing as numbers first - if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::(), b_num_str.parse::()) { - match a_val.cmp(&b_val) { - Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros) - ord => ord, - } - } else { - // If parsing fails (overflow), compare as strings - a_num_str.cmp(&b_num_str) - } - } - } + // Different lengths but same value means leading zeros + ordering => { + // Try parsing as numbers first + if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::(), b_num_str.parse::()) { + match a_val.cmp(&b_val) { + Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros) + ord => ord, + } + } else { + // If parsing fails (overflow), compare as strings + a_num_str.cmp(&b_num_str) + } + } + } } /// Performs natural sorting comparison between two strings. @@ -1112,369 +1117,369 @@ where /// 4. For non-numeric characters, using case-insensitive comparison /// 5. If everything is equal case-insensitively, using case-sensitive comparison as final tie-breaker pub fn natural_sort(a: &str, b: &str) -> Ordering { - let mut a_iter = a.chars().peekable(); - let mut b_iter = b.chars().peekable(); + let mut a_iter = a.chars().peekable(); + let mut b_iter = b.chars().peekable(); - loop { - match (a_iter.peek(), b_iter.peek()) { - (None, None) => { - return b.cmp(a); - } - (None, _) => return Ordering::Less, - (_, None) => return Ordering::Greater, - (Some(&a_char), Some(&b_char)) => { - if a_char.is_ascii_digit() && b_char.is_ascii_digit() { - match compare_numeric_segments(&mut a_iter, &mut b_iter) { - Ordering::Equal => continue, - ordering => return ordering, - } - } else { - match a_char - .to_ascii_lowercase() - .cmp(&b_char.to_ascii_lowercase()) - { - Ordering::Equal => { - a_iter.next(); - b_iter.next(); - } - ordering => return ordering, - } - } - } - } - } + loop { + match (a_iter.peek(), b_iter.peek()) { + (None, None) => { + return b.cmp(a); + } + (None, _) => return Ordering::Less, + (_, None) => return Ordering::Greater, + (Some(&a_char), Some(&b_char)) => { + if a_char.is_ascii_digit() && b_char.is_ascii_digit() { + match compare_numeric_segments(&mut a_iter, &mut b_iter) { + Ordering::Equal => continue, + ordering => return ordering, + } + } else { + match a_char + .to_ascii_lowercase() + .cmp(&b_char.to_ascii_lowercase()) + { + Ordering::Equal => { + a_iter.next(); + b_iter.next(); + } + ordering => return ordering, + } + } + } + } + } } /// Case-insensitive natural sort without applying the final lowercase/uppercase tie-breaker. /// This is useful when comparing individual path components where we want to keep walking /// deeper components before deciding on casing. fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering { - if a.eq_ignore_ascii_case(b) { - Ordering::Equal - } else { - natural_sort(a, b) - } + if a.eq_ignore_ascii_case(b) { + Ordering::Equal + } else { + natural_sort(a, b) + } } fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) { - if filename.is_empty() { - return (None, None); - } + if filename.is_empty() { + return (None, None); + } - match filename.rsplit_once('.') { - // Case 1: No dot was found. The entire name is the stem. - None => (Some(filename), None), + match filename.rsplit_once('.') { + // Case 1: No dot was found. The entire name is the stem. + None => (Some(filename), None), - // Case 2: A dot was found. - Some((before, after)) => { - // This is the crucial check for dotfiles like ".bashrc". - // If `before` is empty, the dot was the first character. - // In that case, we revert to the "whole name is the stem" logic. - if before.is_empty() { - (Some(filename), None) - } else { - // Otherwise, we have a standard stem and extension. - (Some(before), Some(after)) - } - } - } + // Case 2: A dot was found. + Some((before, after)) => { + // This is the crucial check for dotfiles like ".bashrc". + // If `before` is empty, the dot was the first character. + // In that case, we revert to the "whole name is the stem" logic. + if before.is_empty() { + (Some(filename), None) + } else { + // Otherwise, we have a standard stem and extension. + (Some(before), Some(after)) + } + } + } } /// Controls the lexicographic sorting of file and folder names. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum SortOrder { - /// Case-insensitive natural sort with lowercase preferred in ties. - /// Numbers in file names are compared by value (e.g., `file2` before `file10`). - #[default] - Default, - /// Uppercase names are grouped before lowercase names, with case-insensitive - /// natural sort within each group. Dot-prefixed names sort before both groups. - Upper, - /// Lowercase names are grouped before uppercase names, with case-insensitive - /// natural sort within each group. Dot-prefixed names sort before both groups. - Lower, - /// Pure Unicode codepoint comparison. No case folding, no natural number sorting. - /// Uppercase ASCII sorts before lowercase. Accented characters sort after ASCII. - Unicode, + /// Case-insensitive natural sort with lowercase preferred in ties. + /// Numbers in file names are compared by value (e.g., `file2` before `file10`). + #[default] + Default, + /// Uppercase names are grouped before lowercase names, with case-insensitive + /// natural sort within each group. Dot-prefixed names sort before both groups. + Upper, + /// Lowercase names are grouped before uppercase names, with case-insensitive + /// natural sort within each group. Dot-prefixed names sort before both groups. + Lower, + /// Pure Unicode codepoint comparison. No case folding, no natural number sorting. + /// Uppercase ASCII sorts before lowercase. Accented characters sort after ASCII. + Unicode, } /// Controls how files and directories are ordered relative to each other. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum SortMode { - /// Directories are listed before files at each level. - #[default] - DirectoriesFirst, - /// Files and directories are interleaved alphabetically. - Mixed, - /// Files are listed before directories at each level. - FilesFirst, + /// Directories are listed before files at each level. + #[default] + DirectoriesFirst, + /// Files and directories are interleaved alphabetically. + Mixed, + /// Files are listed before directories at each level. + FilesFirst, } fn case_group_key(name: &str, order: SortOrder) -> u8 { - let first = match name.chars().next() { - Some(c) => c, - None => return 0, - }; - match order { - SortOrder::Upper if first.is_lowercase() => 1, - SortOrder::Upper => 0, - SortOrder::Lower if first.is_uppercase() => 1, - SortOrder::Lower => 0, - _ => 0, - } + let first = match name.chars().next() { + Some(c) => c, + None => return 0, + }; + match order { + SortOrder::Upper if first.is_lowercase() => 1, + SortOrder::Upper => 0, + SortOrder::Lower if first.is_uppercase() => 1, + SortOrder::Lower => 0, + _ => 0, + } } fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering { - match order { - SortOrder::Unicode => a.cmp(b), - _ => natural_sort(a, b), - } + match order { + SortOrder::Unicode => a.cmp(b), + _ => natural_sort(a, b), + } } fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering { - match order { - SortOrder::Unicode => a.cmp(b), - _ => natural_sort_no_tiebreak(a, b), - } + match order { + SortOrder::Unicode => a.cmp(b), + _ => natural_sort_no_tiebreak(a, b), + } } pub fn compare_rel_paths( - (path_a, a_is_file): (&RelPath, bool), - (path_b, b_is_file): (&RelPath, bool), + (path_a, a_is_file): (&RelPath, bool), + (path_b, b_is_file): (&RelPath, bool), ) -> Ordering { - compare_rel_paths_by( - (path_a, a_is_file), - (path_b, b_is_file), - SortMode::DirectoriesFirst, - SortOrder::Default, - ) + compare_rel_paths_by( + (path_a, a_is_file), + (path_b, b_is_file), + SortMode::DirectoriesFirst, + SortOrder::Default, + ) } pub fn compare_rel_paths_by( - (path_a, a_is_file): (&RelPath, bool), - (path_b, b_is_file): (&RelPath, bool), - mode: SortMode, - order: SortOrder, + (path_a, a_is_file): (&RelPath, bool), + (path_b, b_is_file): (&RelPath, bool), + mode: SortMode, + order: SortOrder, ) -> Ordering { - let needs_final_tiebreak = - mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b); + let needs_final_tiebreak = + mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b); - let mut components_a = path_a.components(); - let mut components_b = path_b.components(); + let mut components_a = path_a.components(); + let mut components_b = path_b.components(); - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_leaf_file = a_is_file && components_a.rest().is_empty(); - let b_leaf_file = b_is_file && components_b.rest().is_empty(); + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_leaf_file = a_is_file && components_a.rest().is_empty(); + let b_leaf_file = b_is_file && components_b.rest().is_empty(); - let file_dir_ordering = match mode { - SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file), - SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file), - SortMode::Mixed => Ordering::Equal, - }; + let file_dir_ordering = match mode { + SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file), + SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file), + SortMode::Mixed => Ordering::Equal, + }; - if !file_dir_ordering.is_eq() { - return file_dir_ordering; - } + if !file_dir_ordering.is_eq() { + return file_dir_ordering; + } - let (a_stem, a_ext) = if a_leaf_file { - stem_and_extension(component_a) - } else { - Default::default() - }; - let (b_stem, b_ext) = if b_leaf_file { - stem_and_extension(component_b) - } else { - Default::default() - }; - let a_key = if a_leaf_file { - a_stem - } else { - Some(component_a) - }; - let b_key = if b_leaf_file { - b_stem - } else { - Some(component_b) - }; + let (a_stem, a_ext) = if a_leaf_file { + stem_and_extension(component_a) + } else { + Default::default() + }; + let (b_stem, b_ext) = if b_leaf_file { + stem_and_extension(component_b) + } else { + Default::default() + }; + let a_key = if a_leaf_file { + a_stem + } else { + Some(component_a) + }; + let b_key = if b_leaf_file { + b_stem + } else { + Some(component_b) + }; - let ordering = match (a_key, b_key) { - (Some(a), Some(b)) => { - let name_cmp = case_group_key(a, order) - .cmp(&case_group_key(b, order)) - .then_with(|| match mode { - SortMode::DirectoriesFirst => compare_strings(a, b, order), - _ => compare_strings_no_tiebreak(a, b, order), - }); + let ordering = match (a_key, b_key) { + (Some(a), Some(b)) => { + let name_cmp = case_group_key(a, order) + .cmp(&case_group_key(b, order)) + .then_with(|| match mode { + SortMode::DirectoriesFirst => compare_strings(a, b, order), + _ => compare_strings_no_tiebreak(a, b, order), + }); - let name_cmp = if mode == SortMode::Mixed { - name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) { - (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater, - (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less, - _ => Ordering::Equal, - }) - } else { - name_cmp - }; + let name_cmp = if mode == SortMode::Mixed { + name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) { + (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater, + (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less, + _ => Ordering::Equal, + }) + } else { + name_cmp + }; - name_cmp.then_with(|| { - if a_leaf_file && b_leaf_file { - match order { - SortOrder::Unicode => { - a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default()) - } - _ => { - let a_ext_str = a_ext.unwrap_or_default().to_lowercase(); - let b_ext_str = b_ext.unwrap_or_default().to_lowercase(); - a_ext_str.cmp(&b_ext_str) - } - } - } else { - Ordering::Equal - } - }) - } - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; + name_cmp.then_with(|| { + if a_leaf_file && b_leaf_file { + match order { + SortOrder::Unicode => { + a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default()) + } + _ => { + let a_ext_str = a_ext.unwrap_or_default().to_lowercase(); + let b_ext_str = b_ext.unwrap_or_default().to_lowercase(); + a_ext_str.cmp(&b_ext_str) + } + } + } else { + Ordering::Equal + } + }) + } + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }; - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => return Ordering::Greater, - (None, Some(_)) => return Ordering::Less, - (None, None) => { - if needs_final_tiebreak { - return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order); - } - return Ordering::Equal; - } - } - } + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => return Ordering::Greater, + (None, Some(_)) => return Ordering::Less, + (None, None) => { + if needs_final_tiebreak { + return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order); + } + return Ordering::Equal; + } + } + } } pub fn compare_paths( - (path_a, a_is_file): (&Path, bool), - (path_b, b_is_file): (&Path, bool), + (path_a, a_is_file): (&Path, bool), + (path_b, b_is_file): (&Path, bool), ) -> Ordering { - let mut components_a = path_a.components().peekable(); - let mut components_b = path_b.components().peekable(); + let mut components_a = path_a.components().peekable(); + let mut components_b = path_b.components().peekable(); - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_is_file = components_a.peek().is_none() && a_is_file; - let b_is_file = components_b.peek().is_none() && b_is_file; + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_is_file = components_a.peek().is_none() && a_is_file; + let b_is_file = components_b.peek().is_none() && b_is_file; - let ordering = a_is_file.cmp(&b_is_file).then_with(|| { - let path_a = Path::new(component_a.as_os_str()); - let path_string_a = if a_is_file { - path_a.file_stem() - } else { - path_a.file_name() - } - .map(|s| s.to_string_lossy()); + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { + let path_a = Path::new(component_a.as_os_str()); + let path_string_a = if a_is_file { + path_a.file_stem() + } else { + path_a.file_name() + } + .map(|s| s.to_string_lossy()); - let path_b = Path::new(component_b.as_os_str()); - let path_string_b = if b_is_file { - path_b.file_stem() - } else { - path_b.file_name() - } - .map(|s| s.to_string_lossy()); + let path_b = Path::new(component_b.as_os_str()); + let path_string_b = if b_is_file { + path_b.file_stem() + } else { + path_b.file_name() + } + .map(|s| s.to_string_lossy()); - let compare_components = match (path_string_a, path_string_b) { - (Some(a), Some(b)) => natural_sort(&a, &b), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; + let compare_components = match (path_string_a, path_string_b) { + (Some(a), Some(b)) => natural_sort(&a, &b), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }; - compare_components.then_with(|| { - if a_is_file && b_is_file { - let ext_a = path_a.extension().unwrap_or_default(); - let ext_b = path_b.extension().unwrap_or_default(); - ext_a.cmp(ext_b) - } else { - Ordering::Equal - } - }) - }); + compare_components.then_with(|| { + if a_is_file && b_is_file { + let ext_a = path_a.extension().unwrap_or_default(); + let ext_b = path_b.extension().unwrap_or_default(); + ext_a.cmp(ext_b) + } else { + Ordering::Equal + } + }) + }); - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => break Ordering::Greater, - (None, Some(_)) => break Ordering::Less, - (None, None) => break Ordering::Equal, - } - } + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WslPath { - pub distro: String, + pub distro: String, - // the reason this is an OsString and not any of the path types is that it needs to - // represent a unix path (with '/' separators) on windows. `from_path` does this by - // manually constructing it from the path components of a given windows path. - pub path: std::ffi::OsString, + // the reason this is an OsString and not any of the path types is that it needs to + // represent a unix path (with '/' separators) on windows. `from_path` does this by + // manually constructing it from the path components of a given windows path. + pub path: std::ffi::OsString, } impl WslPath { - pub fn from_path>(path: P) -> Option { - if cfg!(not(target_os = "windows")) { - return None; - } - use std::{ - ffi::OsString, - path::{Component, Prefix}, - }; + pub fn from_path>(path: P) -> Option { + if cfg!(not(target_os = "windows")) { + return None; + } + use std::{ + ffi::OsString, + path::{Component, Prefix}, + }; - let mut components = path.as_ref().components(); - let Some(Component::Prefix(prefix)) = components.next() else { - return None; - }; - let (server, distro) = match prefix.kind() { - Prefix::UNC(server, distro) => (server, distro), - Prefix::VerbatimUNC(server, distro) => (server, distro), - _ => return None, - }; - let Some(Component::RootDir) = components.next() else { - return None; - }; + let mut components = path.as_ref().components(); + let Some(Component::Prefix(prefix)) = components.next() else { + return None; + }; + let (server, distro) = match prefix.kind() { + Prefix::UNC(server, distro) => (server, distro), + Prefix::VerbatimUNC(server, distro) => (server, distro), + _ => return None, + }; + let Some(Component::RootDir) = components.next() else { + return None; + }; - let server_str = server.to_string_lossy(); - if server_str == "wsl.localhost" || server_str == "wsl$" { - let mut result = OsString::from(""); - for c in components { - use Component::*; - match c { - Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"), - RootDir => unreachable!("got root dir, but already stripped root"), - CurDir => continue, - ParentDir => result.push("/.."), - Normal(s) => { - result.push("/"); - result.push(s); - } - } - } - if result.is_empty() { - result.push("/"); - } - Some(WslPath { - distro: distro.to_string_lossy().to_string(), - path: result, - }) - } else { - None - } - } + let server_str = server.to_string_lossy(); + if server_str == "wsl.localhost" || server_str == "wsl$" { + let mut result = OsString::from(""); + for c in components { + use Component::*; + match c { + Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"), + RootDir => unreachable!("got root dir, but already stripped root"), + CurDir => continue, + ParentDir => result.push("/.."), + Normal(s) => { + result.push("/"); + result.push(s); + } + } + } + if result.is_empty() { + result.push("/"); + } + Some(WslPath { + distro: distro.to_string_lossy().to_string(), + path: result, + }) + } else { + None + } + } } /// Error returned when a [`url::Url`] cannot be converted into a [`PathBuf`] @@ -1483,2104 +1488,2103 @@ impl WslPath { pub struct ToFilePathError; impl std::fmt::Display for ToFilePathError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("failed to convert URL to file path") - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("failed to convert URL to file path") + } } impl std::error::Error for ToFilePathError {} pub trait UrlExt { - /// A version of `url::Url::to_file_path` that does platform handling based on the provided `PathStyle` instead of the host platform. - /// - /// Prefer using this over `url::Url::to_file_path` when you need to handle paths in a cross-platform way as is the case for remoting interactions. - fn to_file_path_ext(&self, path_style: PathStyle) -> Result; + /// A version of `url::Url::to_file_path` that does platform handling based on the provided `PathStyle` instead of the host platform. + /// + /// Prefer using this over `url::Url::to_file_path` when you need to handle paths in a cross-platform way as is the case for remoting interactions. + fn to_file_path_ext(&self, path_style: PathStyle) -> Result; } impl UrlExt for url::Url { - // Copied from `url::Url::to_file_path`, but the `cfg` handling is replaced with runtime branching on `PathStyle` - fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result { - if let Some(segments) = self.path_segments() { - let host = match self.host() { - None | Some(url::Host::Domain("localhost")) => None, - Some(_) if source_path_style.is_windows() && self.scheme() == "file" => { - self.host_str() - } - _ => return Err(ToFilePathError), - }; + // Copied from `url::Url::to_file_path`, but the `cfg` handling is replaced with runtime branching on `PathStyle` + fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result { + if let Some(segments) = self.path_segments() { + let host = match self.host() { + None | Some(url::Host::Domain("localhost")) => None, + Some(_) if source_path_style.is_windows() && self.scheme() == "file" => { + self.host_str() + } + _ => return Err(ToFilePathError), + }; - let str_len = self.as_str().len(); - let estimated_capacity = if source_path_style.is_windows() { - // remove scheme: - has possible \\ for hostname - str_len.saturating_sub(self.scheme().len() + 1) - } else { - // remove scheme:// - str_len.saturating_sub(self.scheme().len() + 3) - }; - return match source_path_style { - PathStyle::Posix => { - file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments) - } - PathStyle::Windows => { - file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments) - } - }; - } + let str_len = self.as_str().len(); + let estimated_capacity = if source_path_style.is_windows() { + // remove scheme: - has possible \\ for hostname + str_len.saturating_sub(self.scheme().len() + 1) + } else { + // remove scheme:// + str_len.saturating_sub(self.scheme().len() + 3) + }; + return match source_path_style { + PathStyle::Posix => { + file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments) + } + PathStyle::Windows => { + file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments) + } + }; + } - fn file_url_segments_to_pathbuf_posix( - estimated_capacity: usize, - host: Option<&str>, - segments: std::str::Split<'_, char>, - ) -> Result { - use percent_encoding::percent_decode; + fn file_url_segments_to_pathbuf_posix( + estimated_capacity: usize, + host: Option<&str>, + segments: std::str::Split<'_, char>, + ) -> Result { + use percent_encoding::percent_decode; - if host.is_some() { - return Err(ToFilePathError); - } + if host.is_some() { + return Err(ToFilePathError); + } - let mut bytes = Vec::new(); - bytes - .try_reserve(estimated_capacity) - .map_err(|_| ToFilePathError)?; + let mut bytes = Vec::new(); + bytes + .try_reserve(estimated_capacity) + .map_err(|_| ToFilePathError)?; - for segment in segments { - bytes.push(b'/'); - bytes.extend(percent_decode(segment.as_bytes())); - } + for segment in segments { + bytes.push(b'/'); + bytes.extend(percent_decode(segment.as_bytes())); + } - // A windows drive letter must end with a slash. - if bytes.len() > 2 - && bytes[bytes.len() - 2].is_ascii_alphabetic() - && matches!(bytes[bytes.len() - 1], b':' | b'|') - { - bytes.push(b'/'); - } + // A windows drive letter must end with a slash. + if bytes.len() > 2 + && bytes[bytes.len() - 2].is_ascii_alphabetic() + && matches!(bytes[bytes.len() - 1], b':' | b'|') + { + bytes.push(b'/'); + } - let path = String::from_utf8(bytes).map_err(|_| ToFilePathError)?; - debug_assert!( - PathStyle::Posix.is_absolute(&path), - "to_file_path() failed to produce an absolute Path" - ); + let path = String::from_utf8(bytes).map_err(|_| ToFilePathError)?; + debug_assert!( + PathStyle::Posix.is_absolute(&path), + "to_file_path() failed to produce an absolute Path" + ); - Ok(PathBuf::from(path)) - } + Ok(PathBuf::from(path)) + } - fn file_url_segments_to_pathbuf_windows( - estimated_capacity: usize, - host: Option<&str>, - mut segments: std::str::Split<'_, char>, - ) -> Result { - use percent_encoding::percent_decode_str; - let mut string = String::new(); - string - .try_reserve(estimated_capacity) - .map_err(|_| ToFilePathError)?; - if let Some(host) = host { - string.push_str(r"\\"); - string.push_str(host); - } else { - let first = segments.next().ok_or(ToFilePathError)?; + fn file_url_segments_to_pathbuf_windows( + estimated_capacity: usize, + host: Option<&str>, + mut segments: std::str::Split<'_, char>, + ) -> Result { + use percent_encoding::percent_decode_str; + let mut string = String::new(); + string + .try_reserve(estimated_capacity) + .map_err(|_| ToFilePathError)?; + if let Some(host) = host { + string.push_str(r"\\"); + string.push_str(host); + } else { + let first = segments.next().ok_or(ToFilePathError)?; - match first.len() { - 2 => { - if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) - || first.as_bytes()[1] != b':' - { - return Err(ToFilePathError); - } + match first.len() { + 2 => { + if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) + || first.as_bytes()[1] != b':' + { + return Err(ToFilePathError); + } - string.push_str(first); - } + string.push_str(first); + } - 4 => { - if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) { - return Err(ToFilePathError); - } - let bytes = first.as_bytes(); - if bytes[1] != b'%' - || bytes[2] != b'3' - || (bytes[3] != b'a' && bytes[3] != b'A') - { - return Err(ToFilePathError); - } + 4 => { + if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) { + return Err(ToFilePathError); + } + let bytes = first.as_bytes(); + if bytes[1] != b'%' + || bytes[2] != b'3' || (bytes[3] != b'a' && bytes[3] != b'A') + { + return Err(ToFilePathError); + } - string.push_str(&first[0..1]); - string.push(':'); - } + string.push_str(&first[0..1]); + string.push(':'); + } - _ => return Err(ToFilePathError), - } - }; + _ => return Err(ToFilePathError), + } + }; - for segment in segments { - string.push('\\'); + for segment in segments { + string.push('\\'); - // Currently non-unicode windows paths cannot be represented - match percent_decode_str(segment).decode_utf8() { - Ok(s) => string.push_str(&s), - Err(..) => return Err(ToFilePathError), - } - } - // ensure our estimated capacity was good - if cfg!(test) { - debug_assert!( - string.len() <= estimated_capacity, - "len: {}, capacity: {}", - string.len(), - estimated_capacity - ); - } - debug_assert!( - PathStyle::Windows.is_absolute(&string), - "to_file_path() failed to produce an absolute Path" - ); - let path = PathBuf::from(string); - Ok(path) - } - Err(ToFilePathError) - } + // Currently non-unicode windows paths cannot be represented + match percent_decode_str(segment).decode_utf8() { + Ok(s) => string.push_str(&s), + Err(..) => return Err(ToFilePathError), + } + } + // ensure our estimated capacity was good + if cfg!(test) { + debug_assert!( + string.len() <= estimated_capacity, + "len: {}, capacity: {}", + string.len(), + estimated_capacity + ); + } + debug_assert!( + PathStyle::Windows.is_absolute(&string), + "to_file_path() failed to produce an absolute Path" + ); + let path = PathBuf::from(string); + Ok(path) + } + Err(ToFilePathError) + } } #[cfg(test)] mod tests { - use crate::rel_path::rel_path; - - use super::*; - // perf annotations replaced with #[test] - - #[test] - fn test_join_path_uses_path_style_separator() { - let posix_path = PathStyle::Posix - .join_path(Path::new("/home/user/dev"), "worktrees") - .unwrap(); - let windows_path = PathStyle::Windows - .join_path(Path::new("C:\\Users\\user\\dev"), "worktrees") - .unwrap(); - - assert_eq!(posix_path, PathBuf::from("/home/user/dev/worktrees")); - assert_eq!( - windows_path.to_string_lossy(), - "C:\\Users\\user\\dev\\worktrees" - ); - } - - #[test] - fn test_normalize_uses_path_style_separator() { - assert_eq!( - PathStyle::Posix.normalize("/home/user/dev/../worktrees/./zed"), - "/home/user/worktrees/zed" - ); - assert_eq!( - PathStyle::Windows.normalize("C:\\Users\\user\\dev\\worktrees"), - "C:\\Users\\user\\dev\\worktrees" - ); - } - - fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) { - (RelPath::unix(path).unwrap(), is_file) - } - - fn sorted_rel_paths( - mut paths: Vec<(&'static RelPath, bool)>, - mode: SortMode, - order: SortOrder, - ) -> Vec<(&'static RelPath, bool)> { - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order)); - paths - } - - #[test] - fn compare_paths_with_dots() { - let mut paths = vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.45/foo_1"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_1"), true), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), - ] - ); - let mut paths = vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), - ] - ); - } - - #[test] - fn compare_paths_with_same_name_different_extensions() { - let mut paths = vec![ - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), - ] - ); - } - - #[test] - fn compare_paths_case_semi_sensitive() { - let mut paths = vec![ - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_dirs"), false), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), - ] - ); - } - - #[test] - fn compare_paths_mixed_case_numeric_ordering() { - let mut entries = [ - (Path::new(".config"), false), - (Path::new("Dir1"), false), - (Path::new("dir01"), false), - (Path::new("dir2"), false), - (Path::new("Dir02"), false), - (Path::new("dir10"), false), - (Path::new("Dir10"), false), - ]; - - entries.sort_by(|&a, &b| compare_paths(a, b)); - - let ordered: Vec<&str> = entries - .iter() - .map(|(path, _)| path.to_str().unwrap()) - .collect(); - - assert_eq!( - ordered, - vec![ - ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10" - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_case_insensitive() { - // Test that mixed mode is case-insensitive - let mut paths = vec![ - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("aardvark.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - // Case-insensitive: aardvark < Apple < banana < Carrot < zebra - assert_eq!( - paths, - vec![ - (RelPath::unix("aardvark.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("zebra.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_basic() { - // Test that files come before directories - let mut paths = vec![ - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("aardvark.txt").unwrap(), true), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - // Files first (case-insensitive), then directories (case-insensitive) - assert_eq!( - paths, - vec![ - (RelPath::unix("aardvark.txt").unwrap(), true), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("Carrot").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_case_insensitive() { - // Test case-insensitive sorting within files and directories - let mut paths = vec![ - (RelPath::unix("Zebra.txt").unwrap(), true), - (RelPath::unix("apple").unwrap(), false), - (RelPath::unix("Banana.rs").unwrap(), true), - (RelPath::unix("carrot").unwrap(), false), - (RelPath::unix("Aardvark.txt").unwrap(), true), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("Aardvark.txt").unwrap(), true), - (RelPath::unix("Banana.rs").unwrap(), true), - (RelPath::unix("Zebra.txt").unwrap(), true), - (RelPath::unix("apple").unwrap(), false), - (RelPath::unix("carrot").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_numeric() { - // Test natural number sorting with files first - let mut paths = vec![ - (RelPath::unix("file10.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("dir10").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("file10.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("dir10").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_case() { - // Test case-insensitive sorting with varied capitalization - let mut paths = vec![ - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("readme.txt").unwrap(), true), - (RelPath::unix("ReadMe.rs").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - // All "readme" variants should group together, sorted by extension - assert_eq!( - paths, - vec![ - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("ReadMe.rs").unwrap(), true), - (RelPath::unix("readme.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_files_and_dirs() { - // Verify directories and files are still mixed - let mut paths = vec![ - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("Dir1").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - // Case-insensitive: dir1, dir2, file1, file2 (all mixed) - assert_eq!( - paths, - vec![ - (RelPath::unix("Dir1").unwrap(), false), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("file2.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() { - let mut paths = vec![ - (RelPath::unix("Hello.txt").unwrap(), true), - (RelPath::unix("hello").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("hello").unwrap(), false), - (RelPath::unix("Hello.txt").unwrap(), true), - ] - ); - - let mut paths = vec![ - (RelPath::unix("hello").unwrap(), false), - (RelPath::unix("Hello.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("hello").unwrap(), false), - (RelPath::unix("Hello.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_with_nested_paths() { - // Test that nested paths still work correctly - let mut paths = vec![ - (RelPath::unix("src/main.rs").unwrap(), true), - (RelPath::unix("Cargo.toml").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("target").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("Cargo.toml").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("src/main.rs").unwrap(), true), - (RelPath::unix("target").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_with_nested() { - // Files come before directories, even with nested paths - let mut paths = vec![ - (RelPath::unix("src/lib.rs").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("tests").unwrap(), false), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("src/lib.rs").unwrap(), true), - (RelPath::unix("tests").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_dotfiles() { - // Test that dotfiles are handled correctly in mixed mode - let mut paths = vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_dotfiles() { - // Test that dotfiles come first when they're files - let mut paths = vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_same_stem_different_extension() { - // Files with same stem but different extensions should sort by extension - let mut paths = vec![ - (RelPath::unix("file.rs").unwrap(), true), - (RelPath::unix("file.md").unwrap(), true), - (RelPath::unix("file.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("file.md").unwrap(), true), - (RelPath::unix("file.rs").unwrap(), true), - (RelPath::unix("file.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_files_first_same_stem() { - // Same stem files should still sort by extension with files_first - let mut paths = vec![ - (RelPath::unix("main.rs").unwrap(), true), - (RelPath::unix("main.c").unwrap(), true), - (RelPath::unix("main").unwrap(), false), - ]; - paths - .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("main.c").unwrap(), true), - (RelPath::unix("main.rs").unwrap(), true), - (RelPath::unix("main").unwrap(), false), - ] - ); - } - - #[test] - fn compare_rel_paths_mixed_deep_nesting() { - // Test sorting with deeply nested paths - let mut paths = vec![ - (RelPath::unix("a/b/c.txt").unwrap(), true), - (RelPath::unix("A/B.txt").unwrap(), true), - (RelPath::unix("a.txt").unwrap(), true), - (RelPath::unix("A.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); - assert_eq!( - paths, - vec![ - (RelPath::unix("a/b/c.txt").unwrap(), true), - (RelPath::unix("A/B.txt").unwrap(), true), - (RelPath::unix("a.txt").unwrap(), true), - (RelPath::unix("A.txt").unwrap(), true), - ] - ); - } - - #[test] - fn compare_rel_paths_upper() { - let directories_only_paths = vec![ - rel_path_entry("mixedCase", false), - rel_path_entry("Zebra", false), - rel_path_entry("banana", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("dog", false), - rel_path_entry(".hidden", false), - rel_path_entry("Carrot", false), - ]; - assert_eq!( - sorted_rel_paths( - directories_only_paths, - SortMode::DirectoriesFirst, - SortOrder::Upper, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("Carrot", false), - rel_path_entry("Zebra", false), - rel_path_entry("banana", false), - rel_path_entry("dog", false), - rel_path_entry("mixedCase", false), - ] - ); - - let file_and_directory_paths = vec![ - rel_path_entry("banana", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("ALLCAPS", false), - rel_path_entry("file1.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry(".hidden", false), - ]; - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::DirectoriesFirst, - SortOrder::Upper, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("banana", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::Mixed, - SortOrder::Upper, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("banana", false), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths, - SortMode::FilesFirst, - SortOrder::Upper, - ), - vec![ - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("banana", false), - ] - ); - - let natural_sort_paths = vec![ - rel_path_entry("file10.txt", true), - rel_path_entry("file1.txt", true), - rel_path_entry("file20.txt", true), - rel_path_entry("file2.txt", true), - ]; - assert_eq!( - sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,), - vec![ - rel_path_entry("file1.txt", true), - rel_path_entry("file2.txt", true), - rel_path_entry("file10.txt", true), - rel_path_entry("file20.txt", true), - ] - ); - - let accented_paths = vec![ - rel_path_entry("\u{00C9}something.txt", true), - rel_path_entry("zebra.txt", true), - rel_path_entry("Apple.txt", true), - ]; - assert_eq!( - sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper), - vec![ - rel_path_entry("Apple.txt", true), - rel_path_entry("\u{00C9}something.txt", true), - rel_path_entry("zebra.txt", true), - ] - ); - } - - #[test] - fn compare_rel_paths_lower() { - let directories_only_paths = vec![ - rel_path_entry("mixedCase", false), - rel_path_entry("Zebra", false), - rel_path_entry("banana", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("dog", false), - rel_path_entry(".hidden", false), - rel_path_entry("Carrot", false), - ]; - assert_eq!( - sorted_rel_paths( - directories_only_paths, - SortMode::DirectoriesFirst, - SortOrder::Lower, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("banana", false), - rel_path_entry("dog", false), - rel_path_entry("mixedCase", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("Carrot", false), - rel_path_entry("Zebra", false), - ] - ); - - let file_and_directory_paths = vec![ - rel_path_entry("banana", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("ALLCAPS", false), - rel_path_entry("file1.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry(".hidden", false), - ]; - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::DirectoriesFirst, - SortOrder::Lower, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("banana", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::Mixed, - SortOrder::Lower, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("banana", false), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths, - SortMode::FilesFirst, - SortOrder::Lower, - ), - vec![ - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry(".hidden", false), - rel_path_entry("banana", false), - rel_path_entry("ALLCAPS", false), - ] - ); - } - - #[test] - fn compare_rel_paths_unicode() { - let directories_only_paths = vec![ - rel_path_entry("mixedCase", false), - rel_path_entry("Zebra", false), - rel_path_entry("banana", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("dog", false), - rel_path_entry(".hidden", false), - rel_path_entry("Carrot", false), - ]; - assert_eq!( - sorted_rel_paths( - directories_only_paths, - SortMode::DirectoriesFirst, - SortOrder::Unicode, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple", false), - rel_path_entry("Carrot", false), - rel_path_entry("Zebra", false), - rel_path_entry("banana", false), - rel_path_entry("dog", false), - rel_path_entry("mixedCase", false), - ] - ); - - let file_and_directory_paths = vec![ - rel_path_entry("banana", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("ALLCAPS", false), - rel_path_entry("file1.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry(".hidden", false), - ]; - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::DirectoriesFirst, - SortOrder::Unicode, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("banana", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths.clone(), - SortMode::Mixed, - SortOrder::Unicode, - ), - vec![ - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("banana", false), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - ] - ); - assert_eq!( - sorted_rel_paths( - file_and_directory_paths, - SortMode::FilesFirst, - SortOrder::Unicode, - ), - vec![ - rel_path_entry("Apple.txt", true), - rel_path_entry("File2.txt", true), - rel_path_entry("dog.md", true), - rel_path_entry("file1.txt", true), - rel_path_entry(".hidden", false), - rel_path_entry("ALLCAPS", false), - rel_path_entry("banana", false), - ] - ); - - let numeric_paths = vec![ - rel_path_entry("file10.txt", true), - rel_path_entry("file1.txt", true), - rel_path_entry("file2.txt", true), - rel_path_entry("file20.txt", true), - ]; - assert_eq!( - sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,), - vec![ - rel_path_entry("file1.txt", true), - rel_path_entry("file10.txt", true), - rel_path_entry("file2.txt", true), - rel_path_entry("file20.txt", true), - ] - ); - - let accented_paths = vec![ - rel_path_entry("\u{00C9}something.txt", true), - rel_path_entry("zebra.txt", true), - rel_path_entry("Apple.txt", true), - ]; - assert_eq!( - sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode), - vec![ - rel_path_entry("Apple.txt", true), - rel_path_entry("zebra.txt", true), - rel_path_entry("\u{00C9}something.txt", true), - ] - ); - } - - #[test] - fn path_with_position_parse_posix_path() { - // Test POSIX filename edge cases - // Read more at https://en.wikipedia.org/wiki/Filename - assert_eq!( - PathWithPosition::parse_str("test_file"), - PathWithPosition { - path: PathBuf::from("test_file"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("a:bc:.zip:1"), - PathWithPosition { - path: PathBuf::from("a:bc:.zip"), - row: Some(1), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("one.second.zip:1"), - PathWithPosition { - path: PathBuf::from("one.second.zip"), - row: Some(1), - column: None - } - ); - - // Trim off trailing `:`s for otherwise valid input. - assert_eq!( - PathWithPosition::parse_str("test_file:10:1:"), - PathWithPosition { - path: PathBuf::from("test_file"), - row: Some(10), - column: Some(1) - } - ); - - assert_eq!( - PathWithPosition::parse_str("test_file.rs:"), - PathWithPosition { - path: PathBuf::from("test_file.rs"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("test_file.rs:1:"), - PathWithPosition { - path: PathBuf::from("test_file.rs"), - row: Some(1), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("ab\ncd"), - PathWithPosition { - path: PathBuf::from("ab\ncd"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("👋\nab"), - PathWithPosition { - path: PathBuf::from("👋\nab"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"), - PathWithPosition { - path: PathBuf::from("Types.hs"), - row: Some(617), - column: Some(9), - } - ); - - assert_eq!( - PathWithPosition::parse_str("main (1).log"), - PathWithPosition { - path: PathBuf::from("main (1).log"), - row: None, - column: None - } - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn path_with_position_parse_posix_path_with_suffix() { - assert_eq!( - PathWithPosition::parse_str("foo/bar:34:in"), - PathWithPosition { - path: PathBuf::from("foo/bar"), - row: Some(34), - column: None, - } - ); - assert_eq!( - PathWithPosition::parse_str("foo/bar.rs:1902:::15:"), - PathWithPosition { - path: PathBuf::from("foo/bar.rs:1902"), - row: Some(15), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"), - PathWithPosition { - path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"), - row: Some(34), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("crates/file_finder/src/file_finder.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("crate/utils/src/test:today.log:34"), - PathWithPosition { - path: PathBuf::from("crate/utils/src/test:today.log"), - row: Some(34), - column: None, - } - ); - assert_eq!( - PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"), - PathWithPosition { - path: PathBuf::from("/testing/out/src/file_finder.odin"), - row: Some(7), - column: Some(15), - } - ); - } - - #[test] - #[cfg(target_os = "windows")] - fn path_with_position_parse_windows_path() { - assert_eq!( - PathWithPosition::parse_str("crates\\utils\\paths.rs"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\main (1).log"), - row: None, - column: None - } - ); - } - - #[test] - #[cfg(target_os = "windows")] - fn path_with_position_parse_windows_path_with_suffix() { - assert_eq!( - PathWithPosition::parse_str("crates\\utils\\paths.rs:101"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: Some(101), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1), - column: Some(20) - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13) - } - ); - - // Trim off trailing `:`s for otherwise valid input. - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13) - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), - row: Some(13), - column: Some(15) - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), - row: Some(15), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("crates/utils/paths.rs:101"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: Some(101), - column: None, - } - ); - } - - #[test] - fn test_path_compact() { - let path: PathBuf = [ - home_dir().to_string_lossy().into_owned(), - "some_file.txt".to_string(), - ] - .iter() - .collect(); - if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { - assert_eq!(path.compact().to_str(), Some("~/some_file.txt")); - } else { - assert_eq!(path.compact().to_str(), path.to_str()); - } - } - - #[test] - fn test_extension_or_hidden_file_name() { - // No dots in name - let path = Path::new("/a/b/c/file_name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Single dot in name - let path = Path::new("/a/b/c/file.name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Multiple dots in name - let path = Path::new("/a/b/c/long.file.name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Hidden file, no extension - let path = Path::new("/a/b/c/.gitignore"); - assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore")); - - // Hidden file, with extension - let path = Path::new("/a/b/c/.eslintrc.js"); - assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js")); - } - - // fn edge_of_glob() { - // let path = Path::new("/work/node_modules"); - // let path_matcher = - // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // } - - // #[test] - // fn file_in_dirs() { - // let path = Path::new("/work/.env"); - // let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // let path = Path::new("/work/package.json"); - // assert!( - // !path_matcher.is_match(path), - // "Path matcher should not match {path:?}" - // ); - // } - - // #[test] - // fn project_search() { - // let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); - // let path_matcher = - // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // } - #[test] - #[cfg(target_os = "windows")] - fn test_sanitized_path() { - let path = Path::new("C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::new(path); - assert_eq!( - sanitized_path.to_string(), - "C:\\Users\\someone\\test_file.rs" - ); - - let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::new(path); - assert_eq!( - sanitized_path.to_string(), - "C:\\Users\\someone\\test_file.rs" - ); - } - - #[test] - fn test_compare_numeric_segments() { - // Helper function to create peekable iterators and test - fn compare(a: &str, b: &str) -> Ordering { - let mut a_iter = a.chars().peekable(); - let mut b_iter = b.chars().peekable(); - - let result = compare_numeric_segments(&mut a_iter, &mut b_iter); - - // Verify iterators advanced correctly - assert!( - !a_iter.next().is_some_and(|c| c.is_ascii_digit()), - "Iterator a should have consumed all digits" - ); - assert!( - !b_iter.next().is_some_and(|c| c.is_ascii_digit()), - "Iterator b should have consumed all digits" - ); - - result - } - - // Basic numeric comparisons - assert_eq!(compare("0", "0"), Ordering::Equal); - assert_eq!(compare("1", "2"), Ordering::Less); - assert_eq!(compare("9", "10"), Ordering::Less); - assert_eq!(compare("10", "9"), Ordering::Greater); - assert_eq!(compare("99", "100"), Ordering::Less); - - // Leading zeros - assert_eq!(compare("0", "00"), Ordering::Less); - assert_eq!(compare("00", "0"), Ordering::Greater); - assert_eq!(compare("01", "1"), Ordering::Greater); - assert_eq!(compare("001", "1"), Ordering::Greater); - assert_eq!(compare("001", "01"), Ordering::Greater); - - // Same value different representation - assert_eq!(compare("000100", "100"), Ordering::Greater); - assert_eq!(compare("100", "0100"), Ordering::Less); - assert_eq!(compare("0100", "00100"), Ordering::Less); - - // Large numbers - assert_eq!(compare("9999999999", "10000000000"), Ordering::Less); - assert_eq!( - compare( - "340282366920938463463374607431768211455", // u128::MAX - "340282366920938463463374607431768211456" - ), - Ordering::Less - ); - assert_eq!( - compare( - "340282366920938463463374607431768211456", // > u128::MAX - "340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Iterator advancement verification - let mut a_iter = "123abc".chars().peekable(); - let mut b_iter = "456def".chars().peekable(); - - compare_numeric_segments(&mut a_iter, &mut b_iter); - - assert_eq!(a_iter.collect::(), "abc"); - assert_eq!(b_iter.collect::(), "def"); - } - - #[test] - fn test_natural_sort() { - // Basic alphanumeric - assert_eq!(natural_sort("a", "b"), Ordering::Less); - assert_eq!(natural_sort("b", "a"), Ordering::Greater); - assert_eq!(natural_sort("a", "a"), Ordering::Equal); - - // Case sensitivity - assert_eq!(natural_sort("a", "A"), Ordering::Less); - assert_eq!(natural_sort("A", "a"), Ordering::Greater); - assert_eq!(natural_sort("aA", "aa"), Ordering::Greater); - assert_eq!(natural_sort("aa", "aA"), Ordering::Less); - - // Numbers - assert_eq!(natural_sort("1", "2"), Ordering::Less); - assert_eq!(natural_sort("2", "10"), Ordering::Less); - assert_eq!(natural_sort("02", "10"), Ordering::Less); - assert_eq!(natural_sort("02", "2"), Ordering::Greater); - - // Mixed alphanumeric - assert_eq!(natural_sort("a1", "a2"), Ordering::Less); - assert_eq!(natural_sort("a2", "a10"), Ordering::Less); - assert_eq!(natural_sort("a02", "a2"), Ordering::Greater); - assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less); - - // Multiple numeric segments - assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less); - assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater); - assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less); - - // Special characters - assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less); - assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less); - assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less); - - // Unicode - assert_eq!(natural_sort("文1", "文2"), Ordering::Less); - assert_eq!(natural_sort("文2", "文10"), Ordering::Less); - assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less); - - // Empty and special cases - assert_eq!(natural_sort("", ""), Ordering::Equal); - assert_eq!(natural_sort("", "a"), Ordering::Less); - assert_eq!(natural_sort("a", ""), Ordering::Greater); - assert_eq!(natural_sort(" ", " "), Ordering::Less); - - // Mixed everything - assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less); - assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater); - assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less); - assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less); - assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less); - } - - #[test] - fn test_compare_paths() { - // Helper function for cleaner tests - fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering { - compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file)) - } - - // Basic path comparison - assert_eq!(compare("a", true, "b", true), Ordering::Less); - assert_eq!(compare("b", true, "a", true), Ordering::Greater); - assert_eq!(compare("a", true, "a", true), Ordering::Equal); - - // Files vs Directories - assert_eq!(compare("a", true, "a", false), Ordering::Greater); - assert_eq!(compare("a", false, "a", true), Ordering::Less); - assert_eq!(compare("b", false, "a", true), Ordering::Less); - - // Extensions - assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater); - assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less); - assert_eq!(compare("a", true, "a.txt", true), Ordering::Less); - - // Nested paths - assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less); - assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less); - assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less); - - // Case sensitivity in paths - assert_eq!( - compare("Dir/file", true, "dir/file", true), - Ordering::Greater - ); - assert_eq!( - compare("dir/File", true, "dir/file", true), - Ordering::Greater - ); - assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less); - - // Hidden files and special names - assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less); - assert_eq!(compare("_special", true, "normal", true), Ordering::Less); - assert_eq!(compare(".config", false, ".data", false), Ordering::Less); - - // Mixed numeric paths - assert_eq!( - compare("dir1/file", true, "dir2/file", true), - Ordering::Less - ); - assert_eq!( - compare("dir2/file", true, "dir10/file", true), - Ordering::Less - ); - assert_eq!( - compare("dir02/file", true, "dir2/file", true), - Ordering::Greater - ); - - // Root paths - assert_eq!(compare("/a", true, "/b", true), Ordering::Less); - assert_eq!(compare("/", false, "/a", true), Ordering::Less); - - // Complex real-world examples - assert_eq!( - compare("project/src/main.rs", true, "project/src/lib.rs", true), - Ordering::Greater - ); - assert_eq!( - compare( - "project/tests/test_1.rs", - true, - "project/tests/test_2.rs", - true - ), - Ordering::Less - ); - assert_eq!( - compare( - "project/v1.0.0/README.md", - true, - "project/v1.10.0/README.md", - true - ), - Ordering::Less - ); - } - - #[test] - fn test_natural_sort_case_sensitivity() { - std::thread::sleep(std::time::Duration::from_millis(100)); - // Same letter different case - lowercase should come first - assert_eq!(natural_sort("a", "A"), Ordering::Less); - assert_eq!(natural_sort("A", "a"), Ordering::Greater); - assert_eq!(natural_sort("a", "a"), Ordering::Equal); - assert_eq!(natural_sort("A", "A"), Ordering::Equal); - - // Mixed case strings - assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less); - assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater); - assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less); - - // Different letters - assert_eq!(natural_sort("a", "b"), Ordering::Less); - assert_eq!(natural_sort("A", "b"), Ordering::Less); - assert_eq!(natural_sort("a", "B"), Ordering::Less); - } - - #[test] - fn test_natural_sort_with_numbers() { - // Basic number ordering - assert_eq!(natural_sort("file1", "file2"), Ordering::Less); - assert_eq!(natural_sort("file2", "file10"), Ordering::Less); - assert_eq!(natural_sort("file10", "file2"), Ordering::Greater); - - // Numbers in different positions - assert_eq!(natural_sort("1file", "2file"), Ordering::Less); - assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less); - assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less); - - // Multiple numbers in string - assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less); - assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less); - - // Leading zeros - assert_eq!(natural_sort("file002", "file2"), Ordering::Greater); - assert_eq!(natural_sort("file002", "file10"), Ordering::Less); - - // Very large numbers - assert_eq!( - natural_sort("file999999999999999999999", "file999999999999999999998"), - Ordering::Greater - ); - - // u128 edge cases - - // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455) - assert_eq!( - natural_sort( - "file340282366920938463463374607431768211454", - "file340282366920938463463374607431768211455" - ), - Ordering::Less - ); - - // Equal length numbers that overflow u128 - assert_eq!( - natural_sort( - "file340282366920938463463374607431768211456", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Different length numbers that overflow u128 - assert_eq!( - natural_sort( - "file3402823669209384634633746074317682114560", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Leading zeros with numbers near u128::MAX - assert_eq!( - natural_sort( - "file0340282366920938463463374607431768211455", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Very large numbers with different lengths (both overflow u128) - assert_eq!( - natural_sort( - "file999999999999999999999999999999999999999999999999", - "file9999999999999999999999999999999999999999999999999" - ), - Ordering::Less - ); - } - - #[test] - fn test_natural_sort_case_sensitive() { - // Numerically smaller values come first. - assert_eq!(natural_sort("File1", "file2"), Ordering::Less); - assert_eq!(natural_sort("file1", "File2"), Ordering::Less); - - // Numerically equal values: the case-insensitive comparison decides first. - // Case-sensitive comparison only occurs when both are equal case-insensitively. - assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less); - assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less); - assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less); - - // Numerically equal and case-insensitively equal: - // the lexicographically smaller (case-sensitive) one wins. - assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less); - assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less); - assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less); - } - - #[test] - fn test_natural_sort_edge_cases() { - // Empty strings - assert_eq!(natural_sort("", ""), Ordering::Equal); - assert_eq!(natural_sort("", "a"), Ordering::Less); - assert_eq!(natural_sort("a", ""), Ordering::Greater); - - // Special characters - assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less); - assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less); - assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less); - - // Unicode characters - // 9312 vs 9313 - assert_eq!(natural_sort("file①", "file②"), Ordering::Less); - // 9321 vs 9313 - assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater); - // 28450 vs 23383 - assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater); - - // Mixed alphanumeric with special chars - assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less); - assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less); - assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater); - } - - #[test] - fn test_multiple_extensions() { - // No extensions - let path = Path::new("/a/b/c/file_name"); - assert_eq!(path.multiple_extensions(), None); - - // Only one extension - let path = Path::new("/a/b/c/file_name.tsx"); - assert_eq!(path.multiple_extensions(), None); - - // Stories sample extension - let path = Path::new("/a/b/c/file_name.stories.tsx"); - assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string())); - - // Longer sample extension - let path = Path::new("/a/b/c/long.app.tar.gz"); - assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string())); - } - - #[test] - fn test_strip_path_suffix() { - let base = Path::new("/a/b/c/file_name"); - let suffix = Path::new("file_name"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); - - let base = Path::new("/a/b/c/file_name.tsx"); - let suffix = Path::new("file_name.tsx"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); - - let base = Path::new("/a/b/c/file_name.stories.tsx"); - let suffix = Path::new("c/file_name.stories.tsx"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b"))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("b/c/long.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a"))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("/a/b/c/long.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new(""))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("/a/b/c/no_match.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), None); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), None); - } - - #[test] - fn test_strip_prefix() { - let expected = [ - ( - PathStyle::Posix, - "/a/b/c", - "/a/b", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Posix, - "/a/b/c", - "/a/b/", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Posix, - "/a/b/c", - "/", - Some(rel_path("a/b/c").to_arc()), - ), - (PathStyle::Posix, "/a/b/c", "", None), - (PathStyle::Posix, "/a/b//c", "/a/b/", None), - (PathStyle::Posix, "/a/bc", "/a/b", None), - ( - PathStyle::Posix, - "/a/b/c", - "/a/b/c", - Some(rel_path("").to_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\a\\b", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\a\\b\\", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\", - Some(rel_path("a/b/c").to_arc()), - ), - (PathStyle::Windows, "C:\\a\\b\\c", "", None), - (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None), - (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b\\", - Some(rel_path("c").to_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b/", - Some(rel_path("c").to_arc()), - ), - ]; - let actual = expected.clone().map(|(style, child, parent, _)| { - ( - style, - child, - parent, - style - .strip_prefix(child.as_ref(), parent.as_ref()) - .map(|rel_path| rel_path.to_arc()), - ) - }); - pretty_assertions::assert_eq!(actual, expected); - } - - #[cfg(target_os = "windows")] - #[test] - fn test_wsl_path() { - use super::WslPath; - let path = "/a/b/c"; - assert_eq!(WslPath::from_path(&path), None); - - let path = r"\\wsl.localhost"; - assert_eq!(WslPath::from_path(&path), None); - - let path = r"\\wsl.localhost\Distro"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "Distro".to_owned(), - path: "/".into(), - }) - ); - - let path = r"\\wsl.localhost\Distro\blue"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "Distro".to_owned(), - path: "/blue".into() - }) - ); - - let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "archlinux".to_owned(), - path: "/tomato/paprika/../aubergine.txt".into() - }) - ); - - let path = r"\\windows.localhost\Distro\foo"; - assert_eq!(WslPath::from_path(&path), None); - } - - #[test] - fn test_url_to_file_path_ext_posix_basic() { - use super::UrlExt; - - let url = url::Url::parse("file:///home/user/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/home/user/file.txt")) - ); - - let url = url::Url::parse("file:///").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/")) - ); - - let url = url::Url::parse("file:///a/b/c/d/e").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/a/b/c/d/e")) - ); - } - - #[test] - fn test_url_to_file_path_ext_posix_percent_encoding() { - use super::UrlExt; - - let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/home/user/file with spaces.txt")) - ); - - let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/path/with/encoded/slashes")) - ); - - let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/special#chars?.txt")) - ); - } - - #[test] - fn test_url_to_file_path_ext_posix_localhost() { - use super::UrlExt; - - let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/home/user/file.txt")) - ); - } - - #[test] - fn test_url_to_file_path_ext_posix_rejects_host() { - use super::UrlExt; - - let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap(); - assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); - } - - #[test] - fn test_url_to_file_path_ext_posix_windows_drive_letter() { - use super::UrlExt; - - let url = url::Url::parse("file:///C:").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/C:/")) - ); - - let url = url::Url::parse("file:///D|").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Posix), - Ok(PathBuf::from("/D|/")) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_basic() { - use super::UrlExt; - - let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\Users\\user\\file.txt")) - ); - - let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs")) - ); - - let url = url::Url::parse("file:///C:/").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\")) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_encoded_drive_letter() { - use super::UrlExt; - - let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\Users\\file.txt")) - ); - - let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("c:\\Users\\file.txt")) - ); - - let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("D:\\folder\\file.txt")) - ); - - let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("d:\\folder\\file.txt")) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_unc_path() { - use super::UrlExt; - - let url = url::Url::parse("file://server/share/path/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("\\\\server\\share\\path\\file.txt")) - ); - - let url = url::Url::parse("file://server/share").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("\\\\server\\share")) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_percent_encoding() { - use super::UrlExt; - - let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt")) - ); - - let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\special#chars?.txt")) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_invalid_drive() { - use super::UrlExt; - - let url = url::Url::parse("file:///1:/path/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - - let url = url::Url::parse("file:///CC:/path/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - - let url = url::Url::parse("file:///C/path/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - - let url = url::Url::parse("file:///invalid").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - } - - #[test] - fn test_url_to_file_path_ext_non_file_scheme() { - use super::UrlExt; - - let url = url::Url::parse("http://example.com/path").unwrap(); - assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - - let url = url::Url::parse("https://example.com/path").unwrap(); - assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Err(ToFilePathError) - ); - } - - #[test] - fn test_url_to_file_path_ext_windows_localhost() { - use super::UrlExt; - - let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap(); - assert_eq!( - url.to_file_path_ext(PathStyle::Windows), - Ok(PathBuf::from("C:\\Users\\file.txt")) - ); - } + use crate::rel_path::rel_path; + + use super::*; + // perf annotations replaced with #[test] + + #[test] + fn test_join_path_uses_path_style_separator() { + let posix_path = PathStyle::Posix + .join_path(Path::new("/home/user/dev"), "worktrees") + .unwrap(); + let windows_path = PathStyle::Windows + .join_path(Path::new("C:\\Users\\user\\dev"), "worktrees") + .unwrap(); + + assert_eq!(posix_path, PathBuf::from("/home/user/dev/worktrees")); + assert_eq!( + windows_path.to_string_lossy(), + "C:\\Users\\user\\dev\\worktrees" + ); + } + + #[test] + fn test_normalize_uses_path_style_separator() { + assert_eq!( + PathStyle::Posix.normalize("/home/user/dev/../worktrees/./zed"), + "/home/user/worktrees/zed" + ); + assert_eq!( + PathStyle::Windows.normalize("C:\\Users\\user\\dev\\worktrees"), + "C:\\Users\\user\\dev\\worktrees" + ); + } + + fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) { + (RelPath::unix(path).unwrap(), is_file) + } + + fn sorted_rel_paths( + mut paths: Vec<(&'static RelPath, bool)>, + mode: SortMode, + order: SortOrder, + ) -> Vec<(&'static RelPath, bool)> { + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order)); + paths + } + + #[test] + fn compare_paths_with_dots() { + let mut paths = vec![ + (Path::new("test_dirs"), false), + (Path::new("test_dirs/1.46"), false), + (Path::new("test_dirs/1.46/bar_1"), true), + (Path::new("test_dirs/1.46/bar_2"), true), + (Path::new("test_dirs/1.45"), false), + (Path::new("test_dirs/1.45/foo_2"), true), + (Path::new("test_dirs/1.45/foo_1"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("test_dirs"), false), + (Path::new("test_dirs/1.45"), false), + (Path::new("test_dirs/1.45/foo_1"), true), + (Path::new("test_dirs/1.45/foo_2"), true), + (Path::new("test_dirs/1.46"), false), + (Path::new("test_dirs/1.46/bar_1"), true), + (Path::new("test_dirs/1.46/bar_2"), true), + ] + ); + let mut paths = vec![ + (Path::new("root1/one.txt"), true), + (Path::new("root1/one.two.txt"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("root1/one.txt"), true), + (Path::new("root1/one.two.txt"), true), + ] + ); + } + + #[test] + fn compare_paths_with_same_name_different_extensions() { + let mut paths = vec![ + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + ] + ); + } + + #[test] + fn compare_paths_case_semi_sensitive() { + let mut paths = vec![ + (Path::new("test_DIRS"), false), + (Path::new("test_DIRS/foo_1"), true), + (Path::new("test_DIRS/foo_2"), true), + (Path::new("test_DIRS/bar"), true), + (Path::new("test_DIRS/BAR"), true), + (Path::new("test_dirs"), false), + (Path::new("test_dirs/foo_1"), true), + (Path::new("test_dirs/foo_2"), true), + (Path::new("test_dirs/bar"), true), + (Path::new("test_dirs/BAR"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("test_dirs"), false), + (Path::new("test_dirs/bar"), true), + (Path::new("test_dirs/BAR"), true), + (Path::new("test_dirs/foo_1"), true), + (Path::new("test_dirs/foo_2"), true), + (Path::new("test_DIRS"), false), + (Path::new("test_DIRS/bar"), true), + (Path::new("test_DIRS/BAR"), true), + (Path::new("test_DIRS/foo_1"), true), + (Path::new("test_DIRS/foo_2"), true), + ] + ); + } + + #[test] + fn compare_paths_mixed_case_numeric_ordering() { + let mut entries = [ + (Path::new(".config"), false), + (Path::new("Dir1"), false), + (Path::new("dir01"), false), + (Path::new("dir2"), false), + (Path::new("Dir02"), false), + (Path::new("dir10"), false), + (Path::new("Dir10"), false), + ]; + + entries.sort_by(|&a, &b| compare_paths(a, b)); + + let ordered: Vec<&str> = entries + .iter() + .map(|(path, _)| path.to_str().unwrap()) + .collect(); + + assert_eq!( + ordered, + vec![ + ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10" + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_case_insensitive() { + // Test that mixed mode is case-insensitive + let mut paths = vec![ + (RelPath::unix("zebra.txt").unwrap(), true), + (RelPath::unix("Apple").unwrap(), false), + (RelPath::unix("banana.rs").unwrap(), true), + (RelPath::unix("Carrot").unwrap(), false), + (RelPath::unix("aardvark.txt").unwrap(), true), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + // Case-insensitive: aardvark < Apple < banana < Carrot < zebra + assert_eq!( + paths, + vec![ + (RelPath::unix("aardvark.txt").unwrap(), true), + (RelPath::unix("Apple").unwrap(), false), + (RelPath::unix("banana.rs").unwrap(), true), + (RelPath::unix("Carrot").unwrap(), false), + (RelPath::unix("zebra.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_basic() { + // Test that files come before directories + let mut paths = vec![ + (RelPath::unix("zebra.txt").unwrap(), true), + (RelPath::unix("Apple").unwrap(), false), + (RelPath::unix("banana.rs").unwrap(), true), + (RelPath::unix("Carrot").unwrap(), false), + (RelPath::unix("aardvark.txt").unwrap(), true), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + // Files first (case-insensitive), then directories (case-insensitive) + assert_eq!( + paths, + vec![ + (RelPath::unix("aardvark.txt").unwrap(), true), + (RelPath::unix("banana.rs").unwrap(), true), + (RelPath::unix("zebra.txt").unwrap(), true), + (RelPath::unix("Apple").unwrap(), false), + (RelPath::unix("Carrot").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_case_insensitive() { + // Test case-insensitive sorting within files and directories + let mut paths = vec![ + (RelPath::unix("Zebra.txt").unwrap(), true), + (RelPath::unix("apple").unwrap(), false), + (RelPath::unix("Banana.rs").unwrap(), true), + (RelPath::unix("carrot").unwrap(), false), + (RelPath::unix("Aardvark.txt").unwrap(), true), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("Aardvark.txt").unwrap(), true), + (RelPath::unix("Banana.rs").unwrap(), true), + (RelPath::unix("Zebra.txt").unwrap(), true), + (RelPath::unix("apple").unwrap(), false), + (RelPath::unix("carrot").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_numeric() { + // Test natural number sorting with files first + let mut paths = vec![ + (RelPath::unix("file10.txt").unwrap(), true), + (RelPath::unix("dir2").unwrap(), false), + (RelPath::unix("file2.txt").unwrap(), true), + (RelPath::unix("dir10").unwrap(), false), + (RelPath::unix("file1.txt").unwrap(), true), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("file1.txt").unwrap(), true), + (RelPath::unix("file2.txt").unwrap(), true), + (RelPath::unix("file10.txt").unwrap(), true), + (RelPath::unix("dir2").unwrap(), false), + (RelPath::unix("dir10").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_case() { + // Test case-insensitive sorting with varied capitalization + let mut paths = vec![ + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix("readme.txt").unwrap(), true), + (RelPath::unix("ReadMe.rs").unwrap(), true), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + // All "readme" variants should group together, sorted by extension + assert_eq!( + paths, + vec![ + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix("ReadMe.rs").unwrap(), true), + (RelPath::unix("readme.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_files_and_dirs() { + // Verify directories and files are still mixed + let mut paths = vec![ + (RelPath::unix("file2.txt").unwrap(), true), + (RelPath::unix("Dir1").unwrap(), false), + (RelPath::unix("file1.txt").unwrap(), true), + (RelPath::unix("dir2").unwrap(), false), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + // Case-insensitive: dir1, dir2, file1, file2 (all mixed) + assert_eq!( + paths, + vec![ + (RelPath::unix("Dir1").unwrap(), false), + (RelPath::unix("dir2").unwrap(), false), + (RelPath::unix("file1.txt").unwrap(), true), + (RelPath::unix("file2.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() { + let mut paths = vec![ + (RelPath::unix("Hello.txt").unwrap(), true), + (RelPath::unix("hello").unwrap(), false), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("hello").unwrap(), false), + (RelPath::unix("Hello.txt").unwrap(), true), + ] + ); + + let mut paths = vec![ + (RelPath::unix("hello").unwrap(), false), + (RelPath::unix("Hello.txt").unwrap(), true), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("hello").unwrap(), false), + (RelPath::unix("Hello.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_with_nested_paths() { + // Test that nested paths still work correctly + let mut paths = vec![ + (RelPath::unix("src/main.rs").unwrap(), true), + (RelPath::unix("Cargo.toml").unwrap(), true), + (RelPath::unix("src").unwrap(), false), + (RelPath::unix("target").unwrap(), false), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("Cargo.toml").unwrap(), true), + (RelPath::unix("src").unwrap(), false), + (RelPath::unix("src/main.rs").unwrap(), true), + (RelPath::unix("target").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_with_nested() { + // Files come before directories, even with nested paths + let mut paths = vec![ + (RelPath::unix("src/lib.rs").unwrap(), true), + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix("src").unwrap(), false), + (RelPath::unix("tests").unwrap(), false), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix("src").unwrap(), false), + (RelPath::unix("src/lib.rs").unwrap(), true), + (RelPath::unix("tests").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_dotfiles() { + // Test that dotfiles are handled correctly in mixed mode + let mut paths = vec![ + (RelPath::unix(".gitignore").unwrap(), true), + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix(".github").unwrap(), false), + (RelPath::unix("src").unwrap(), false), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix(".github").unwrap(), false), + (RelPath::unix(".gitignore").unwrap(), true), + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix("src").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_dotfiles() { + // Test that dotfiles come first when they're files + let mut paths = vec![ + (RelPath::unix(".gitignore").unwrap(), true), + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix(".github").unwrap(), false), + (RelPath::unix("src").unwrap(), false), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix(".gitignore").unwrap(), true), + (RelPath::unix("README.md").unwrap(), true), + (RelPath::unix(".github").unwrap(), false), + (RelPath::unix("src").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_same_stem_different_extension() { + // Files with same stem but different extensions should sort by extension + let mut paths = vec![ + (RelPath::unix("file.rs").unwrap(), true), + (RelPath::unix("file.md").unwrap(), true), + (RelPath::unix("file.txt").unwrap(), true), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("file.md").unwrap(), true), + (RelPath::unix("file.rs").unwrap(), true), + (RelPath::unix("file.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_files_first_same_stem() { + // Same stem files should still sort by extension with files_first + let mut paths = vec![ + (RelPath::unix("main.rs").unwrap(), true), + (RelPath::unix("main.c").unwrap(), true), + (RelPath::unix("main").unwrap(), false), + ]; + paths + .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("main.c").unwrap(), true), + (RelPath::unix("main.rs").unwrap(), true), + (RelPath::unix("main").unwrap(), false), + ] + ); + } + + #[test] + fn compare_rel_paths_mixed_deep_nesting() { + // Test sorting with deeply nested paths + let mut paths = vec![ + (RelPath::unix("a/b/c.txt").unwrap(), true), + (RelPath::unix("A/B.txt").unwrap(), true), + (RelPath::unix("a.txt").unwrap(), true), + (RelPath::unix("A.txt").unwrap(), true), + ]; + paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default)); + assert_eq!( + paths, + vec![ + (RelPath::unix("a/b/c.txt").unwrap(), true), + (RelPath::unix("A/B.txt").unwrap(), true), + (RelPath::unix("a.txt").unwrap(), true), + (RelPath::unix("A.txt").unwrap(), true), + ] + ); + } + + #[test] + fn compare_rel_paths_upper() { + let directories_only_paths = vec![ + rel_path_entry("mixedCase", false), + rel_path_entry("Zebra", false), + rel_path_entry("banana", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("dog", false), + rel_path_entry(".hidden", false), + rel_path_entry("Carrot", false), + ]; + assert_eq!( + sorted_rel_paths( + directories_only_paths, + SortMode::DirectoriesFirst, + SortOrder::Upper, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("Carrot", false), + rel_path_entry("Zebra", false), + rel_path_entry("banana", false), + rel_path_entry("dog", false), + rel_path_entry("mixedCase", false), + ] + ); + + let file_and_directory_paths = vec![ + rel_path_entry("banana", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("ALLCAPS", false), + rel_path_entry("file1.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry(".hidden", false), + ]; + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::DirectoriesFirst, + SortOrder::Upper, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("banana", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::Mixed, + SortOrder::Upper, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("banana", false), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths, + SortMode::FilesFirst, + SortOrder::Upper, + ), + vec![ + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("banana", false), + ] + ); + + let natural_sort_paths = vec![ + rel_path_entry("file10.txt", true), + rel_path_entry("file1.txt", true), + rel_path_entry("file20.txt", true), + rel_path_entry("file2.txt", true), + ]; + assert_eq!( + sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,), + vec![ + rel_path_entry("file1.txt", true), + rel_path_entry("file2.txt", true), + rel_path_entry("file10.txt", true), + rel_path_entry("file20.txt", true), + ] + ); + + let accented_paths = vec![ + rel_path_entry("\u{00C9}something.txt", true), + rel_path_entry("zebra.txt", true), + rel_path_entry("Apple.txt", true), + ]; + assert_eq!( + sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper), + vec![ + rel_path_entry("Apple.txt", true), + rel_path_entry("\u{00C9}something.txt", true), + rel_path_entry("zebra.txt", true), + ] + ); + } + + #[test] + fn compare_rel_paths_lower() { + let directories_only_paths = vec![ + rel_path_entry("mixedCase", false), + rel_path_entry("Zebra", false), + rel_path_entry("banana", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("dog", false), + rel_path_entry(".hidden", false), + rel_path_entry("Carrot", false), + ]; + assert_eq!( + sorted_rel_paths( + directories_only_paths, + SortMode::DirectoriesFirst, + SortOrder::Lower, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("banana", false), + rel_path_entry("dog", false), + rel_path_entry("mixedCase", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("Carrot", false), + rel_path_entry("Zebra", false), + ] + ); + + let file_and_directory_paths = vec![ + rel_path_entry("banana", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("ALLCAPS", false), + rel_path_entry("file1.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry(".hidden", false), + ]; + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::DirectoriesFirst, + SortOrder::Lower, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("banana", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::Mixed, + SortOrder::Lower, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("banana", false), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths, + SortMode::FilesFirst, + SortOrder::Lower, + ), + vec![ + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry(".hidden", false), + rel_path_entry("banana", false), + rel_path_entry("ALLCAPS", false), + ] + ); + } + + #[test] + fn compare_rel_paths_unicode() { + let directories_only_paths = vec![ + rel_path_entry("mixedCase", false), + rel_path_entry("Zebra", false), + rel_path_entry("banana", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("dog", false), + rel_path_entry(".hidden", false), + rel_path_entry("Carrot", false), + ]; + assert_eq!( + sorted_rel_paths( + directories_only_paths, + SortMode::DirectoriesFirst, + SortOrder::Unicode, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple", false), + rel_path_entry("Carrot", false), + rel_path_entry("Zebra", false), + rel_path_entry("banana", false), + rel_path_entry("dog", false), + rel_path_entry("mixedCase", false), + ] + ); + + let file_and_directory_paths = vec![ + rel_path_entry("banana", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("ALLCAPS", false), + rel_path_entry("file1.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry(".hidden", false), + ]; + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::DirectoriesFirst, + SortOrder::Unicode, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("banana", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths.clone(), + SortMode::Mixed, + SortOrder::Unicode, + ), + vec![ + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("banana", false), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + ] + ); + assert_eq!( + sorted_rel_paths( + file_and_directory_paths, + SortMode::FilesFirst, + SortOrder::Unicode, + ), + vec![ + rel_path_entry("Apple.txt", true), + rel_path_entry("File2.txt", true), + rel_path_entry("dog.md", true), + rel_path_entry("file1.txt", true), + rel_path_entry(".hidden", false), + rel_path_entry("ALLCAPS", false), + rel_path_entry("banana", false), + ] + ); + + let numeric_paths = vec![ + rel_path_entry("file10.txt", true), + rel_path_entry("file1.txt", true), + rel_path_entry("file2.txt", true), + rel_path_entry("file20.txt", true), + ]; + assert_eq!( + sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,), + vec![ + rel_path_entry("file1.txt", true), + rel_path_entry("file10.txt", true), + rel_path_entry("file2.txt", true), + rel_path_entry("file20.txt", true), + ] + ); + + let accented_paths = vec![ + rel_path_entry("\u{00C9}something.txt", true), + rel_path_entry("zebra.txt", true), + rel_path_entry("Apple.txt", true), + ]; + assert_eq!( + sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode), + vec![ + rel_path_entry("Apple.txt", true), + rel_path_entry("zebra.txt", true), + rel_path_entry("\u{00C9}something.txt", true), + ] + ); + } + + #[test] + fn path_with_position_parse_posix_path() { + // Test POSIX filename edge cases + // Read more at https://en.wikipedia.org/wiki/Filename + assert_eq!( + PathWithPosition::parse_str("test_file"), + PathWithPosition { + path: PathBuf::from("test_file"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("a:bc:.zip:1"), + PathWithPosition { + path: PathBuf::from("a:bc:.zip"), + row: Some(1), + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("one.second.zip:1"), + PathWithPosition { + path: PathBuf::from("one.second.zip"), + row: Some(1), + column: None + } + ); + + // Trim off trailing `:`s for otherwise valid input. + assert_eq!( + PathWithPosition::parse_str("test_file:10:1:"), + PathWithPosition { + path: PathBuf::from("test_file"), + row: Some(10), + column: Some(1) + } + ); + + assert_eq!( + PathWithPosition::parse_str("test_file.rs:"), + PathWithPosition { + path: PathBuf::from("test_file.rs"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("test_file.rs:1:"), + PathWithPosition { + path: PathBuf::from("test_file.rs"), + row: Some(1), + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("ab\ncd"), + PathWithPosition { + path: PathBuf::from("ab\ncd"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("👋\nab"), + PathWithPosition { + path: PathBuf::from("👋\nab"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"), + PathWithPosition { + path: PathBuf::from("Types.hs"), + row: Some(617), + column: Some(9), + } + ); + + assert_eq!( + PathWithPosition::parse_str("main (1).log"), + PathWithPosition { + path: PathBuf::from("main (1).log"), + row: None, + column: None + } + ); + } + + #[test] + #[cfg(not(target_os = "windows"))] + fn path_with_position_parse_posix_path_with_suffix() { + assert_eq!( + PathWithPosition::parse_str("foo/bar:34:in"), + PathWithPosition { + path: PathBuf::from("foo/bar"), + row: Some(34), + column: None, + } + ); + assert_eq!( + PathWithPosition::parse_str("foo/bar.rs:1902:::15:"), + PathWithPosition { + path: PathBuf::from("foo/bar.rs:1902"), + row: Some(15), + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"), + PathWithPosition { + path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"), + row: Some(34), + column: None, + } + ); + + assert_eq!( + PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"), + PathWithPosition { + path: PathBuf::from("crates/file_finder/src/file_finder.rs"), + row: Some(1902), + column: Some(13), + } + ); + + assert_eq!( + PathWithPosition::parse_str("crate/utils/src/test:today.log:34"), + PathWithPosition { + path: PathBuf::from("crate/utils/src/test:today.log"), + row: Some(34), + column: None, + } + ); + assert_eq!( + PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"), + PathWithPosition { + path: PathBuf::from("/testing/out/src/file_finder.odin"), + row: Some(7), + column: Some(15), + } + ); + } + + #[test] + #[cfg(target_os = "windows")] + fn path_with_position_parse_windows_path() { + assert_eq!( + PathWithPosition::parse_str("crates\\utils\\paths.rs"), + PathWithPosition { + path: PathBuf::from("crates\\utils\\paths.rs"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), + row: None, + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\main (1).log"), + row: None, + column: None + } + ); + } + + #[test] + #[cfg(target_os = "windows")] + fn path_with_position_parse_windows_path_with_suffix() { + assert_eq!( + PathWithPosition::parse_str("crates\\utils\\paths.rs:101"), + PathWithPosition { + path: PathBuf::from("crates\\utils\\paths.rs"), + row: Some(101), + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), + row: Some(1), + column: Some(20) + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: Some(13) + } + ); + + // Trim off trailing `:`s for otherwise valid input. + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: Some(13) + } + ); + + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), + row: Some(13), + column: Some(15) + } + ); + + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), + row: Some(15), + column: None + } + ); + + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: Some(13), + } + ); + + assert_eq!( + PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"), + PathWithPosition { + path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: None, + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: Some(13), + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: Some(13), + } + ); + + assert_eq!( + PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"), + PathWithPosition { + path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), + row: Some(1902), + column: None, + } + ); + + assert_eq!( + PathWithPosition::parse_str("crates/utils/paths.rs:101"), + PathWithPosition { + path: PathBuf::from("crates\\utils\\paths.rs"), + row: Some(101), + column: None, + } + ); + } + + #[test] + fn test_path_compact() { + let path: PathBuf = [ + home_dir().to_string_lossy().into_owned(), + "some_file.txt".to_string(), + ] + .iter() + .collect(); + if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { + assert_eq!(path.compact().to_str(), Some("~/some_file.txt")); + } else { + assert_eq!(path.compact().to_str(), path.to_str()); + } + } + + #[test] + fn test_extension_or_hidden_file_name() { + // No dots in name + let path = Path::new("/a/b/c/file_name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Single dot in name + let path = Path::new("/a/b/c/file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Multiple dots in name + let path = Path::new("/a/b/c/long.file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Hidden file, no extension + let path = Path::new("/a/b/c/.gitignore"); + assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore")); + + // Hidden file, with extension + let path = Path::new("/a/b/c/.eslintrc.js"); + assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js")); + } + + // fn edge_of_glob() { + // let path = Path::new("/work/node_modules"); + // let path_matcher = + // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); + // assert!( + // path_matcher.is_match(path), + // "Path matcher should match {path:?}" + // ); + // } + + // #[test] + // fn file_in_dirs() { + // let path = Path::new("/work/.env"); + // let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap(); + // assert!( + // path_matcher.is_match(path), + // "Path matcher should match {path:?}" + // ); + // let path = Path::new("/work/package.json"); + // assert!( + // !path_matcher.is_match(path), + // "Path matcher should not match {path:?}" + // ); + // } + + // #[test] + // fn project_search() { + // let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); + // let path_matcher = + // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); + // assert!( + // path_matcher.is_match(path), + // "Path matcher should match {path:?}" + // ); + // } + #[test] + #[cfg(target_os = "windows")] + fn test_sanitized_path() { + let path = Path::new("C:\\Users\\someone\\test_file.rs"); + let sanitized_path = SanitizedPath::new(path); + assert_eq!( + sanitized_path.to_string(), + "C:\\Users\\someone\\test_file.rs" + ); + + let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs"); + let sanitized_path = SanitizedPath::new(path); + assert_eq!( + sanitized_path.to_string(), + "C:\\Users\\someone\\test_file.rs" + ); + } + + #[test] + fn test_compare_numeric_segments() { + // Helper function to create peekable iterators and test + fn compare(a: &str, b: &str) -> Ordering { + let mut a_iter = a.chars().peekable(); + let mut b_iter = b.chars().peekable(); + + let result = compare_numeric_segments(&mut a_iter, &mut b_iter); + + // Verify iterators advanced correctly + assert!( + !a_iter.next().is_some_and(|c| c.is_ascii_digit()), + "Iterator a should have consumed all digits" + ); + assert!( + !b_iter.next().is_some_and(|c| c.is_ascii_digit()), + "Iterator b should have consumed all digits" + ); + + result + } + + // Basic numeric comparisons + assert_eq!(compare("0", "0"), Ordering::Equal); + assert_eq!(compare("1", "2"), Ordering::Less); + assert_eq!(compare("9", "10"), Ordering::Less); + assert_eq!(compare("10", "9"), Ordering::Greater); + assert_eq!(compare("99", "100"), Ordering::Less); + + // Leading zeros + assert_eq!(compare("0", "00"), Ordering::Less); + assert_eq!(compare("00", "0"), Ordering::Greater); + assert_eq!(compare("01", "1"), Ordering::Greater); + assert_eq!(compare("001", "1"), Ordering::Greater); + assert_eq!(compare("001", "01"), Ordering::Greater); + + // Same value different representation + assert_eq!(compare("000100", "100"), Ordering::Greater); + assert_eq!(compare("100", "0100"), Ordering::Less); + assert_eq!(compare("0100", "00100"), Ordering::Less); + + // Large numbers + assert_eq!(compare("9999999999", "10000000000"), Ordering::Less); + assert_eq!( + compare( + "340282366920938463463374607431768211455", // u128::MAX + "340282366920938463463374607431768211456" + ), + Ordering::Less + ); + assert_eq!( + compare( + "340282366920938463463374607431768211456", // > u128::MAX + "340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Iterator advancement verification + let mut a_iter = "123abc".chars().peekable(); + let mut b_iter = "456def".chars().peekable(); + + compare_numeric_segments(&mut a_iter, &mut b_iter); + + assert_eq!(a_iter.collect::(), "abc"); + assert_eq!(b_iter.collect::(), "def"); + } + + #[test] + fn test_natural_sort() { + // Basic alphanumeric + assert_eq!(natural_sort("a", "b"), Ordering::Less); + assert_eq!(natural_sort("b", "a"), Ordering::Greater); + assert_eq!(natural_sort("a", "a"), Ordering::Equal); + + // Case sensitivity + assert_eq!(natural_sort("a", "A"), Ordering::Less); + assert_eq!(natural_sort("A", "a"), Ordering::Greater); + assert_eq!(natural_sort("aA", "aa"), Ordering::Greater); + assert_eq!(natural_sort("aa", "aA"), Ordering::Less); + + // Numbers + assert_eq!(natural_sort("1", "2"), Ordering::Less); + assert_eq!(natural_sort("2", "10"), Ordering::Less); + assert_eq!(natural_sort("02", "10"), Ordering::Less); + assert_eq!(natural_sort("02", "2"), Ordering::Greater); + + // Mixed alphanumeric + assert_eq!(natural_sort("a1", "a2"), Ordering::Less); + assert_eq!(natural_sort("a2", "a10"), Ordering::Less); + assert_eq!(natural_sort("a02", "a2"), Ordering::Greater); + assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less); + + // Multiple numeric segments + assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less); + assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater); + assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less); + + // Special characters + assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less); + assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less); + assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less); + + // Unicode + assert_eq!(natural_sort("文1", "文2"), Ordering::Less); + assert_eq!(natural_sort("文2", "文10"), Ordering::Less); + assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less); + + // Empty and special cases + assert_eq!(natural_sort("", ""), Ordering::Equal); + assert_eq!(natural_sort("", "a"), Ordering::Less); + assert_eq!(natural_sort("a", ""), Ordering::Greater); + assert_eq!(natural_sort(" ", " "), Ordering::Less); + + // Mixed everything + assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less); + assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater); + assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less); + assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less); + assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less); + } + + #[test] + fn test_compare_paths() { + // Helper function for cleaner tests + fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering { + compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file)) + } + + // Basic path comparison + assert_eq!(compare("a", true, "b", true), Ordering::Less); + assert_eq!(compare("b", true, "a", true), Ordering::Greater); + assert_eq!(compare("a", true, "a", true), Ordering::Equal); + + // Files vs Directories + assert_eq!(compare("a", true, "a", false), Ordering::Greater); + assert_eq!(compare("a", false, "a", true), Ordering::Less); + assert_eq!(compare("b", false, "a", true), Ordering::Less); + + // Extensions + assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater); + assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less); + assert_eq!(compare("a", true, "a.txt", true), Ordering::Less); + + // Nested paths + assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less); + assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less); + assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less); + + // Case sensitivity in paths + assert_eq!( + compare("Dir/file", true, "dir/file", true), + Ordering::Greater + ); + assert_eq!( + compare("dir/File", true, "dir/file", true), + Ordering::Greater + ); + assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less); + + // Hidden files and special names + assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less); + assert_eq!(compare("_special", true, "normal", true), Ordering::Less); + assert_eq!(compare(".config", false, ".data", false), Ordering::Less); + + // Mixed numeric paths + assert_eq!( + compare("dir1/file", true, "dir2/file", true), + Ordering::Less + ); + assert_eq!( + compare("dir2/file", true, "dir10/file", true), + Ordering::Less + ); + assert_eq!( + compare("dir02/file", true, "dir2/file", true), + Ordering::Greater + ); + + // Root paths + assert_eq!(compare("/a", true, "/b", true), Ordering::Less); + assert_eq!(compare("/", false, "/a", true), Ordering::Less); + + // Complex real-world examples + assert_eq!( + compare("project/src/main.rs", true, "project/src/lib.rs", true), + Ordering::Greater + ); + assert_eq!( + compare( + "project/tests/test_1.rs", + true, + "project/tests/test_2.rs", + true + ), + Ordering::Less + ); + assert_eq!( + compare( + "project/v1.0.0/README.md", + true, + "project/v1.10.0/README.md", + true + ), + Ordering::Less + ); + } + + #[test] + fn test_natural_sort_case_sensitivity() { + std::thread::sleep(std::time::Duration::from_millis(100)); + // Same letter different case - lowercase should come first + assert_eq!(natural_sort("a", "A"), Ordering::Less); + assert_eq!(natural_sort("A", "a"), Ordering::Greater); + assert_eq!(natural_sort("a", "a"), Ordering::Equal); + assert_eq!(natural_sort("A", "A"), Ordering::Equal); + + // Mixed case strings + assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less); + assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater); + assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less); + + // Different letters + assert_eq!(natural_sort("a", "b"), Ordering::Less); + assert_eq!(natural_sort("A", "b"), Ordering::Less); + assert_eq!(natural_sort("a", "B"), Ordering::Less); + } + + #[test] + fn test_natural_sort_with_numbers() { + // Basic number ordering + assert_eq!(natural_sort("file1", "file2"), Ordering::Less); + assert_eq!(natural_sort("file2", "file10"), Ordering::Less); + assert_eq!(natural_sort("file10", "file2"), Ordering::Greater); + + // Numbers in different positions + assert_eq!(natural_sort("1file", "2file"), Ordering::Less); + assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less); + assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less); + + // Multiple numbers in string + assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less); + assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less); + + // Leading zeros + assert_eq!(natural_sort("file002", "file2"), Ordering::Greater); + assert_eq!(natural_sort("file002", "file10"), Ordering::Less); + + // Very large numbers + assert_eq!( + natural_sort("file999999999999999999999", "file999999999999999999998"), + Ordering::Greater + ); + + // u128 edge cases + + // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455) + assert_eq!( + natural_sort( + "file340282366920938463463374607431768211454", + "file340282366920938463463374607431768211455" + ), + Ordering::Less + ); + + // Equal length numbers that overflow u128 + assert_eq!( + natural_sort( + "file340282366920938463463374607431768211456", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Different length numbers that overflow u128 + assert_eq!( + natural_sort( + "file3402823669209384634633746074317682114560", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Leading zeros with numbers near u128::MAX + assert_eq!( + natural_sort( + "file0340282366920938463463374607431768211455", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Very large numbers with different lengths (both overflow u128) + assert_eq!( + natural_sort( + "file999999999999999999999999999999999999999999999999", + "file9999999999999999999999999999999999999999999999999" + ), + Ordering::Less + ); + } + + #[test] + fn test_natural_sort_case_sensitive() { + // Numerically smaller values come first. + assert_eq!(natural_sort("File1", "file2"), Ordering::Less); + assert_eq!(natural_sort("file1", "File2"), Ordering::Less); + + // Numerically equal values: the case-insensitive comparison decides first. + // Case-sensitive comparison only occurs when both are equal case-insensitively. + assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less); + assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less); + assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less); + + // Numerically equal and case-insensitively equal: + // the lexicographically smaller (case-sensitive) one wins. + assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less); + assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less); + assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less); + } + + #[test] + fn test_natural_sort_edge_cases() { + // Empty strings + assert_eq!(natural_sort("", ""), Ordering::Equal); + assert_eq!(natural_sort("", "a"), Ordering::Less); + assert_eq!(natural_sort("a", ""), Ordering::Greater); + + // Special characters + assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less); + assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less); + assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less); + + // Unicode characters + // 9312 vs 9313 + assert_eq!(natural_sort("file①", "file②"), Ordering::Less); + // 9321 vs 9313 + assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater); + // 28450 vs 23383 + assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater); + + // Mixed alphanumeric with special chars + assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less); + assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less); + assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater); + } + + #[test] + fn test_multiple_extensions() { + // No extensions + let path = Path::new("/a/b/c/file_name"); + assert_eq!(path.multiple_extensions(), None); + + // Only one extension + let path = Path::new("/a/b/c/file_name.tsx"); + assert_eq!(path.multiple_extensions(), None); + + // Stories sample extension + let path = Path::new("/a/b/c/file_name.stories.tsx"); + assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string())); + + // Longer sample extension + let path = Path::new("/a/b/c/long.app.tar.gz"); + assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string())); + } + + #[test] + fn test_strip_path_suffix() { + let base = Path::new("/a/b/c/file_name"); + let suffix = Path::new("file_name"); + assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); + + let base = Path::new("/a/b/c/file_name.tsx"); + let suffix = Path::new("file_name.tsx"); + assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); + + let base = Path::new("/a/b/c/file_name.stories.tsx"); + let suffix = Path::new("c/file_name.stories.tsx"); + assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b"))); + + let base = Path::new("/a/b/c/long.app.tar.gz"); + let suffix = Path::new("b/c/long.app.tar.gz"); + assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a"))); + + let base = Path::new("/a/b/c/long.app.tar.gz"); + let suffix = Path::new("/a/b/c/long.app.tar.gz"); + assert_eq!(strip_path_suffix(base, suffix), Some(Path::new(""))); + + let base = Path::new("/a/b/c/long.app.tar.gz"); + let suffix = Path::new("/a/b/c/no_match.app.tar.gz"); + assert_eq!(strip_path_suffix(base, suffix), None); + + let base = Path::new("/a/b/c/long.app.tar.gz"); + let suffix = Path::new("app.tar.gz"); + assert_eq!(strip_path_suffix(base, suffix), None); + } + + #[test] + fn test_strip_prefix() { + let expected = [ + ( + PathStyle::Posix, + "/a/b/c", + "/a/b", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Posix, + "/a/b/c", + "/a/b/", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Posix, + "/a/b/c", + "/", + Some(rel_path("a/b/c").to_arc()), + ), + (PathStyle::Posix, "/a/b/c", "", None), + (PathStyle::Posix, "/a/b//c", "/a/b/", None), + (PathStyle::Posix, "/a/bc", "/a/b", None), + ( + PathStyle::Posix, + "/a/b/c", + "/a/b/c", + Some(rel_path("").to_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\a\\b", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\a\\b\\", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\", + Some(rel_path("a/b/c").to_arc()), + ), + (PathStyle::Windows, "C:\\a\\b\\c", "", None), + (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None), + (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b\\", + Some(rel_path("c").to_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b/", + Some(rel_path("c").to_arc()), + ), + ]; + let actual = expected.clone().map(|(style, child, parent, _)| { + ( + style, + child, + parent, + style + .strip_prefix(child.as_ref(), parent.as_ref()) + .map(|rel_path| rel_path.to_arc()), + ) + }); + pretty_assertions::assert_eq!(actual, expected); + } + + #[cfg(target_os = "windows")] + #[test] + fn test_wsl_path() { + use super::WslPath; + let path = "/a/b/c"; + assert_eq!(WslPath::from_path(&path), None); + + let path = r"\\wsl.localhost"; + assert_eq!(WslPath::from_path(&path), None); + + let path = r"\\wsl.localhost\Distro"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "Distro".to_owned(), + path: "/".into(), + }) + ); + + let path = r"\\wsl.localhost\Distro\blue"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "Distro".to_owned(), + path: "/blue".into() + }) + ); + + let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "archlinux".to_owned(), + path: "/tomato/paprika/../aubergine.txt".into() + }) + ); + + let path = r"\\windows.localhost\Distro\foo"; + assert_eq!(WslPath::from_path(&path), None); + } + + #[test] + fn test_url_to_file_path_ext_posix_basic() { + use super::UrlExt; + + let url = url::Url::parse("file:///home/user/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/home/user/file.txt")) + ); + + let url = url::Url::parse("file:///").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/")) + ); + + let url = url::Url::parse("file:///a/b/c/d/e").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/a/b/c/d/e")) + ); + } + + #[test] + fn test_url_to_file_path_ext_posix_percent_encoding() { + use super::UrlExt; + + let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/home/user/file with spaces.txt")) + ); + + let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/path/with/encoded/slashes")) + ); + + let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/special#chars?.txt")) + ); + } + + #[test] + fn test_url_to_file_path_ext_posix_localhost() { + use super::UrlExt; + + let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/home/user/file.txt")) + ); + } + + #[test] + fn test_url_to_file_path_ext_posix_rejects_host() { + use super::UrlExt; + + let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap(); + assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); + } + + #[test] + fn test_url_to_file_path_ext_posix_windows_drive_letter() { + use super::UrlExt; + + let url = url::Url::parse("file:///C:").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/C:/")) + ); + + let url = url::Url::parse("file:///D|").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Posix), + Ok(PathBuf::from("/D|/")) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_basic() { + use super::UrlExt; + + let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\Users\\user\\file.txt")) + ); + + let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs")) + ); + + let url = url::Url::parse("file:///C:/").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\")) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_encoded_drive_letter() { + use super::UrlExt; + + let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\Users\\file.txt")) + ); + + let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("c:\\Users\\file.txt")) + ); + + let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("D:\\folder\\file.txt")) + ); + + let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("d:\\folder\\file.txt")) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_unc_path() { + use super::UrlExt; + + let url = url::Url::parse("file://server/share/path/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("\\\\server\\share\\path\\file.txt")) + ); + + let url = url::Url::parse("file://server/share").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("\\\\server\\share")) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_percent_encoding() { + use super::UrlExt; + + let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt")) + ); + + let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\special#chars?.txt")) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_invalid_drive() { + use super::UrlExt; + + let url = url::Url::parse("file:///1:/path/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + + let url = url::Url::parse("file:///CC:/path/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + + let url = url::Url::parse("file:///C/path/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + + let url = url::Url::parse("file:///invalid").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + } + + #[test] + fn test_url_to_file_path_ext_non_file_scheme() { + use super::UrlExt; + + let url = url::Url::parse("http://example.com/path").unwrap(); + assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + + let url = url::Url::parse("https://example.com/path").unwrap(); + assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(ToFilePathError)); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Err(ToFilePathError) + ); + } + + #[test] + fn test_url_to_file_path_ext_windows_localhost() { + use super::UrlExt; + + let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap(); + assert_eq!( + url.to_file_path_ext(PathStyle::Windows), + Ok(PathBuf::from("C:\\Users\\file.txt")) + ); + } } diff --git a/crates/gpui_zed_util/src/process.rs b/crates/gpui_zed_util/src/process.rs index eaf543dbd8..6e7f68d640 100644 --- a/crates/gpui_zed_util/src/process.rs +++ b/crates/gpui_zed_util/src/process.rs @@ -4,89 +4,89 @@ use std::process::Stdio; /// A wrapper around `smol::process::Child` that ensures all subprocesses /// are killed when the process is terminated by using process groups. pub struct Child { - process: smol::process::Child, + process: smol::process::Child, } impl std::ops::Deref for Child { - type Target = smol::process::Child; + type Target = smol::process::Child; - fn deref(&self) -> &Self::Target { - &self.process - } + fn deref(&self) -> &Self::Target { + &self.process + } } impl std::ops::DerefMut for Child { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.process - } + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.process + } } impl Child { - #[cfg(not(windows))] - pub fn spawn( - mut command: std::process::Command, - stdin: Stdio, - stdout: Stdio, - stderr: Stdio, - ) -> Result { - crate::set_pre_exec_to_start_new_session(&mut command); - let mut command = smol::process::Command::from(command); - let process = command - .stdin(stdin) - .stdout(stdout) - .stderr(stderr) - .spawn() - .with_context(|| { - format!( - "failed to spawn command {}", - crate::redact::redact_command(&format!("{command:?}")) - ) - })?; - Ok(Self { process }) - } + #[cfg(not(windows))] + pub fn spawn( + mut command: std::process::Command, + stdin: Stdio, + stdout: Stdio, + stderr: Stdio, + ) -> Result { + crate::set_pre_exec_to_start_new_session(&mut command); + let mut command = smol::process::Command::from(command); + let process = command + .stdin(stdin) + .stdout(stdout) + .stderr(stderr) + .spawn() + .with_context(|| { + format!( + "failed to spawn command {}", + crate::redact::redact_command(&format!("{command:?}")) + ) + })?; + Ok(Self { process }) + } - #[cfg(windows)] - pub fn spawn( - command: std::process::Command, - stdin: Stdio, - stdout: Stdio, - stderr: Stdio, - ) -> Result { - // TODO(windows): create a job object and add the child process handle to it, - // see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects - let mut command = smol::process::Command::from(command); - let process = command - .stdin(stdin) - .stdout(stdout) - .stderr(stderr) - .spawn() - .with_context(|| { - format!( - "failed to spawn command {}", - crate::redact::redact_command(&format!("{command:?}")) - ) - })?; + #[cfg(windows)] + pub fn spawn( + command: std::process::Command, + stdin: Stdio, + stdout: Stdio, + stderr: Stdio, + ) -> Result { + // TODO(windows): create a job object and add the child process handle to it, + // see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects + let mut command = smol::process::Command::from(command); + let process = command + .stdin(stdin) + .stdout(stdout) + .stderr(stderr) + .spawn() + .with_context(|| { + format!( + "failed to spawn command {}", + crate::redact::redact_command(&format!("{command:?}")) + ) + })?; - Ok(Self { process }) - } + Ok(Self { process }) + } - pub fn into_inner(self) -> smol::process::Child { - self.process - } + pub fn into_inner(self) -> smol::process::Child { + self.process + } - #[cfg(not(windows))] - pub fn kill(&mut self) -> Result<()> { - let pid = self.process.id(); - unsafe { - libc::killpg(pid as i32, libc::SIGKILL); - } - Ok(()) - } + #[cfg(not(windows))] + pub fn kill(&mut self) -> Result<()> { + let pid = self.process.id(); + unsafe { + libc::killpg(pid as i32, libc::SIGKILL); + } + Ok(()) + } - #[cfg(windows)] - pub fn kill(&mut self) -> Result<()> { - // TODO(windows): terminate the job object in kill - self.process.kill()?; - Ok(()) - } + #[cfg(windows)] + pub fn kill(&mut self) -> Result<()> { + // TODO(windows): terminate the job object in kill + self.process.kill()?; + Ok(()) + } } diff --git a/crates/gpui_zed_util/src/redact.rs b/crates/gpui_zed_util/src/redact.rs index ad11f7618b..b0f1c0bada 100644 --- a/crates/gpui_zed_util/src/redact.rs +++ b/crates/gpui_zed_util/src/redact.rs @@ -1,49 +1,49 @@ use std::sync::LazyLock; static REDACT_REGEX: LazyLock = LazyLock::new(|| { - regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap() + regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap() }); /// Whether a given environment variable name should have its value redacted pub fn should_redact(env_var_name: &str) -> bool { - const REDACTED_SUFFIXES: &[&str] = &[ - "KEY", - "TOKEN", - "PASSWORD", - "SECRET", - "PASS", - "CREDENTIALS", - "LICENSE", - ]; - REDACTED_SUFFIXES - .iter() - .any(|suffix| env_var_name.ends_with(suffix)) + const REDACTED_SUFFIXES: &[&str] = &[ + "KEY", + "TOKEN", + "PASSWORD", + "SECRET", + "PASS", + "CREDENTIALS", + "LICENSE", + ]; + REDACTED_SUFFIXES + .iter() + .any(|suffix| env_var_name.ends_with(suffix)) } /// Redact a string which could include a command with environment variables pub fn redact_command(command: &str) -> String { - REDACT_REGEX - .replace_all(command, |caps: ®ex::Captures| { - let var_name = &caps[1]; - let value = &caps[2]; - if should_redact(var_name) { - format!(r#"{}="[REDACTED]""#, var_name) - } else { - format!("{}={}", var_name, value) - } - }) - .to_string() + REDACT_REGEX + .replace_all(command, |caps: ®ex::Captures| { + let var_name = &caps[1]; + let value = &caps[2]; + if should_redact(var_name) { + format!(r#"{}="[REDACTED]""#, var_name) + } else { + format!("{}={}", var_name, value) + } + }) + .to_string() } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_redact_string_with_multiple_env_vars() { - let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#; - let result = redact_command(input); - let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#; - assert_eq!(result, expected); - } + #[test] + fn test_redact_string_with_multiple_env_vars() { + let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#; + let result = redact_command(input); + let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#; + assert_eq!(result, expected); + } } diff --git a/crates/gpui_zed_util/src/rel_path.rs b/crates/gpui_zed_util/src/rel_path.rs index bd08473623..40268afe4b 100644 --- a/crates/gpui_zed_util/src/rel_path.rs +++ b/crates/gpui_zed_util/src/rel_path.rs @@ -2,11 +2,11 @@ use crate::paths::{PathStyle, is_absolute}; use anyhow::{Context as _, Result, anyhow}; use serde::{Deserialize, Serialize}; use std::{ - borrow::{Borrow, Cow}, - fmt, - ops::Deref, - path::{Path, PathBuf}, - sync::Arc, + borrow::{Borrow, Cow}, + fmt, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, }; /// A file system path that is guaranteed to be relative and normalized. @@ -31,406 +31,406 @@ pub struct RelPath(str); pub struct RelPathBuf(String); impl RelPath { - /// Creates an empty [`RelPath`]. - pub fn empty() -> &'static Self { - Self::new_unchecked("") - } + /// Creates an empty [`RelPath`]. + pub fn empty() -> &'static Self { + Self::new_unchecked("") + } - /// Converts a path with a given style into a [`RelPath`]. - /// - /// Returns an error if the path is absolute, or is not valid unicode. - /// - /// This method will normalize the path by removing `.` components, - /// processing `..` components, and removing trailing separators. It does - /// not allocate unless it's necessary to reformat the path. - #[track_caller] - pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result> { - let mut path = path.to_str().context("non utf-8 path")?; + /// Converts a path with a given style into a [`RelPath`]. + /// + /// Returns an error if the path is absolute, or is not valid unicode. + /// + /// This method will normalize the path by removing `.` components, + /// processing `..` components, and removing trailing separators. It does + /// not allocate unless it's necessary to reformat the path. + #[track_caller] + pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result> { + let mut path = path.to_str().context("non utf-8 path")?; - let (prefixes, suffixes): (&[_], &[_]) = match path_style { - PathStyle::Posix => (&["./"], &['/']), - PathStyle::Windows => (&["./", ".\\"], &['/', '\\']), - }; + let (prefixes, suffixes): (&[_], &[_]) = match path_style { + PathStyle::Posix => (&["./"], &['/']), + PathStyle::Windows => (&["./", ".\\"], &['/', '\\']), + }; - while prefixes.iter().any(|prefix| path.starts_with(prefix)) { - path = &path[prefixes[0].len()..]; - } - while let Some(prefix) = path.strip_suffix(suffixes) - && !prefix.is_empty() - { - path = prefix; - } + while prefixes.iter().any(|prefix| path.starts_with(prefix)) { + path = &path[prefixes[0].len()..]; + } + while let Some(prefix) = path.strip_suffix(suffixes) + && !prefix.is_empty() + { + path = prefix; + } - if is_absolute(path, path_style) { - return Err(anyhow!("absolute path not allowed: {path:?}")); - } + if is_absolute(path, path_style) { + return Err(anyhow!("absolute path not allowed: {path:?}")); + } - let mut string = Cow::Borrowed(path); - if path_style == PathStyle::Windows && path.contains('\\') { - string = Cow::Owned(string.as_ref().replace('\\', "/")) - } + let mut string = Cow::Borrowed(path); + if path_style == PathStyle::Windows && path.contains('\\') { + string = Cow::Owned(string.as_ref().replace('\\', "/")) + } - let mut result = match string { - Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)), - Cow::Owned(string) => Cow::Owned(RelPathBuf(string)), - }; + let mut result = match string { + Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)), + Cow::Owned(string) => Cow::Owned(RelPathBuf(string)), + }; - if result - .components() - .any(|component| component.is_empty() || component == "." || component == "..") - { - let mut normalized = RelPathBuf::new(); - for component in result.components() { - match component { - "" => {} - "." => {} - ".." => { - if !normalized.pop() { - return Err(anyhow!("path is not relative: {result:?}")); - } - } - other => normalized.push(RelPath::new_unchecked(other)), - } - } - result = Cow::Owned(normalized) - } + if result + .components() + .any(|component| component.is_empty() || component == "." || component == "..") + { + let mut normalized = RelPathBuf::new(); + for component in result.components() { + match component { + "" => {} + "." => {} + ".." => { + if !normalized.pop() { + return Err(anyhow!("path is not relative: {result:?}")); + } + } + other => normalized.push(RelPath::new_unchecked(other)), + } + } + result = Cow::Owned(normalized) + } - Ok(result) - } + Ok(result) + } - /// Converts a path that is already normalized and uses '/' separators - /// into a [`RelPath`] . - /// - /// Returns an error if the path is not already in the correct format. - #[track_caller] - pub fn unix + ?Sized>(path: &S) -> anyhow::Result<&Self> { - let path = path.as_ref(); - match Self::new(path, PathStyle::Posix)? { - Cow::Borrowed(path) => Ok(path), - Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")), - } - } + /// Converts a path that is already normalized and uses '/' separators + /// into a [`RelPath`] . + /// + /// Returns an error if the path is not already in the correct format. + #[track_caller] + pub fn unix + ?Sized>(path: &S) -> anyhow::Result<&Self> { + let path = path.as_ref(); + match Self::new(path, PathStyle::Posix)? { + Cow::Borrowed(path) => Ok(path), + Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")), + } + } - fn new_unchecked(s: &str) -> &Self { - // Safety: `RelPath` is a transparent wrapper around `str`. - unsafe { &*(s as *const str as *const Self) } - } + fn new_unchecked(s: &str) -> &Self { + // Safety: `RelPath` is a transparent wrapper around `str`. + unsafe { &*(s as *const str as *const Self) } + } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } - pub fn components(&self) -> RelPathComponents<'_> { - RelPathComponents(&self.0) - } + pub fn components(&self) -> RelPathComponents<'_> { + RelPathComponents(&self.0) + } - pub fn ancestors(&self) -> RelPathAncestors<'_> { - RelPathAncestors(Some(&self.0)) - } + pub fn ancestors(&self) -> RelPathAncestors<'_> { + RelPathAncestors(Some(&self.0)) + } - pub fn file_name(&self) -> Option<&str> { - self.components().next_back() - } + pub fn file_name(&self) -> Option<&str> { + self.components().next_back() + } - pub fn file_stem(&self) -> Option<&str> { - Some(self.as_std_path().file_stem()?.to_str().unwrap()) - } + pub fn file_stem(&self) -> Option<&str> { + Some(self.as_std_path().file_stem()?.to_str().unwrap()) + } - pub fn extension(&self) -> Option<&str> { - Some(self.as_std_path().extension()?.to_str().unwrap()) - } + pub fn extension(&self) -> Option<&str> { + Some(self.as_std_path().extension()?.to_str().unwrap()) + } - pub fn parent(&self) -> Option<&Self> { - let mut components = self.components(); - components.next_back()?; - Some(components.rest()) - } + pub fn parent(&self) -> Option<&Self> { + let mut components = self.components(); + components.next_back()?; + Some(components.rest()) + } - pub fn starts_with(&self, other: &Self) -> bool { - self.strip_prefix(other).is_ok() - } + pub fn starts_with(&self, other: &Self) -> bool { + self.strip_prefix(other).is_ok() + } - pub fn ends_with(&self, other: &Self) -> bool { - self.0 - .strip_suffix(&other.0) - .is_some_and(|suffix| suffix.ends_with('/') || suffix.is_empty()) - } + pub fn ends_with(&self, other: &Self) -> bool { + self.0 + .strip_suffix(&other.0) + .is_some_and(|suffix| suffix.ends_with('/') || suffix.is_empty()) + } - pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> { - if other.is_empty() { - return Ok(self); - } - if let Some(suffix) = self.0.strip_prefix(&other.0) { - if let Some(suffix) = suffix.strip_prefix('/') { - return Ok(Self::new_unchecked(suffix)); - } else if suffix.is_empty() { - return Ok(Self::empty()); - } - } - Err(StripPrefixError) - } + pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> { + if other.is_empty() { + return Ok(self); + } + if let Some(suffix) = self.0.strip_prefix(&other.0) { + if let Some(suffix) = suffix.strip_prefix('/') { + return Ok(Self::new_unchecked(suffix)); + } else if suffix.is_empty() { + return Ok(Self::empty()); + } + } + Err(StripPrefixError) + } - pub fn len(&self) -> usize { - self.0.matches('/').count() + 1 - } + pub fn len(&self) -> usize { + self.0.matches('/').count() + 1 + } - pub fn last_n_components(&self, count: usize) -> Option<&Self> { - let len = self.len(); - if len >= count { - let mut components = self.components(); - for _ in 0..(len - count) { - components.next()?; - } - Some(components.rest()) - } else { - None - } - } + pub fn last_n_components(&self, count: usize) -> Option<&Self> { + let len = self.len(); + if len >= count { + let mut components = self.components(); + for _ in 0..(len - count) { + components.next()?; + } + Some(components.rest()) + } else { + None + } + } - pub fn join(&self, other: &Self) -> Arc { - let result = if self.0.is_empty() { - Cow::Borrowed(&other.0) - } else if other.0.is_empty() { - Cow::Borrowed(&self.0) - } else { - Cow::Owned(format!("{}/{}", &self.0, &other.0)) - }; - Arc::from(Self::new_unchecked(result.as_ref())) - } + pub fn join(&self, other: &Self) -> Arc { + let result = if self.0.is_empty() { + Cow::Borrowed(&other.0) + } else if other.0.is_empty() { + Cow::Borrowed(&self.0) + } else { + Cow::Owned(format!("{}/{}", &self.0, &other.0)) + }; + Arc::from(Self::new_unchecked(result.as_ref())) + } - pub fn to_rel_path_buf(&self) -> RelPathBuf { - RelPathBuf(self.0.to_string()) - } + pub fn to_rel_path_buf(&self) -> RelPathBuf { + RelPathBuf(self.0.to_string()) + } - pub fn to_arc(&self) -> Arc { - Arc::from(self) - } + pub fn to_arc(&self) -> Arc { + Arc::from(self) + } - /// Convert the path into the wire representation. - pub fn to_proto(&self) -> String { - self.as_unix_str().to_owned() - } + /// Convert the path into the wire representation. + pub fn to_proto(&self) -> String { + self.as_unix_str().to_owned() + } - /// Load the path from its wire representation. - pub fn from_proto(path: &str) -> Result> { - Ok(Arc::from(Self::unix(path)?)) - } + /// Load the path from its wire representation. + pub fn from_proto(path: &str) -> Result> { + Ok(Arc::from(Self::unix(path)?)) + } - /// Convert the path into a string with the given path style. - /// - /// Whenever a path is presented to the user, it should be converted to - /// a string via this method. - pub fn display(&self, style: PathStyle) -> Cow<'_, str> { - match style { - PathStyle::Posix => Cow::Borrowed(&self.0), - PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")), - PathStyle::Windows => Cow::Borrowed(&self.0), - } - } + /// Convert the path into a string with the given path style. + /// + /// Whenever a path is presented to the user, it should be converted to + /// a string via this method. + pub fn display(&self, style: PathStyle) -> Cow<'_, str> { + match style { + PathStyle::Posix => Cow::Borrowed(&self.0), + PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")), + PathStyle::Windows => Cow::Borrowed(&self.0), + } + } - /// Get the internal unix-style representation of the path. - /// - /// This should not be shown to the user. - pub fn as_unix_str(&self) -> &str { - &self.0 - } + /// Get the internal unix-style representation of the path. + /// + /// This should not be shown to the user. + pub fn as_unix_str(&self) -> &str { + &self.0 + } - /// Interprets the path as a [`std::path::Path`], suitable for file system calls. - /// - /// This is guaranteed to be a valid path regardless of the host platform, because - /// the `/` is accepted as a path separator on windows. - /// - /// This should not be shown to the user. - pub fn as_std_path(&self) -> &Path { - Path::new(&self.0) - } + /// Interprets the path as a [`std::path::Path`], suitable for file system calls. + /// + /// This is guaranteed to be a valid path regardless of the host platform, because + /// the `/` is accepted as a path separator on windows. + /// + /// This should not be shown to the user. + pub fn as_std_path(&self) -> &Path { + Path::new(&self.0) + } } #[derive(Debug)] pub struct StripPrefixError; impl std::fmt::Display for StripPrefixError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("prefix not found") - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("prefix not found") + } } impl std::error::Error for StripPrefixError {} impl ToOwned for RelPath { - type Owned = RelPathBuf; + type Owned = RelPathBuf; - fn to_owned(&self) -> Self::Owned { - self.to_rel_path_buf() - } + fn to_owned(&self) -> Self::Owned { + self.to_rel_path_buf() + } } impl Borrow for RelPathBuf { - fn borrow(&self) -> &RelPath { - self.as_rel_path() - } + fn borrow(&self) -> &RelPath { + self.as_rel_path() + } } impl PartialOrd for RelPath { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } impl Ord for RelPath { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.components().cmp(other.components()) - } + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.components().cmp(other.components()) + } } impl fmt::Debug for RelPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } } impl fmt::Debug for RelPathBuf { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } } impl Default for RelPathBuf { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl RelPathBuf { - pub fn new() -> Self { - Self(String::new()) - } + pub fn new() -> Self { + Self(String::new()) + } - pub fn pop(&mut self) -> bool { - if let Some(ix) = self.0.rfind('/') { - self.0.truncate(ix); - true - } else if !self.is_empty() { - self.0.clear(); - true - } else { - false - } - } + pub fn pop(&mut self) -> bool { + if let Some(ix) = self.0.rfind('/') { + self.0.truncate(ix); + true + } else if !self.is_empty() { + self.0.clear(); + true + } else { + false + } + } - pub fn push(&mut self, path: &RelPath) { - if !self.is_empty() { - self.0.push('/'); - } - self.0.push_str(&path.0); - } + pub fn push(&mut self, path: &RelPath) { + if !self.is_empty() { + self.0.push('/'); + } + self.0.push_str(&path.0); + } - pub fn as_rel_path(&self) -> &RelPath { - RelPath::new_unchecked(self.0.as_str()) - } + pub fn as_rel_path(&self) -> &RelPath { + RelPath::new_unchecked(self.0.as_str()) + } - pub fn set_extension(&mut self, extension: &str) -> bool { - if let Some(filename) = self.file_name() { - let mut filename = PathBuf::from(filename); - filename.set_extension(extension); - self.pop(); - self.0.push_str(filename.to_str().unwrap()); - true - } else { - false - } - } + pub fn set_extension(&mut self, extension: &str) -> bool { + if let Some(filename) = self.file_name() { + let mut filename = PathBuf::from(filename); + filename.set_extension(extension); + self.pop(); + self.0.push_str(filename.to_str().unwrap()); + true + } else { + false + } + } } impl<'de> Deserialize<'de> for RelPathBuf { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - let path = String::deserialize(deserializer)?; - let rel_path = - RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?; - Ok(rel_path.into_owned()) - } + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let path = String::deserialize(deserializer)?; + let rel_path = + RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?; + Ok(rel_path.into_owned()) + } } impl From for Arc { - fn from(value: RelPathBuf) -> Self { - Arc::from(value.as_rel_path()) - } + fn from(value: RelPathBuf) -> Self { + Arc::from(value.as_rel_path()) + } } impl AsRef for RelPathBuf { - fn as_ref(&self) -> &Path { - self.as_std_path() - } + fn as_ref(&self) -> &Path { + self.as_std_path() + } } impl AsRef for RelPath { - fn as_ref(&self) -> &Path { - self.as_std_path() - } + fn as_ref(&self) -> &Path { + self.as_std_path() + } } impl AsRef for RelPathBuf { - fn as_ref(&self) -> &RelPath { - self.as_rel_path() - } + fn as_ref(&self) -> &RelPath { + self.as_rel_path() + } } impl AsRef for RelPath { - fn as_ref(&self) -> &RelPath { - self - } + fn as_ref(&self) -> &RelPath { + self + } } impl Deref for RelPathBuf { - type Target = RelPath; + type Target = RelPath; - fn deref(&self) -> &Self::Target { - self.as_ref() - } + fn deref(&self) -> &Self::Target { + self.as_ref() + } } impl<'a> From<&'a RelPath> for Cow<'a, RelPath> { - fn from(value: &'a RelPath) -> Self { - Self::Borrowed(value) - } + fn from(value: &'a RelPath) -> Self { + Self::Borrowed(value) + } } impl From<&RelPath> for Arc { - fn from(rel_path: &RelPath) -> Self { - let bytes: Arc = Arc::from(&rel_path.0); - unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) } - } + fn from(rel_path: &RelPath) -> Self { + let bytes: Arc = Arc::from(&rel_path.0); + unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) } + } } #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn rel_path(path: &str) -> &RelPath { - RelPath::unix(path).unwrap() + RelPath::unix(path).unwrap() } #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn rel_path_buf(path: &str) -> RelPathBuf { - RelPath::unix(path).unwrap().to_rel_path_buf() + RelPath::unix(path).unwrap().to_rel_path_buf() } impl PartialEq for RelPath { - fn eq(&self, other: &str) -> bool { - self.0 == *other - } + fn eq(&self, other: &str) -> bool { + self.0 == *other + } } pub trait PathExt { - fn to_rel_path_buf(&self) -> Result; + fn to_rel_path_buf(&self) -> Result; } impl + ?Sized> PathExt for T { - fn to_rel_path_buf(&self) -> Result { - Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned()) - } + fn to_rel_path_buf(&self) -> Result { + Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned()) + } } #[derive(Default)] @@ -441,197 +441,197 @@ pub struct RelPathAncestors<'a>(Option<&'a str>); const SEPARATOR: char = '/'; impl<'a> RelPathComponents<'a> { - pub fn rest(&self) -> &'a RelPath { - RelPath::new_unchecked(self.0) - } + pub fn rest(&self) -> &'a RelPath { + RelPath::new_unchecked(self.0) + } } impl<'a> Iterator for RelPathComponents<'a> { - type Item = &'a str; + type Item = &'a str; - fn next(&mut self) -> Option { - if let Some(sep_ix) = self.0.find(SEPARATOR) { - let (head, tail) = self.0.split_at(sep_ix); - self.0 = &tail[1..]; - Some(head) - } else if self.0.is_empty() { - None - } else { - let result = self.0; - self.0 = ""; - Some(result) - } - } + fn next(&mut self) -> Option { + if let Some(sep_ix) = self.0.find(SEPARATOR) { + let (head, tail) = self.0.split_at(sep_ix); + self.0 = &tail[1..]; + Some(head) + } else if self.0.is_empty() { + None + } else { + let result = self.0; + self.0 = ""; + Some(result) + } + } } impl<'a> Iterator for RelPathAncestors<'a> { - type Item = &'a RelPath; + type Item = &'a RelPath; - fn next(&mut self) -> Option { - let result = self.0?; - if let Some(sep_ix) = result.rfind(SEPARATOR) { - self.0 = Some(&result[..sep_ix]); - } else if !result.is_empty() { - self.0 = Some(""); - } else { - self.0 = None; - } - Some(RelPath::new_unchecked(result)) - } + fn next(&mut self) -> Option { + let result = self.0?; + if let Some(sep_ix) = result.rfind(SEPARATOR) { + self.0 = Some(&result[..sep_ix]); + } else if !result.is_empty() { + self.0 = Some(""); + } else { + self.0 = None; + } + Some(RelPath::new_unchecked(result)) + } } impl<'a> DoubleEndedIterator for RelPathComponents<'a> { - fn next_back(&mut self) -> Option { - if let Some(sep_ix) = self.0.rfind(SEPARATOR) { - let (head, tail) = self.0.split_at(sep_ix); - self.0 = head; - Some(&tail[1..]) - } else if self.0.is_empty() { - None - } else { - let result = self.0; - self.0 = ""; - Some(result) - } - } + fn next_back(&mut self) -> Option { + if let Some(sep_ix) = self.0.rfind(SEPARATOR) { + let (head, tail) = self.0.split_at(sep_ix); + self.0 = head; + Some(&tail[1..]) + } else if self.0.is_empty() { + None + } else { + let result = self.0; + self.0 = ""; + Some(result) + } + } } #[cfg(test)] mod tests { - use super::*; - use itertools::Itertools; - use pretty_assertions::assert_matches; + use super::*; + use itertools::Itertools; + use pretty_assertions::assert_matches; - #[test] - fn test_rel_path_new() { - assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err()); - assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err()); - assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err()); + #[test] + fn test_rel_path_new() { + assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err()); + assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err()); + assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err()); - let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap(); + assert_eq!(path, rel_path("foo").into()); + assert_matches!(path, Cow::Borrowed(_)); - let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap(); + assert_eq!(path, rel_path("foo").into()); + assert_matches!(path, Cow::Borrowed(_)); - assert_eq!( - RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local()) - .unwrap() - .as_ref(), - rel_path("foo/baz/quux") - ); + assert_eq!( + RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local()) + .unwrap() + .as_ref(), + rel_path("foo/baz/quux") + ); - let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap(); + assert_eq!(path.as_ref(), rel_path("foo/bar")); + assert_matches!(path, Cow::Borrowed(_)); - let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap(); + assert_eq!(path, rel_path("foo").into()); + assert_matches!(path, Cow::Borrowed(_)); - let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap(); + assert_eq!(path, rel_path("foo").into()); + assert_matches!(path, Cow::Borrowed(_)); - let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Owned(_)); + let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap(); + assert_eq!(path.as_ref(), rel_path("foo/bar")); + assert_matches!(path, Cow::Owned(_)); - let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Borrowed(_)); + let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap(); + assert_eq!(path.as_ref(), rel_path("foo/bar")); + assert_matches!(path, Cow::Borrowed(_)); - let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Owned(_)); - } + let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap(); + assert_eq!(path.as_ref(), rel_path("foo/bar")); + assert_matches!(path, Cow::Owned(_)); + } - #[test] - fn test_rel_path_components() { - let path = rel_path("foo/bar/baz"); - assert_eq!( - path.components().collect::>(), - vec!["foo", "bar", "baz"] - ); - assert_eq!( - path.components().rev().collect::>(), - vec!["baz", "bar", "foo"] - ); + #[test] + fn test_rel_path_components() { + let path = rel_path("foo/bar/baz"); + assert_eq!( + path.components().collect::>(), + vec!["foo", "bar", "baz"] + ); + assert_eq!( + path.components().rev().collect::>(), + vec!["baz", "bar", "foo"] + ); - let path = rel_path(""); - let mut components = path.components(); - assert_eq!(components.next(), None); - } + let path = rel_path(""); + let mut components = path.components(); + assert_eq!(components.next(), None); + } - #[test] - fn test_rel_path_ancestors() { - let path = rel_path("foo/bar/baz"); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz"))); - assert_eq!(ancestors.next(), Some(rel_path("foo/bar"))); - assert_eq!(ancestors.next(), Some(rel_path("foo"))); - assert_eq!(ancestors.next(), Some(rel_path(""))); - assert_eq!(ancestors.next(), None); + #[test] + fn test_rel_path_ancestors() { + let path = rel_path("foo/bar/baz"); + let mut ancestors = path.ancestors(); + assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz"))); + assert_eq!(ancestors.next(), Some(rel_path("foo/bar"))); + assert_eq!(ancestors.next(), Some(rel_path("foo"))); + assert_eq!(ancestors.next(), Some(rel_path(""))); + assert_eq!(ancestors.next(), None); - let path = rel_path("foo"); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(rel_path("foo"))); - assert_eq!(ancestors.next(), Some(RelPath::empty())); - assert_eq!(ancestors.next(), None); + let path = rel_path("foo"); + let mut ancestors = path.ancestors(); + assert_eq!(ancestors.next(), Some(rel_path("foo"))); + assert_eq!(ancestors.next(), Some(RelPath::empty())); + assert_eq!(ancestors.next(), None); - let path = RelPath::empty(); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(RelPath::empty())); - assert_eq!(ancestors.next(), None); - } + let path = RelPath::empty(); + let mut ancestors = path.ancestors(); + assert_eq!(ancestors.next(), Some(RelPath::empty())); + assert_eq!(ancestors.next(), None); + } - #[test] - fn test_rel_path_parent() { - assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar"))); - assert_eq!(rel_path("foo").parent(), Some(RelPath::empty())); - assert_eq!(rel_path("").parent(), None); - } + #[test] + fn test_rel_path_parent() { + assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar"))); + assert_eq!(rel_path("foo").parent(), Some(RelPath::empty())); + assert_eq!(rel_path("").parent(), None); + } - #[test] - fn test_rel_path_partial_ord_is_compatible_with_std() { - let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"]; - for [lhs, rhs] in test_cases.iter().array_combinations::<2>() { - assert_eq!( - Path::new(lhs).cmp(Path::new(rhs)), - RelPath::unix(lhs).unwrap().cmp(RelPath::unix(rhs).unwrap()) - ); - } - } + #[test] + fn test_rel_path_partial_ord_is_compatible_with_std() { + let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"]; + for [lhs, rhs] in test_cases.iter().array_combinations::<2>() { + assert_eq!( + Path::new(lhs).cmp(Path::new(rhs)), + RelPath::unix(lhs).unwrap().cmp(RelPath::unix(rhs).unwrap()) + ); + } + } - #[test] - fn test_strip_prefix() { - let parent = rel_path(""); - let child = rel_path(".foo"); + #[test] + fn test_strip_prefix() { + let parent = rel_path(""); + let child = rel_path(".foo"); - assert!(child.starts_with(parent)); - assert_eq!(child.strip_prefix(parent).unwrap(), child); - } + assert!(child.starts_with(parent)); + assert_eq!(child.strip_prefix(parent).unwrap(), child); + } - #[test] - fn test_rel_path_constructors_absolute_path() { - assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err()); - assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok()); - } + #[test] + fn test_rel_path_constructors_absolute_path() { + assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err()); + assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err()); + assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err()); + assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err()); + assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err()); + assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok()); + } - #[test] - fn test_pop() { - let mut path = rel_path("a/b").to_rel_path_buf(); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), "a"); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), ""); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), ""); - } + #[test] + fn test_pop() { + let mut path = rel_path("a/b").to_rel_path_buf(); + path.pop(); + assert_eq!(path.as_rel_path().as_unix_str(), "a"); + path.pop(); + assert_eq!(path.as_rel_path().as_unix_str(), ""); + path.pop(); + assert_eq!(path.as_rel_path().as_unix_str(), ""); + } } diff --git a/crates/gpui_zed_util/src/schemars.rs b/crates/gpui_zed_util/src/schemars.rs index 8124ca8cfe..fff29902df 100644 --- a/crates/gpui_zed_util/src/schemars.rs +++ b/crates/gpui_zed_util/src/schemars.rs @@ -12,27 +12,27 @@ const DEFS_PATH: &str = "#/$defs/"; /// This takes a closure for `schema` because some settings types are not available on the remote /// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. pub fn replace_subschema( - generator: &mut schemars::SchemaGenerator, - schema: impl Fn() -> schemars::Schema, + generator: &mut schemars::SchemaGenerator, + schema: impl Fn() -> schemars::Schema, ) -> schemars::Schema { - let schema_name = T::schema_name(); - let definitions = generator.definitions_mut(); - assert!(!definitions.contains_key(&format!("{schema_name}2"))); - assert!(definitions.contains_key(schema_name.as_ref())); - definitions.insert(schema_name.to_string(), schema().to_value()); - schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) + let schema_name = T::schema_name(); + let definitions = generator.definitions_mut(); + assert!(!definitions.contains_key(&format!("{schema_name}2"))); + assert!(definitions.contains_key(schema_name.as_ref())); + definitions.insert(schema_name.to_string(), schema().to_value()); + schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) } /// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is /// already in use. pub fn add_new_subschema( - generator: &mut schemars::SchemaGenerator, - name: &str, - schema: serde_json::Value, + generator: &mut schemars::SchemaGenerator, + name: &str, + schema: serde_json::Value, ) -> schemars::Schema { - let old_definition = generator.definitions_mut().insert(name.to_string(), schema); - assert_eq!(old_definition, None); - schemars::Schema::new_ref(format!("{DEFS_PATH}{name}")) + let old_definition = generator.definitions_mut().insert(name.to_string(), schema); + assert_eq!(old_definition, None); + schemars::Schema::new_ref(format!("{DEFS_PATH}{name}")) } /// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every @@ -42,16 +42,16 @@ pub fn add_new_subschema( pub struct DefaultDenyUnknownFields; impl schemars::transform::Transform for DefaultDenyUnknownFields { - fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() - && object.contains_key("properties") - && !object.contains_key("additionalProperties") - && !object.contains_key("unevaluatedProperties") - { - object.insert("additionalProperties".to_string(), false.into()); - } - transform_subschemas(self, schema); - } + fn transform(&mut self, schema: &mut schemars::Schema) { + if let Some(object) = schema.as_object_mut() + && object.contains_key("properties") + && !object.contains_key("additionalProperties") + && !object.contains_key("unevaluatedProperties") + { + object.insert("additionalProperties".to_string(), false.into()); + } + transform_subschemas(self, schema); + } } /// Defaults `allowTrailingCommas` to `true`, for use with `json-language-server`. @@ -62,11 +62,11 @@ impl schemars::transform::Transform for DefaultDenyUnknownFields { pub struct AllowTrailingCommas; impl schemars::transform::Transform for AllowTrailingCommas { - fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() - && !object.contains_key("allowTrailingCommas") - { - object.insert("allowTrailingCommas".to_string(), true.into()); - } - } + fn transform(&mut self, schema: &mut schemars::Schema) { + if let Some(object) = schema.as_object_mut() + && !object.contains_key("allowTrailingCommas") + { + object.insert("allowTrailingCommas".to_string(), true.into()); + } + } } diff --git a/crates/gpui_zed_util/src/serde.rs b/crates/gpui_zed_util/src/serde.rs index 4aa4bb1a49..4b2002e499 100644 --- a/crates/gpui_zed_util/src/serde.rs +++ b/crates/gpui_zed_util/src/serde.rs @@ -1,7 +1,7 @@ pub const fn default_true() -> bool { - true + true } pub fn is_default(value: &T) -> bool { - *value == T::default() + *value == T::default() } diff --git a/crates/gpui_zed_util/src/shell.rs b/crates/gpui_zed_util/src/shell.rs index 31bb08586c..487aef1e3e 100644 --- a/crates/gpui_zed_util/src/shell.rs +++ b/crates/gpui_zed_util/src/shell.rs @@ -6,1046 +6,1035 @@ use std::{borrow::Cow, fmt, path::Path, sync::LazyLock}; #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, + /// Use the system's default terminal configuration in /etc/passwd + #[default] + System, + /// Use a specific program with no arguments. + Program(String), + /// Use a specific program with arguments. + WithArguments { + /// The program to run. + program: String, + /// The arguments to pass to the program. + args: Vec, + /// An optional string to override the title of the terminal tab + title_override: Option, + }, } impl Shell { - pub fn program(&self) -> String { - match self { - Shell::Program(program) => program.clone(), - Shell::WithArguments { program, .. } => program.clone(), - Shell::System => get_system_shell(), - } - } + pub fn program(&self) -> String { + match self { + Shell::Program(program) => program.clone(), + Shell::WithArguments { program, .. } => program.clone(), + Shell::System => get_system_shell(), + } + } - pub fn program_and_args(&self) -> (String, &[String]) { - match self { - Shell::Program(program) => (program.clone(), &[]), - Shell::WithArguments { program, args, .. } => (program.clone(), args), - Shell::System => (get_system_shell(), &[]), - } - } + pub fn program_and_args(&self) -> (String, &[String]) { + match self { + Shell::Program(program) => (program.clone(), &[]), + Shell::WithArguments { program, args, .. } => (program.clone(), args), + Shell::System => (get_system_shell(), &[]), + } + } - pub fn shell_kind(&self, is_windows: bool) -> ShellKind { - match self { - Shell::Program(program) => ShellKind::new(program, is_windows), - Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), - Shell::System => ShellKind::system(), - } - } + pub fn shell_kind(&self, is_windows: bool) -> ShellKind { + match self { + Shell::Program(program) => ShellKind::new(program, is_windows), + Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), + Shell::System => ShellKind::system(), + } + } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ShellKind { - #[default] - Posix, - Csh, - Tcsh, - Rc, - Fish, - /// Pre-installed "legacy" powershell for windows - PowerShell, - /// PowerShell 7.x - Pwsh, - Nushell, - Cmd, - Xonsh, - Elvish, + #[default] + Posix, + Csh, + Tcsh, + Rc, + Fish, + /// Pre-installed "legacy" powershell for windows + PowerShell, + /// PowerShell 7.x + Pwsh, + Nushell, + Cmd, + Xonsh, + Elvish, } pub fn get_system_shell() -> String { - if cfg!(windows) { - get_windows_system_shell() - } else { - std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) - } + if cfg!(windows) { + get_windows_system_shell() + } else { + std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) + } } pub fn get_default_system_shell() -> String { - if cfg!(windows) { - get_windows_system_shell() - } else { - "/bin/sh".to_string() - } + if cfg!(windows) { + get_windows_system_shell() + } else { + "/bin/sh".to_string() + } } /// Get the default system shell, preferring bash on Windows. pub fn get_default_system_shell_preferring_bash() -> String { - if cfg!(windows) { - get_windows_bash().unwrap_or_else(get_windows_system_shell) - } else { - "/bin/sh".to_string() - } + if cfg!(windows) { + get_windows_bash().unwrap_or_else(get_windows_system_shell) + } else { + "/bin/sh".to_string() + } } pub fn get_windows_bash() -> Option { - use std::path::PathBuf; + use std::path::PathBuf; - fn find_bash_in_scoop() -> Option { - let bash_exe = - PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe"); - bash_exe.exists().then_some(bash_exe) - } + fn find_bash_in_scoop() -> Option { + let bash_exe = + PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe"); + bash_exe.exists().then_some(bash_exe) + } - fn find_bash_in_git() -> Option { - // /path/to/git/cmd/git.exe/../../bin/bash.exe - let git = which::which("git").ok()?; - let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe"); - git_bash.exists().then_some(git_bash) - } + fn find_bash_in_git() -> Option { + // /path/to/git/cmd/git.exe/../../bin/bash.exe + let git = which::which("git").ok()?; + let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe"); + git_bash.exists().then_some(git_bash) + } - static BASH: LazyLock> = LazyLock::new(|| { - let bash = find_bash_in_scoop() - .or_else(find_bash_in_git) - .map(|p| p.to_string_lossy().into_owned()); - if let Some(ref path) = bash { - log::info!("Found bash at {}", path); - } - bash - }); + static BASH: LazyLock> = LazyLock::new(|| { + let bash = find_bash_in_scoop() + .or_else(find_bash_in_git) + .map(|p| p.to_string_lossy().into_owned()); + if let Some(ref path) = bash { + log::info!("Found bash at {}", path); + } + bash + }); - (*BASH).clone() + (*BASH).clone() } pub fn get_windows_system_shell() -> String { - use std::path::PathBuf; + use std::path::PathBuf; - fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option { - #[cfg(target_pointer_width = "64")] - let env_var = if find_alternate { - "ProgramFiles(x86)" - } else { - "ProgramFiles" - }; + fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option { + #[cfg(target_pointer_width = "64")] + let env_var = if find_alternate { + "ProgramFiles(x86)" + } else { + "ProgramFiles" + }; - #[cfg(target_pointer_width = "32")] - let env_var = if find_alternate { - "ProgramW6432" - } else { - "ProgramFiles" - }; + #[cfg(target_pointer_width = "32")] + let env_var = if find_alternate { + "ProgramW6432" + } else { + "ProgramFiles" + }; - let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell"); - install_base_dir - .read_dir() - .ok()? - .filter_map(Result::ok) - .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir())) - .filter_map(|entry| { - let dir_name = entry.file_name(); - let dir_name = dir_name.to_string_lossy(); + let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell"); + install_base_dir + .read_dir() + .ok()? + .filter_map(Result::ok) + .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir())) + .filter_map(|entry| { + let dir_name = entry.file_name(); + let dir_name = dir_name.to_string_lossy(); - let version = if find_preview { - let dash_index = dir_name.find('-')?; - if &dir_name[dash_index + 1..] != "preview" { - return None; - }; - dir_name[..dash_index].parse::().ok()? - } else { - dir_name.parse::().ok()? - }; + let version = if find_preview { + let dash_index = dir_name.find('-')?; + if &dir_name[dash_index + 1..] != "preview" { + return None; + }; + dir_name[..dash_index].parse::().ok()? + } else { + dir_name.parse::().ok()? + }; - let exe_path = entry.path().join("pwsh.exe"); - if exe_path.exists() { - Some((version, exe_path)) - } else { - None - } - }) - .max_by_key(|(version, _)| *version) - .map(|(_, path)| path) - } + let exe_path = entry.path().join("pwsh.exe"); + if exe_path.exists() { + Some((version, exe_path)) + } else { + None + } + }) + .max_by_key(|(version, _)| *version) + .map(|(_, path)| path) + } - fn find_pwsh_in_msix(find_preview: bool) -> Option { - let msix_app_dir = - PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps"); - if !msix_app_dir.exists() { - return None; - } + fn find_pwsh_in_msix(find_preview: bool) -> Option { + let msix_app_dir = + PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps"); + if !msix_app_dir.exists() { + return None; + } - let prefix = if find_preview { - "Microsoft.PowerShellPreview_" - } else { - "Microsoft.PowerShell_" - }; - msix_app_dir - .read_dir() - .ok()? - .filter_map(|entry| { - let entry = entry.ok()?; - if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) { - return None; - } + let prefix = if find_preview { + "Microsoft.PowerShellPreview_" + } else { + "Microsoft.PowerShell_" + }; + msix_app_dir + .read_dir() + .ok()? + .filter_map(|entry| { + let entry = entry.ok()?; + if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) { + return None; + } - if !entry.file_name().to_string_lossy().starts_with(prefix) { - return None; - } + if !entry.file_name().to_string_lossy().starts_with(prefix) { + return None; + } - let exe_path = entry.path().join("pwsh.exe"); - exe_path.exists().then_some(exe_path) - }) - .next() - } + let exe_path = entry.path().join("pwsh.exe"); + exe_path.exists().then_some(exe_path) + }) + .next() + } - fn find_pwsh_in_scoop() -> Option { - let pwsh_exe = - PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe"); - pwsh_exe.exists().then_some(pwsh_exe) - } + fn find_pwsh_in_scoop() -> Option { + let pwsh_exe = + PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe"); + pwsh_exe.exists().then_some(pwsh_exe) + } - static SYSTEM_SHELL: LazyLock = LazyLock::new(|| { - let locations = [ - || find_pwsh_in_programfiles(false, false), - || find_pwsh_in_programfiles(true, false), - || find_pwsh_in_msix(false), - || find_pwsh_in_programfiles(false, true), - || find_pwsh_in_msix(true), - || find_pwsh_in_programfiles(true, true), - || find_pwsh_in_scoop(), - || which::which_global("pwsh.exe").ok(), - || which::which_global("powershell.exe").ok(), - ]; + static SYSTEM_SHELL: LazyLock = LazyLock::new(|| { + let locations = [ + || find_pwsh_in_programfiles(false, false), + || find_pwsh_in_programfiles(true, false), + || find_pwsh_in_msix(false), + || find_pwsh_in_programfiles(false, true), + || find_pwsh_in_msix(true), + || find_pwsh_in_programfiles(true, true), + || find_pwsh_in_scoop(), + || which::which_global("pwsh.exe").ok(), + || which::which_global("powershell.exe").ok(), + ]; - locations - .into_iter() - .find_map(|f| f()) - .map(|p| p.to_string_lossy().trim().to_owned()) - .inspect(|shell| log::info!("Found powershell in: {}", shell)) - .unwrap_or_else(|| { - log::warn!("Powershell not found, falling back to `cmd`"); - "cmd.exe".to_string() - }) - }); + locations + .into_iter() + .find_map(|f| f()) + .map(|p| p.to_string_lossy().trim().to_owned()) + .inspect(|shell| log::info!("Found powershell in: {}", shell)) + .unwrap_or_else(|| { + log::warn!("Powershell not found, falling back to `cmd`"); + "cmd.exe".to_string() + }) + }); - (*SYSTEM_SHELL).clone() + (*SYSTEM_SHELL).clone() } impl fmt::Display for ShellKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ShellKind::Posix => write!(f, "sh"), - ShellKind::Csh => write!(f, "csh"), - ShellKind::Tcsh => write!(f, "tcsh"), - ShellKind::Fish => write!(f, "fish"), - ShellKind::PowerShell => write!(f, "powershell"), - ShellKind::Pwsh => write!(f, "pwsh"), - ShellKind::Nushell => write!(f, "nu"), - ShellKind::Cmd => write!(f, "cmd"), - ShellKind::Rc => write!(f, "rc"), - ShellKind::Xonsh => write!(f, "xonsh"), - ShellKind::Elvish => write!(f, "elvish"), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ShellKind::Posix => write!(f, "sh"), + ShellKind::Csh => write!(f, "csh"), + ShellKind::Tcsh => write!(f, "tcsh"), + ShellKind::Fish => write!(f, "fish"), + ShellKind::PowerShell => write!(f, "powershell"), + ShellKind::Pwsh => write!(f, "pwsh"), + ShellKind::Nushell => write!(f, "nu"), + ShellKind::Cmd => write!(f, "cmd"), + ShellKind::Rc => write!(f, "rc"), + ShellKind::Xonsh => write!(f, "xonsh"), + ShellKind::Elvish => write!(f, "elvish"), + } + } } impl ShellKind { - pub fn system() -> Self { - Self::new(get_system_shell(), cfg!(windows)) - } + pub fn system() -> Self { + Self::new(get_system_shell(), cfg!(windows)) + } - /// Returns whether this shell's command chaining syntax can be parsed by brush-parser. - /// - /// This is used to determine if we can safely parse shell commands to extract sub-commands - /// for security purposes (e.g., preventing shell injection in "always allow" patterns). - /// - /// The brush-parser handles `;` (sequential execution) and `|` (piping), which are - /// supported by all common shells. It also handles `&&` and `||` for conditional - /// execution, `$()` and backticks for command substitution, and process substitution. - /// - /// # Shell Notes - /// - /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are boolean - /// operators on values (e.g., `$true and $false`), not command chaining operators. - /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish does - /// not have `&&` or `||` operators. Its `and`/`or` are special commands that operate - /// on values, not command chaining (e.g., `and $true $false`). - /// - **Rc (Plan 9)**: Uses `;` for sequential execution and `|` for piping. Does not - /// have `&&`/`||` operators for conditional chaining. - /// All current shell variants are listed here because brush-parser can handle - /// their syntax. If a new `ShellKind` variant is added, evaluate whether - /// brush-parser can safely parse its command chaining syntax before including - /// it. Omitting a variant will cause `tool_permissions::from_input` to deny - /// terminal commands that have `always_allow` patterns configured. - pub fn supports_posix_chaining(&self) -> bool { - matches!( - self, - ShellKind::Posix - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Cmd - | ShellKind::Xonsh - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Nushell - | ShellKind::Elvish - | ShellKind::Rc - ) - } + /// Returns whether this shell's command chaining syntax can be parsed by brush-parser. + /// + /// This is used to determine if we can safely parse shell commands to extract sub-commands + /// for security purposes (e.g., preventing shell injection in "always allow" patterns). + /// + /// The brush-parser handles `;` (sequential execution) and `|` (piping), which are + /// supported by all common shells. It also handles `&&` and `||` for conditional + /// execution, `$()` and backticks for command substitution, and process substitution. + /// + /// # Shell Notes + /// + /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are boolean + /// operators on values (e.g., `$true and $false`), not command chaining operators. + /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish does + /// not have `&&` or `||` operators. Its `and`/`or` are special commands that operate + /// on values, not command chaining (e.g., `and $true $false`). + /// - **Rc (Plan 9)**: Uses `;` for sequential execution and `|` for piping. Does not + /// have `&&`/`||` operators for conditional chaining. + /// All current shell variants are listed here because brush-parser can handle + /// their syntax. If a new `ShellKind` variant is added, evaluate whether + /// brush-parser can safely parse its command chaining syntax before including + /// it. Omitting a variant will cause `tool_permissions::from_input` to deny + /// terminal commands that have `always_allow` patterns configured. + pub fn supports_posix_chaining(&self) -> bool { + matches!( + self, + ShellKind::Posix + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Pwsh + | ShellKind::Cmd + | ShellKind::Xonsh + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Nushell + | ShellKind::Elvish + | ShellKind::Rc + ) + } - pub fn new(program: impl AsRef, is_windows: bool) -> Self { - let program = program.as_ref(); - let program = program - .file_stem() - .unwrap_or(program.as_os_str()) - .to_string_lossy(); + pub fn new(program: impl AsRef, is_windows: bool) -> Self { + let program = program.as_ref(); + let program = program + .file_stem() + .unwrap_or(program.as_os_str()) + .to_string_lossy(); - match &*program { - "powershell" => ShellKind::PowerShell, - "pwsh" => ShellKind::Pwsh, - "cmd" => ShellKind::Cmd, - "nu" => ShellKind::Nushell, - "fish" => ShellKind::Fish, - "csh" => ShellKind::Csh, - "tcsh" => ShellKind::Tcsh, - "rc" => ShellKind::Rc, - "xonsh" => ShellKind::Xonsh, - "elvish" => ShellKind::Elvish, - "sh" | "bash" | "zsh" => ShellKind::Posix, - _ if is_windows => ShellKind::PowerShell, - // Some other shell detected, the user might install and use a - // unix-like shell. - _ => ShellKind::Posix, - } - } + match &*program { + "powershell" => ShellKind::PowerShell, + "pwsh" => ShellKind::Pwsh, + "cmd" => ShellKind::Cmd, + "nu" => ShellKind::Nushell, + "fish" => ShellKind::Fish, + "csh" => ShellKind::Csh, + "tcsh" => ShellKind::Tcsh, + "rc" => ShellKind::Rc, + "xonsh" => ShellKind::Xonsh, + "elvish" => ShellKind::Elvish, + "sh" | "bash" | "zsh" => ShellKind::Posix, + _ if is_windows => ShellKind::PowerShell, + // Some other shell detected, the user might install and use a + // unix-like shell. + _ => ShellKind::Posix, + } + } - pub fn to_shell_variable(self, input: &str) -> String { - match self { - Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input), - Self::Cmd => Self::to_cmd_variable(input), - Self::Posix => input.to_owned(), - Self::Fish => input.to_owned(), - Self::Csh => input.to_owned(), - Self::Tcsh => input.to_owned(), - Self::Rc => input.to_owned(), - Self::Nushell => Self::to_nushell_variable(input), - Self::Xonsh => input.to_owned(), - Self::Elvish => input.to_owned(), - } - } + pub fn to_shell_variable(self, input: &str) -> String { + match self { + Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input), + Self::Cmd => Self::to_cmd_variable(input), + Self::Posix => input.to_owned(), + Self::Fish => input.to_owned(), + Self::Csh => input.to_owned(), + Self::Tcsh => input.to_owned(), + Self::Rc => input.to_owned(), + Self::Nushell => Self::to_nushell_variable(input), + Self::Xonsh => input.to_owned(), + Self::Elvish => input.to_owned(), + } + } - fn to_cmd_variable(input: &str) -> String { - if let Some(var_str) = input.strip_prefix("${") { - if var_str.find(':').is_none() { - // If the input starts with "${", remove the trailing "}" - format!("%{}%", &var_str[..var_str.len() - 1]) - } else { - // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, - // which will result in the task failing to run in such cases. - input.into() - } - } else if let Some(var_str) = input.strip_prefix('$') { - // If the input starts with "$", directly append to "$env:" - format!("%{}%", var_str) - } else { - // If no prefix is found, return the input as is - input.into() - } - } + fn to_cmd_variable(input: &str) -> String { + if let Some(var_str) = input.strip_prefix("${") { + if var_str.find(':').is_none() { + // If the input starts with "${", remove the trailing "}" + format!("%{}%", &var_str[..var_str.len() - 1]) + } else { + // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, + // which will result in the task failing to run in such cases. + input.into() + } + } else if let Some(var_str) = input.strip_prefix('$') { + // If the input starts with "$", directly append to "$env:" + format!("%{}%", var_str) + } else { + // If no prefix is found, return the input as is + input.into() + } + } - fn to_powershell_variable(input: &str) -> String { - if let Some(var_str) = input.strip_prefix("${") { - if var_str.find(':').is_none() { - // If the input starts with "${", remove the trailing "}" - format!("$env:{}", &var_str[..var_str.len() - 1]) - } else { - // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, - // which will result in the task failing to run in such cases. - input.into() - } - } else if let Some(var_str) = input.strip_prefix('$') { - // If the input starts with "$", directly append to "$env:" - format!("$env:{}", var_str) - } else { - // If no prefix is found, return the input as is - input.into() - } - } + fn to_powershell_variable(input: &str) -> String { + if let Some(var_str) = input.strip_prefix("${") { + if var_str.find(':').is_none() { + // If the input starts with "${", remove the trailing "}" + format!("$env:{}", &var_str[..var_str.len() - 1]) + } else { + // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, + // which will result in the task failing to run in such cases. + input.into() + } + } else if let Some(var_str) = input.strip_prefix('$') { + // If the input starts with "$", directly append to "$env:" + format!("$env:{}", var_str) + } else { + // If no prefix is found, return the input as is + input.into() + } + } - fn to_nushell_variable(input: &str) -> String { - let mut result = String::new(); - let mut source = input; - let mut is_start = true; + fn to_nushell_variable(input: &str) -> String { + let mut result = String::new(); + let mut source = input; + let mut is_start = true; - loop { - match source.chars().next() { - None => return result, - Some('$') => { - source = Self::parse_nushell_var(&source[1..], &mut result, is_start); - is_start = false; - } - Some(_) => { - is_start = false; - let chunk_end = source.find('$').unwrap_or(source.len()); - let (chunk, rest) = source.split_at(chunk_end); - result.push_str(chunk); - source = rest; - } - } - } - } + loop { + match source.chars().next() { + None => return result, + Some('$') => { + source = Self::parse_nushell_var(&source[1..], &mut result, is_start); + is_start = false; + } + Some(_) => { + is_start = false; + let chunk_end = source.find('$').unwrap_or(source.len()); + let (chunk, rest) = source.split_at(chunk_end); + result.push_str(chunk); + source = rest; + } + } + } + } - fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { - if source.starts_with("env.") { - text.push('$'); - return source; - } + fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { + if source.starts_with("env.") { + text.push('$'); + return source; + } - match source.chars().next() { - Some('{') => { - let source = &source[1..]; - if let Some(end) = source.find('}') { - let var_name = &source[..end]; - if !var_name.is_empty() { - if !is_start { - text.push('('); - } - text.push_str("$env."); - text.push_str(var_name); - if !is_start { - text.push(')'); - } - &source[end + 1..] - } else { - text.push_str("${}"); - &source[end + 1..] - } - } else { - text.push_str("${"); - source - } - } - Some(c) if c.is_alphabetic() || c == '_' => { - let end = source - .find(|c: char| !c.is_alphanumeric() && c != '_') - .unwrap_or(source.len()); - let var_name = &source[..end]; - if !is_start { - text.push('('); - } - text.push_str("$env."); - text.push_str(var_name); - if !is_start { - text.push(')'); - } - &source[end..] - } - _ => { - text.push('$'); - source - } - } - } + match source.chars().next() { + Some('{') => { + let source = &source[1..]; + if let Some(end) = source.find('}') { + let var_name = &source[..end]; + if !var_name.is_empty() { + if !is_start { + text.push('('); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push(')'); + } + &source[end + 1..] + } else { + text.push_str("${}"); + &source[end + 1..] + } + } else { + text.push_str("${"); + source + } + } + Some(c) if c.is_alphabetic() || c == '_' => { + let end = source + .find(|c: char| !c.is_alphanumeric() && c != '_') + .unwrap_or(source.len()); + let var_name = &source[..end]; + if !is_start { + text.push('('); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push(')'); + } + &source[end..] + } + _ => { + text.push('$'); + source + } + } + } - pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { - match self { - ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command], - ShellKind::Cmd => vec![ - "/S".to_owned(), - "/C".to_owned(), - format!("\"{combined_command}\""), - ], - ShellKind::Posix - | ShellKind::Nushell - | ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => interactive - .then(|| "-i".to_owned()) - .into_iter() - .chain(["-c".to_owned(), combined_command]) - .collect(), - } - } + pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { + match self { + ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command], + ShellKind::Cmd => vec![ + "/S".to_owned(), + "/C".to_owned(), + format!("\"{combined_command}\""), + ], + ShellKind::Posix + | ShellKind::Nushell + | ShellKind::Fish + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Xonsh + | ShellKind::Elvish => interactive + .then(|| "-i".to_owned()) + .into_iter() + .chain(["-c".to_owned(), combined_command]) + .collect(), + } + } - pub const fn command_prefix(&self) -> Option { - match self { - ShellKind::PowerShell | ShellKind::Pwsh => Some('&'), - ShellKind::Nushell => Some('^'), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Cmd - | ShellKind::Xonsh - | ShellKind::Elvish => None, - } - } + pub const fn command_prefix(&self) -> Option { + match self { + ShellKind::PowerShell | ShellKind::Pwsh => Some('&'), + ShellKind::Nushell => Some('^'), + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Cmd + | ShellKind::Xonsh + | ShellKind::Elvish => None, + } + } - pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> { - match self.command_prefix() { - Some(prefix) if !command.starts_with(prefix) => { - Cow::Owned(format!("{prefix}{command}")) - } - _ => Cow::Borrowed(command), - } - } + pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> { + match self.command_prefix() { + Some(prefix) if !command.starts_with(prefix) => { + Cow::Owned(format!("{prefix}{command}")) + } + _ => Cow::Borrowed(command), + } + } - pub const fn sequential_commands_separator(&self) -> char { - match self { - ShellKind::Cmd => '&', - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => ';', - } - } + pub const fn sequential_commands_separator(&self) -> char { + match self { + ShellKind::Cmd => '&', + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Pwsh + | ShellKind::Nushell + | ShellKind::Xonsh + | ShellKind::Elvish => ';', + } + } - pub const fn sequential_and_commands_separator(&self) -> &'static str { - match self { - ShellKind::Cmd - | ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Pwsh - | ShellKind::Xonsh => "&&", - ShellKind::PowerShell | ShellKind::Nushell | ShellKind::Elvish => ";", - } - } + pub const fn sequential_and_commands_separator(&self) -> &'static str { + match self { + ShellKind::Cmd + | ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Pwsh + | ShellKind::Xonsh => "&&", + ShellKind::PowerShell | ShellKind::Nushell | ShellKind::Elvish => ";", + } + } - pub fn try_quote<'a>(&self, arg: &'a str) -> Option> { - match self { - ShellKind::PowerShell => Some(Self::quote_powershell(arg)), - ShellKind::Pwsh => Some(Self::quote_pwsh(arg)), - ShellKind::Cmd => Some(Self::quote_cmd(arg)), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => shlex::try_quote(arg).ok(), - } - } + pub fn try_quote<'a>(&self, arg: &'a str) -> Option> { + match self { + ShellKind::PowerShell => Some(Self::quote_powershell(arg)), + ShellKind::Pwsh => Some(Self::quote_pwsh(arg)), + ShellKind::Cmd => Some(Self::quote_cmd(arg)), + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Nushell + | ShellKind::Xonsh + | ShellKind::Elvish => shlex::try_quote(arg).ok(), + } + } - fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> { - if arg.is_empty() { - return Cow::Borrowed("\"\""); - } + fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> { + if arg.is_empty() { + return Cow::Borrowed("\"\""); + } - let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"'); - if !needs_quoting { - return Cow::Borrowed(arg); - } + let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"'); + if !needs_quoting { + return Cow::Borrowed(arg); + } - let mut result = String::with_capacity(arg.len() + 2); + let mut result = String::with_capacity(arg.len() + 2); - if enclose { - result.push('"'); - } + if enclose { + result.push('"'); + } - let chars: Vec = arg.chars().collect(); - let mut i = 0; + let chars: Vec = arg.chars().collect(); + let mut i = 0; - while i < chars.len() { - if chars[i] == '\\' { - let mut num_backslashes = 0; - while i < chars.len() && chars[i] == '\\' { - num_backslashes += 1; - i += 1; - } + while i < chars.len() { + if chars[i] == '\\' { + let mut num_backslashes = 0; + while i < chars.len() && chars[i] == '\\' { + num_backslashes += 1; + i += 1; + } - if i < chars.len() && chars[i] == '"' { - // Backslashes followed by quote: double the backslashes and escape the quote - for _ in 0..(num_backslashes * 2 + 1) { - result.push('\\'); - } - result.push('"'); - i += 1; - } else if i >= chars.len() { - // Trailing backslashes: double them (they precede the closing quote) - for _ in 0..(num_backslashes * 2) { - result.push('\\'); - } - } else { - // Backslashes not followed by quote: output as-is - for _ in 0..num_backslashes { - result.push('\\'); - } - } - } else if chars[i] == '"' { - // Quote not preceded by backslash: escape it - result.push('\\'); - result.push('"'); - i += 1; - } else { - result.push(chars[i]); - i += 1; - } - } + if i < chars.len() && chars[i] == '"' { + // Backslashes followed by quote: double the backslashes and escape the quote + for _ in 0..(num_backslashes * 2 + 1) { + result.push('\\'); + } + result.push('"'); + i += 1; + } else if i >= chars.len() { + // Trailing backslashes: double them (they precede the closing quote) + for _ in 0..(num_backslashes * 2) { + result.push('\\'); + } + } else { + // Backslashes not followed by quote: output as-is + for _ in 0..num_backslashes { + result.push('\\'); + } + } + } else if chars[i] == '"' { + // Quote not preceded by backslash: escape it + result.push('\\'); + result.push('"'); + i += 1; + } else { + result.push(chars[i]); + i += 1; + } + } - if enclose { - result.push('"'); - } - Cow::Owned(result) - } + if enclose { + result.push('"'); + } + Cow::Owned(result) + } - fn needs_quoting_powershell(s: &str) -> bool { - s.is_empty() - || s.chars().any(|c| { - c.is_whitespace() - || matches!( - c, - '"' | '`' - | '$' - | '&' - | '|' - | '<' - | '>' - | ';' - | '(' - | ')' - | '[' - | ']' - | '{' - | '}' - | ',' - | '\'' - | '@' - ) - }) - } + fn needs_quoting_powershell(s: &str) -> bool { + s.is_empty() + || s.chars().any(|c| { + c.is_whitespace() + || matches!( + c, + '"' | '`' + | '$' | '&' | '|' | '<' + | '>' | ';' | '(' | ')' + | '[' | ']' | '{' | '}' + | ',' | '\'' | '@' + ) + }) + } - fn need_quotes_powershell(arg: &str) -> bool { - let mut quote_count = 0; - for c in arg.chars() { - if c == '"' { - quote_count += 1; - } else if c.is_whitespace() && (quote_count % 2 == 0) { - return true; - } - } - false - } + fn need_quotes_powershell(arg: &str) -> bool { + let mut quote_count = 0; + for c in arg.chars() { + if c == '"' { + quote_count += 1; + } else if c.is_whitespace() && (quote_count % 2 == 0) { + return true; + } + } + false + } - fn escape_powershell_quotes(s: &str) -> String { - let mut result = String::with_capacity(s.len() + 4); - result.push('\''); - for c in s.chars() { - if c == '\'' { - result.push('\''); - } - result.push(c); - } - result.push('\''); - result - } + fn escape_powershell_quotes(s: &str) -> String { + let mut result = String::with_capacity(s.len() + 4); + result.push('\''); + for c in s.chars() { + if c == '\'' { + result.push('\''); + } + result.push(c); + } + result.push('\''); + result + } - pub fn quote_powershell(arg: &str) -> Cow<'_, str> { - let ps_will_quote = Self::need_quotes_powershell(arg); - let crt_quoted = Self::quote_windows(arg, !ps_will_quote); + pub fn quote_powershell(arg: &str) -> Cow<'_, str> { + let ps_will_quote = Self::need_quotes_powershell(arg); + let crt_quoted = Self::quote_windows(arg, !ps_will_quote); - if !Self::needs_quoting_powershell(arg) { - return crt_quoted; - } + if !Self::needs_quoting_powershell(arg) { + return crt_quoted; + } - Cow::Owned(Self::escape_powershell_quotes(&crt_quoted)) - } + Cow::Owned(Self::escape_powershell_quotes(&crt_quoted)) + } - pub fn quote_pwsh(arg: &str) -> Cow<'_, str> { - if arg.is_empty() { - return Cow::Borrowed("''"); - } + pub fn quote_pwsh(arg: &str) -> Cow<'_, str> { + if arg.is_empty() { + return Cow::Borrowed("''"); + } - if !Self::needs_quoting_powershell(arg) { - return Cow::Borrowed(arg); - } + if !Self::needs_quoting_powershell(arg) { + return Cow::Borrowed(arg); + } - Cow::Owned(Self::escape_powershell_quotes(arg)) - } + Cow::Owned(Self::escape_powershell_quotes(arg)) + } - pub fn quote_cmd(arg: &str) -> Cow<'_, str> { - let crt_quoted = Self::quote_windows(arg, true); + pub fn quote_cmd(arg: &str) -> Cow<'_, str> { + let crt_quoted = Self::quote_windows(arg, true); - let needs_cmd_escaping = crt_quoted.contains(['"', '%', '^', '<', '>', '&', '|', '(', ')']); + let needs_cmd_escaping = crt_quoted.contains(['"', '%', '^', '<', '>', '&', '|', '(', ')']); - if !needs_cmd_escaping { - return crt_quoted; - } + if !needs_cmd_escaping { + return crt_quoted; + } - let mut result = String::with_capacity(crt_quoted.len() * 2); - for c in crt_quoted.chars() { - match c { - '^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => { - result.push('^'); - result.push(c); - } - '%' => { - result.push_str("%%cd:~,%"); - } - _ => result.push(c), - } - } - Cow::Owned(result) - } + let mut result = String::with_capacity(crt_quoted.len() * 2); + for c in crt_quoted.chars() { + match c { + '^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => { + result.push('^'); + result.push(c); + } + '%' => { + result.push_str("%%cd:~,%"); + } + _ => result.push(c), + } + } + Cow::Owned(result) + } - /// Quotes the given argument if necessary, taking into account the command prefix. - /// - /// In other words, this will consider quoting arg without its command prefix to not break the command. - /// You should use this over `try_quote` when you want to quote a shell command. - pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option> { - if let Some(char) = self.command_prefix() - && let Some(arg) = arg.strip_prefix(char) - { - // we have a command that is prefixed - for quote in ['\'', '"'] { - if let Some(arg) = arg - .strip_prefix(quote) - .and_then(|arg| arg.strip_suffix(quote)) - { - // and the command itself is wrapped as a literal, that - // means the prefix exists to interpret a literal as a - // command. So strip the quotes, quote the command, and - // re-add the quotes if they are missing after requoting - let quoted = self.try_quote(arg)?; - return Some(if quoted.starts_with(['\'', '"']) { - Cow::Owned(self.prepend_command_prefix("ed).into_owned()) - } else { - Cow::Owned( - self.prepend_command_prefix(&format!("{quote}{quoted}{quote}")) - .into_owned(), - ) - }); - } - } - return self - .try_quote(arg) - .map(|quoted| Cow::Owned(self.prepend_command_prefix("ed).into_owned())); - } - self.try_quote(arg).map(|quoted| match quoted { - unquoted @ Cow::Borrowed(_) => unquoted, - Cow::Owned(quoted) => Cow::Owned(self.prepend_command_prefix("ed).into_owned()), - }) - } + /// Quotes the given argument if necessary, taking into account the command prefix. + /// + /// In other words, this will consider quoting arg without its command prefix to not break the command. + /// You should use this over `try_quote` when you want to quote a shell command. + pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option> { + if let Some(char) = self.command_prefix() + && let Some(arg) = arg.strip_prefix(char) + { + // we have a command that is prefixed + for quote in ['\'', '"'] { + if let Some(arg) = arg + .strip_prefix(quote) + .and_then(|arg| arg.strip_suffix(quote)) + { + // and the command itself is wrapped as a literal, that + // means the prefix exists to interpret a literal as a + // command. So strip the quotes, quote the command, and + // re-add the quotes if they are missing after requoting + let quoted = self.try_quote(arg)?; + return Some(if quoted.starts_with(['\'', '"']) { + Cow::Owned(self.prepend_command_prefix("ed).into_owned()) + } else { + Cow::Owned( + self.prepend_command_prefix(&format!("{quote}{quoted}{quote}")) + .into_owned(), + ) + }); + } + } + return self + .try_quote(arg) + .map(|quoted| Cow::Owned(self.prepend_command_prefix("ed).into_owned())); + } + self.try_quote(arg).map(|quoted| match quoted { + unquoted @ Cow::Borrowed(_) => unquoted, + Cow::Owned(quoted) => Cow::Owned(self.prepend_command_prefix("ed).into_owned()), + }) + } - pub fn split(&self, input: &str) -> Option> { - shlex::split(input) - } + pub fn split(&self, input: &str) -> Option> { + shlex::split(input) + } - pub const fn activate_keyword(&self) -> &'static str { - match self { - ShellKind::Cmd => "", - ShellKind::Nushell => "overlay use", - ShellKind::PowerShell | ShellKind::Pwsh => ".", - ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Posix - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => "source", - } - } + pub const fn activate_keyword(&self) -> &'static str { + match self { + ShellKind::Cmd => "", + ShellKind::Nushell => "overlay use", + ShellKind::PowerShell | ShellKind::Pwsh => ".", + ShellKind::Fish + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Posix + | ShellKind::Rc + | ShellKind::Xonsh + | ShellKind::Elvish => "source", + } + } - pub const fn clear_screen_command(&self) -> &'static str { - match self { - ShellKind::Cmd => "cls", - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => "clear", - } - } + pub const fn clear_screen_command(&self) -> &'static str { + match self { + ShellKind::Cmd => "cls", + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Pwsh + | ShellKind::Nushell + | ShellKind::Xonsh + | ShellKind::Elvish => "clear", + } + } - #[cfg(windows)] - /// We do not want to escape arguments if we are using CMD as our shell. - /// If we do we end up with too many quotes/escaped quotes for CMD to handle. - pub const fn tty_escape_args(&self) -> bool { - match self { - ShellKind::Cmd => false, - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => true, - } - } + #[cfg(windows)] + /// We do not want to escape arguments if we are using CMD as our shell. + /// If we do we end up with too many quotes/escaped quotes for CMD to handle. + pub const fn tty_escape_args(&self) -> bool { + match self { + ShellKind::Cmd => false, + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Pwsh + | ShellKind::Nushell + | ShellKind::Xonsh + | ShellKind::Elvish => true, + } + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - // Examples - // WSL - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello" - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello" - // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53 - // PowerShell from Nushell - // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\"" - // PowerShell from CMD - // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\" + // Examples + // WSL + // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello" + // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello" + // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53 + // PowerShell from Nushell + // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\"" + // PowerShell from CMD + // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\" - #[test] - fn test_try_quote_powershell() { - let shell_kind = ShellKind::PowerShell; - assert_eq!( + #[test] + fn test_try_quote_powershell() { + let shell_kind = ShellKind::PowerShell; + assert_eq!( shell_kind .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") .unwrap() .into_owned(), "'C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"'".to_string() ); - } + } - #[test] - fn test_try_quote_cmd() { - let shell_kind = ShellKind::Cmd; - assert_eq!( + #[test] + fn test_try_quote_cmd() { + let shell_kind = ShellKind::Cmd; + assert_eq!( shell_kind .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") .unwrap() .into_owned(), "^\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\^\"test_foo.py::test_foo\\^\"^\"".to_string() ); - } + } - #[test] - fn test_try_quote_powershell_edge_cases() { - let shell_kind = ShellKind::PowerShell; + #[test] + fn test_try_quote_powershell_edge_cases() { + let shell_kind = ShellKind::PowerShell; - // Empty string - assert_eq!( - shell_kind.try_quote("").unwrap().into_owned(), - "'\"\"'".to_string() - ); + // Empty string + assert_eq!( + shell_kind.try_quote("").unwrap().into_owned(), + "'\"\"'".to_string() + ); - // String without special characters (no quoting needed) - assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); + // String without special characters (no quoting needed) + assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); - // String with spaces - assert_eq!( - shell_kind.try_quote("hello world").unwrap().into_owned(), - "'hello world'".to_string() - ); + // String with spaces + assert_eq!( + shell_kind.try_quote("hello world").unwrap().into_owned(), + "'hello world'".to_string() + ); - // String with dollar signs - assert_eq!( - shell_kind.try_quote("$variable").unwrap().into_owned(), - "'$variable'".to_string() - ); + // String with dollar signs + assert_eq!( + shell_kind.try_quote("$variable").unwrap().into_owned(), + "'$variable'".to_string() + ); - // String with backticks - assert_eq!( - shell_kind.try_quote("test`command").unwrap().into_owned(), - "'test`command'".to_string() - ); + // String with backticks + assert_eq!( + shell_kind.try_quote("test`command").unwrap().into_owned(), + "'test`command'".to_string() + ); - // String with multiple special characters - assert_eq!( - shell_kind - .try_quote("test `\"$var`\" end") - .unwrap() - .into_owned(), - "'test `\\\"$var`\\\" end'".to_string() - ); + // String with multiple special characters + assert_eq!( + shell_kind + .try_quote("test `\"$var`\" end") + .unwrap() + .into_owned(), + "'test `\\\"$var`\\\" end'".to_string() + ); - // String with backslashes and colon (path without spaces doesn't need quoting) - assert_eq!( - shell_kind.try_quote("C:\\path\\to\\file").unwrap(), - "C:\\path\\to\\file" - ); - } + // String with backslashes and colon (path without spaces doesn't need quoting) + assert_eq!( + shell_kind.try_quote("C:\\path\\to\\file").unwrap(), + "C:\\path\\to\\file" + ); + } - #[test] - fn test_try_quote_cmd_edge_cases() { - let shell_kind = ShellKind::Cmd; + #[test] + fn test_try_quote_cmd_edge_cases() { + let shell_kind = ShellKind::Cmd; - // Empty string - assert_eq!( - shell_kind.try_quote("").unwrap().into_owned(), - "^\"^\"".to_string() - ); + // Empty string + assert_eq!( + shell_kind.try_quote("").unwrap().into_owned(), + "^\"^\"".to_string() + ); - // String without special characters (no quoting needed) - assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); + // String without special characters (no quoting needed) + assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); - // String with spaces - assert_eq!( - shell_kind.try_quote("hello world").unwrap().into_owned(), - "^\"hello world^\"".to_string() - ); + // String with spaces + assert_eq!( + shell_kind.try_quote("hello world").unwrap().into_owned(), + "^\"hello world^\"".to_string() + ); - // String with space and backslash (backslash not at end, so not doubled) - assert_eq!( - shell_kind.try_quote("path\\ test").unwrap().into_owned(), - "^\"path\\ test^\"".to_string() - ); + // String with space and backslash (backslash not at end, so not doubled) + assert_eq!( + shell_kind.try_quote("path\\ test").unwrap().into_owned(), + "^\"path\\ test^\"".to_string() + ); - // String ending with backslash (must be doubled before closing quote) - assert_eq!( - shell_kind.try_quote("test path\\").unwrap().into_owned(), - "^\"test path\\\\^\"".to_string() - ); + // String ending with backslash (must be doubled before closing quote) + assert_eq!( + shell_kind.try_quote("test path\\").unwrap().into_owned(), + "^\"test path\\\\^\"".to_string() + ); - // String ending with multiple backslashes (all doubled before closing quote) - assert_eq!( - shell_kind.try_quote("test path\\\\").unwrap().into_owned(), - "^\"test path\\\\\\\\^\"".to_string() - ); + // String ending with multiple backslashes (all doubled before closing quote) + assert_eq!( + shell_kind.try_quote("test path\\\\").unwrap().into_owned(), + "^\"test path\\\\\\\\^\"".to_string() + ); - // String with embedded quote (quote is escaped, backslash before it is doubled) - assert_eq!( - shell_kind.try_quote("test\\\"quote").unwrap().into_owned(), - "^\"test\\\\\\^\"quote^\"".to_string() - ); + // String with embedded quote (quote is escaped, backslash before it is doubled) + assert_eq!( + shell_kind.try_quote("test\\\"quote").unwrap().into_owned(), + "^\"test\\\\\\^\"quote^\"".to_string() + ); - // String with multiple backslashes before embedded quote (all doubled) - assert_eq!( - shell_kind - .try_quote("test\\\\\"quote") - .unwrap() - .into_owned(), - "^\"test\\\\\\\\\\^\"quote^\"".to_string() - ); + // String with multiple backslashes before embedded quote (all doubled) + assert_eq!( + shell_kind + .try_quote("test\\\\\"quote") + .unwrap() + .into_owned(), + "^\"test\\\\\\\\\\^\"quote^\"".to_string() + ); - // String with backslashes not before quotes (path without spaces doesn't need quoting) - assert_eq!( - shell_kind.try_quote("C:\\path\\to\\file").unwrap(), - "C:\\path\\to\\file" - ); - } + // String with backslashes not before quotes (path without spaces doesn't need quoting) + assert_eq!( + shell_kind.try_quote("C:\\path\\to\\file").unwrap(), + "C:\\path\\to\\file" + ); + } - #[test] - fn test_try_quote_nu_command() { - let shell_kind = ShellKind::Nushell; - assert_eq!( - shell_kind.try_quote("'uname'").unwrap().into_owned(), - "\"'uname'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("'uname'") - .unwrap() - .into_owned(), - "^\"'uname'\"".to_string() - ); - assert_eq!( - shell_kind.try_quote("^uname").unwrap().into_owned(), - "'^uname'".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^uname") - .unwrap() - .into_owned(), - "^uname".to_string() - ); - assert_eq!( - shell_kind.try_quote("^'uname'").unwrap().into_owned(), - "'^'\"'uname\'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^'uname'") - .unwrap() - .into_owned(), - "^'uname'".to_string() - ); - assert_eq!( - shell_kind.try_quote("'uname a'").unwrap().into_owned(), - "\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("'uname a'") - .unwrap() - .into_owned(), - "^\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind.try_quote("^'uname a'").unwrap().into_owned(), - "'^'\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^'uname a'") - .unwrap() - .into_owned(), - "^'uname a'".to_string() - ); - assert_eq!( - shell_kind.try_quote("uname").unwrap().into_owned(), - "uname".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("uname") - .unwrap() - .into_owned(), - "uname".to_string() - ); - } + #[test] + fn test_try_quote_nu_command() { + let shell_kind = ShellKind::Nushell; + assert_eq!( + shell_kind.try_quote("'uname'").unwrap().into_owned(), + "\"'uname'\"".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("'uname'") + .unwrap() + .into_owned(), + "^\"'uname'\"".to_string() + ); + assert_eq!( + shell_kind.try_quote("^uname").unwrap().into_owned(), + "'^uname'".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("^uname") + .unwrap() + .into_owned(), + "^uname".to_string() + ); + assert_eq!( + shell_kind.try_quote("^'uname'").unwrap().into_owned(), + "'^'\"'uname\'\"".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("^'uname'") + .unwrap() + .into_owned(), + "^'uname'".to_string() + ); + assert_eq!( + shell_kind.try_quote("'uname a'").unwrap().into_owned(), + "\"'uname a'\"".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("'uname a'") + .unwrap() + .into_owned(), + "^\"'uname a'\"".to_string() + ); + assert_eq!( + shell_kind.try_quote("^'uname a'").unwrap().into_owned(), + "'^'\"'uname a'\"".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("^'uname a'") + .unwrap() + .into_owned(), + "^'uname a'".to_string() + ); + assert_eq!( + shell_kind.try_quote("uname").unwrap().into_owned(), + "uname".to_string() + ); + assert_eq!( + shell_kind + .try_quote_prefix_aware("uname") + .unwrap() + .into_owned(), + "uname".to_string() + ); + } - #[test] - fn test_try_quote_single_quote_paths() { - let path_with_quote = r"C:\Temp\O'Brien\repo"; - let shlex_shells = [ - ShellKind::Posix, - ShellKind::Fish, - ShellKind::Csh, - ShellKind::Tcsh, - ShellKind::Rc, - ShellKind::Xonsh, - ShellKind::Elvish, - ShellKind::Nushell, - ]; + #[test] + fn test_try_quote_single_quote_paths() { + let path_with_quote = r"C:\Temp\O'Brien\repo"; + let shlex_shells = [ + ShellKind::Posix, + ShellKind::Fish, + ShellKind::Csh, + ShellKind::Tcsh, + ShellKind::Rc, + ShellKind::Xonsh, + ShellKind::Elvish, + ShellKind::Nushell, + ]; - for shell_kind in shlex_shells { - let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned(); - assert_ne!(quoted, path_with_quote); - assert_eq!( - shlex::split("ed), - Some(vec![path_with_quote.to_string()]) - ); + for shell_kind in shlex_shells { + let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned(); + assert_ne!(quoted, path_with_quote); + assert_eq!( + shlex::split("ed), + Some(vec![path_with_quote.to_string()]) + ); - if shell_kind == ShellKind::Nushell { - let prefixed = shell_kind.prepend_command_prefix("ed); - assert!(prefixed.starts_with('^')); - } - } + if shell_kind == ShellKind::Nushell { + let prefixed = shell_kind.prepend_command_prefix("ed); + assert!(prefixed.starts_with('^')); + } + } - for shell_kind in [ShellKind::PowerShell, ShellKind::Pwsh] { - let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned(); - assert!(quoted.starts_with('\'')); - assert!(quoted.ends_with('\'')); - assert!(quoted.contains("O''Brien")); - } - } + for shell_kind in [ShellKind::PowerShell, ShellKind::Pwsh] { + let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned(); + assert!(quoted.starts_with('\'')); + assert!(quoted.ends_with('\'')); + assert!(quoted.contains("O''Brien")); + } + } } diff --git a/crates/gpui_zed_util/src/shell_builder.rs b/crates/gpui_zed_util/src/shell_builder.rs index 1d488d4456..e7358e6f27 100644 --- a/crates/gpui_zed_util/src/shell_builder.rs +++ b/crates/gpui_zed_util/src/shell_builder.rs @@ -6,322 +6,322 @@ use crate::shell::{Shell, ShellKind}; /// ShellBuilder is used to turn a user-requested task into a /// program that can be executed by the shell. pub struct ShellBuilder { - /// The shell to run - program: String, - args: Vec, - interactive: bool, - /// Whether to redirect stdin to /dev/null for the spawned command as a subshell. - redirect_stdin: bool, - kind: ShellKind, + /// The shell to run + program: String, + args: Vec, + interactive: bool, + /// Whether to redirect stdin to /dev/null for the spawned command as a subshell. + redirect_stdin: bool, + kind: ShellKind, } impl ShellBuilder { - /// Create a new ShellBuilder as configured. - pub fn new(shell: &Shell, is_windows: bool) -> Self { - let (program, args) = match shell { - Shell::System => (get_system_shell(), Vec::new()), - Shell::Program(shell) => (shell.clone(), Vec::new()), - Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), - }; + /// Create a new ShellBuilder as configured. + pub fn new(shell: &Shell, is_windows: bool) -> Self { + let (program, args) = match shell { + Shell::System => (get_system_shell(), Vec::new()), + Shell::Program(shell) => (shell.clone(), Vec::new()), + Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), + }; - let kind = ShellKind::new(&program, is_windows); - Self { - program, - args, - interactive: true, - kind, - redirect_stdin: false, - } - } - pub fn non_interactive(mut self) -> Self { - self.interactive = false; - self - } + let kind = ShellKind::new(&program, is_windows); + Self { + program, + args, + interactive: true, + kind, + redirect_stdin: false, + } + } + pub fn non_interactive(mut self) -> Self { + self.interactive = false; + self + } - /// Returns the label to show in the terminal tab - pub fn command_label(&self, command_to_use_in_label: &str) -> String { - if command_to_use_in_label.trim().is_empty() { - self.program.clone() - } else { - match self.kind { - ShellKind::PowerShell | ShellKind::Pwsh => { - format!("{} -C '{}'", self.program, command_to_use_in_label) - } - ShellKind::Cmd => { - format!("{} /C \"{}\"", self.program, command_to_use_in_label) - } - ShellKind::Posix - | ShellKind::Nushell - | ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => { - let interactivity = if self.interactive { "-i " } else { "" }; - format!( - "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'", - PROGRAM = self.program - ) - } - } - } - } + /// Returns the label to show in the terminal tab + pub fn command_label(&self, command_to_use_in_label: &str) -> String { + if command_to_use_in_label.trim().is_empty() { + self.program.clone() + } else { + match self.kind { + ShellKind::PowerShell | ShellKind::Pwsh => { + format!("{} -C '{}'", self.program, command_to_use_in_label) + } + ShellKind::Cmd => { + format!("{} /C \"{}\"", self.program, command_to_use_in_label) + } + ShellKind::Posix + | ShellKind::Nushell + | ShellKind::Fish + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Xonsh + | ShellKind::Elvish => { + let interactivity = if self.interactive { "-i " } else { "" }; + format!( + "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'", + PROGRAM = self.program + ) + } + } + } + } - pub fn redirect_stdin_to_dev_null(mut self) -> Self { - self.redirect_stdin = true; - self - } + pub fn redirect_stdin_to_dev_null(mut self) -> Self { + self.redirect_stdin = true; + self + } - /// Returns the program and arguments to run this task in a shell. - pub fn build( - mut self, - task_command: Option, - task_args: &[String], - ) -> (String, Vec) { - if let Some(task_command) = task_command { - let task_command = if !task_args.is_empty() { - match self.kind.try_quote_prefix_aware(&task_command) { - Some(task_command) => task_command.into_owned(), - None => task_command, - } - } else { - task_command - }; - let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { - command.push(' '); - let shell_variable = self.kind.to_shell_variable(arg); - command.push_str(&match self.kind.try_quote(&shell_variable) { - Some(shell_variable) => shell_variable, - None => Cow::Owned(shell_variable), - }); - command - }); - if self.redirect_stdin { - match self.kind { - ShellKind::Fish => { - combined_command.insert_str(0, "begin; "); - combined_command.push_str("; end { - combined_command.insert(0, '('); - combined_command.push_str("\n) { - combined_command.insert_str(0, "$null | & {"); - combined_command.push('}'); - } - ShellKind::Cmd => { - combined_command.push_str("< NUL"); - } - } - } + /// Returns the program and arguments to run this task in a shell. + pub fn build( + mut self, + task_command: Option, + task_args: &[String], + ) -> (String, Vec) { + if let Some(task_command) = task_command { + let task_command = if !task_args.is_empty() { + match self.kind.try_quote_prefix_aware(&task_command) { + Some(task_command) => task_command.into_owned(), + None => task_command, + } + } else { + task_command + }; + let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { + command.push(' '); + let shell_variable = self.kind.to_shell_variable(arg); + command.push_str(&match self.kind.try_quote(&shell_variable) { + Some(shell_variable) => shell_variable, + None => Cow::Owned(shell_variable), + }); + command + }); + if self.redirect_stdin { + match self.kind { + ShellKind::Fish => { + combined_command.insert_str(0, "begin; "); + combined_command.push_str("; end { + combined_command.insert(0, '('); + combined_command.push_str("\n) { + combined_command.insert_str(0, "$null | & {"); + combined_command.push('}'); + } + ShellKind::Cmd => { + combined_command.push_str("< NUL"); + } + } + } - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); - } + self.args + .extend(self.kind.args_for_shell(self.interactive, combined_command)); + } - (self.program, self.args) - } + (self.program, self.args) + } - // This should not exist, but our task infra is broken beyond repair right now - #[doc(hidden)] - pub fn build_no_quote( - mut self, - task_command: Option, - task_args: &[String], - ) -> (String, Vec) { - if let Some(task_command) = task_command { - let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); - if self.redirect_stdin { - match self.kind { - ShellKind::Fish => { - combined_command.insert_str(0, "begin; "); - combined_command.push_str("; end { - combined_command.insert(0, '('); - combined_command.push_str("\n) { - combined_command.insert_str(0, "$null | & {"); - combined_command.push('}'); - } - ShellKind::Cmd => { - combined_command.push_str("< NUL"); - } - } - } + // This should not exist, but our task infra is broken beyond repair right now + #[doc(hidden)] + pub fn build_no_quote( + mut self, + task_command: Option, + task_args: &[String], + ) -> (String, Vec) { + if let Some(task_command) = task_command { + let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { + command.push(' '); + command.push_str(&self.kind.to_shell_variable(arg)); + command + }); + if self.redirect_stdin { + match self.kind { + ShellKind::Fish => { + combined_command.insert_str(0, "begin; "); + combined_command.push_str("; end { + combined_command.insert(0, '('); + combined_command.push_str("\n) { + combined_command.insert_str(0, "$null | & {"); + combined_command.push('}'); + } + ShellKind::Cmd => { + combined_command.push_str("< NUL"); + } + } + } - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); - } + self.args + .extend(self.kind.args_for_shell(self.interactive, combined_command)); + } - (self.program, self.args) - } + (self.program, self.args) + } - /// Builds a `smol::process::Command` with the given task command and arguments. - /// - /// Prefer this over manually constructing a command with the output of `Self::build`, - /// as this method handles `cmd` weirdness on windows correctly. - pub fn build_smol_command( - self, - task_command: Option, - task_args: &[String], - ) -> smol::process::Command { - smol::process::Command::from(self.build_std_command(task_command, task_args)) - } + /// Builds a `smol::process::Command` with the given task command and arguments. + /// + /// Prefer this over manually constructing a command with the output of `Self::build`, + /// as this method handles `cmd` weirdness on windows correctly. + pub fn build_smol_command( + self, + task_command: Option, + task_args: &[String], + ) -> smol::process::Command { + smol::process::Command::from(self.build_std_command(task_command, task_args)) + } - /// Builds a `std::process::Command` with the given task command and arguments. - /// - /// Prefer this over manually constructing a command with the output of `Self::build`, - /// as this method handles `cmd` weirdness on windows correctly. - pub fn build_std_command( - self, - mut task_command: Option, - task_args: &[String], - ) -> std::process::Command { - #[cfg(windows)] - let kind = self.kind; - if task_args.is_empty() { - task_command = task_command - .as_ref() - .map(|cmd| self.kind.try_quote_prefix_aware(cmd).map(Cow::into_owned)) - .unwrap_or(task_command); - } - let (program, args) = self.build(task_command, task_args); + /// Builds a `std::process::Command` with the given task command and arguments. + /// + /// Prefer this over manually constructing a command with the output of `Self::build`, + /// as this method handles `cmd` weirdness on windows correctly. + pub fn build_std_command( + self, + mut task_command: Option, + task_args: &[String], + ) -> std::process::Command { + #[cfg(windows)] + let kind = self.kind; + if task_args.is_empty() { + task_command = task_command + .as_ref() + .map(|cmd| self.kind.try_quote_prefix_aware(cmd).map(Cow::into_owned)) + .unwrap_or(task_command); + } + let (program, args) = self.build(task_command, task_args); - let mut child = crate::command::new_std_command(program); + let mut child = crate::command::new_std_command(program); - #[cfg(windows)] - if kind == ShellKind::Cmd { - use std::os::windows::process::CommandExt; + #[cfg(windows)] + if kind == ShellKind::Cmd { + use std::os::windows::process::CommandExt; - for arg in args { - child.raw_arg(arg); - } - } else { - child.args(args); - } + for arg in args { + child.raw_arg(arg); + } + } else { + child.args(args); + } - #[cfg(not(windows))] - child.args(args); + #[cfg(not(windows))] + child.args(args); - child - } + child + } - pub fn kind(&self) -> ShellKind { - self.kind - } + pub fn kind(&self) -> ShellKind { + self.kind + } } #[cfg(test)] mod test { - use super::*; + use super::*; - #[test] - fn test_nu_shell_variable_substitution() { - let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(&shell, false); + #[test] + fn test_nu_shell_variable_substitution() { + let shell = Shell::Program("nu".to_owned()); + let shell_builder = ShellBuilder::new(&shell, false); - let (program, args) = shell_builder.build( - Some("echo".into()), - &[ - "${hello}".to_string(), - "$world".to_string(), - "nothing".to_string(), - "--$something".to_string(), - "$".to_string(), - "${test".to_string(), - ], - ); + let (program, args) = shell_builder.build( + Some("echo".into()), + &[ + "${hello}".to_string(), + "$world".to_string(), + "nothing".to_string(), + "--$something".to_string(), + "$".to_string(), + "${test".to_string(), + ], + ); - assert_eq!(program, "nu"); - assert_eq!( - args, - vec![ - "-i", - "-c", - "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'" - ] - ); - } + assert_eq!(program, "nu"); + assert_eq!( + args, + vec![ + "-i", + "-c", + "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'" + ] + ); + } - #[test] - fn redirect_stdin_to_dev_null_precedence() { - let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(&shell, false); + #[test] + fn redirect_stdin_to_dev_null_precedence() { + let shell = Shell::Program("nu".to_owned()); + let shell_builder = ShellBuilder::new(&shell, false); - let (program, args) = shell_builder - .redirect_stdin_to_dev_null() - .build(Some("echo".into()), &["nothing".to_string()]); + let (program, args) = shell_builder + .redirect_stdin_to_dev_null() + .build(Some("echo".into()), &["nothing".to_string()]); - assert_eq!(program, "nu"); - assert_eq!(args, vec!["-i", "-c", "(echo nothing\n) Result> { - for (position, _) in output.match_indices('{') { - let candidate = &output[position..]; - let mut deserializer = serde_json::Deserializer::from_str(candidate); - if let Ok(env_map) = HashMap::::deserialize(&mut deserializer) { - return Ok(env_map); - } - } - anyhow::bail!("Failed to find JSON in shell output: {output}") + for (position, _) in output.match_indices('{') { + let candidate = &output[position..]; + let mut deserializer = serde_json::Deserializer::from_str(candidate); + if let Ok(env_map) = HashMap::::deserialize(&mut deserializer) { + return Ok(env_map); + } + } + anyhow::bail!("Failed to find JSON in shell output: {output}") } pub fn print_env() { - let env_vars: HashMap = std::env::vars().collect(); - let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| { - eprintln!("Error serializing environment variables: {}", err); - std::process::exit(1); - }); - println!("{}", json); + let env_vars: HashMap = std::env::vars().collect(); + let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| { + eprintln!("Error serializing environment variables: {}", err); + std::process::exit(1); + }); + println!("{}", json); } /// Capture all environment variables from the login shell in the given directory. pub async fn capture( - shell_path: impl AsRef, - args: &[String], - directory: impl AsRef, + shell_path: impl AsRef, + args: &[String], + directory: impl AsRef, ) -> Result> { - #[cfg(windows)] - return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await; - #[cfg(unix)] - return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await; + #[cfg(windows)] + return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await; + #[cfg(unix)] + return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await; } /// Try to parse the environment output before checking the exit status. @@ -44,301 +44,301 @@ pub async fn capture( /// non-zero exit status even though `zed --printenv` ran successfully and /// produced valid output on its separate fd. fn parse_env_output( - env_output: &str, - status: &std::process::ExitStatus, - successful_capture_warning: impl FnOnce() -> String, - failed_capture_error: impl FnOnce() -> String, + env_output: &str, + status: &std::process::ExitStatus, + successful_capture_warning: impl FnOnce() -> String, + failed_capture_error: impl FnOnce() -> String, ) -> Result> { - match parse_env_map_from_noisy_output(env_output) { - Ok(env_map) => { - if !status.success() { - log::warn!("{}", successful_capture_warning()); - } - Ok(env_map) - } - Err(parse_error) => { - if !status.success() { - anyhow::bail!( - "{}. Failed to deserialize environment variables from json: {parse_error}. output: {env_output}", - failed_capture_error(), - ); - } + match parse_env_map_from_noisy_output(env_output) { + Ok(env_map) => { + if !status.success() { + log::warn!("{}", successful_capture_warning()); + } + Ok(env_map) + } + Err(parse_error) => { + if !status.success() { + anyhow::bail!( + "{}. Failed to deserialize environment variables from json: {parse_error}. output: {env_output}", + failed_capture_error(), + ); + } - anyhow::bail!( - "Failed to deserialize environment variables from json: {parse_error}. output: {env_output}" - ); - } - } + anyhow::bail!( + "Failed to deserialize environment variables from json: {parse_error}. output: {env_output}" + ); + } + } } #[cfg(unix)] async fn capture_unix( - shell_path: &Path, - args: &[String], - directory: &Path, + shell_path: &Path, + args: &[String], + directory: &Path, ) -> Result> { - use std::os::unix::process::CommandExt; + use std::os::unix::process::CommandExt; - use crate::command::new_std_command; + use crate::command::new_std_command; - let shell_kind = ShellKind::new(shell_path, false); - let quoted_zed_path = super::get_shell_safe_zed_path(shell_kind)?; + let shell_kind = ShellKind::new(shell_path, false); + let quoted_zed_path = super::get_shell_safe_zed_path(shell_kind)?; - let mut command_string = String::new(); - let mut command = new_std_command(shell_path); - command.args(args); - // In some shells, file descriptors greater than 2 cannot be used in interactive mode, - // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others. - // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482 - const FD_STDIN: std::os::fd::RawFd = 0; - const FD_STDOUT: std::os::fd::RawFd = 1; - const FD_STDERR: std::os::fd::RawFd = 2; + let mut command_string = String::new(); + let mut command = new_std_command(shell_path); + command.args(args); + // In some shells, file descriptors greater than 2 cannot be used in interactive mode, + // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others. + // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482 + const FD_STDIN: std::os::fd::RawFd = 0; + const FD_STDOUT: std::os::fd::RawFd = 1; + const FD_STDERR: std::os::fd::RawFd = 2; - let (fd_num, redir) = match shell_kind { - ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` - ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()), - // xonsh doesn't support redirecting to stdin, and control sequences are printed to - // stdout on startup - ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()), - ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)), - _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` - }; + let (fd_num, redir) = match shell_kind { + ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` + ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()), + // xonsh doesn't support redirecting to stdin, and control sequences are printed to + // stdout on startup + ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()), + ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)), + _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` + }; - match shell_kind { - ShellKind::Csh | ShellKind::Tcsh => { - // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`) - command.arg0("-"); - } - ShellKind::Fish => { - // in fish, asdf, direnv attach to the `fish_prompt` event - command_string.push_str("emit fish_prompt;"); - command.arg("-l"); - } - _ => { - command.arg("-l"); - } - } + match shell_kind { + ShellKind::Csh | ShellKind::Tcsh => { + // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`) + command.arg0("-"); + } + ShellKind::Fish => { + // in fish, asdf, direnv attach to the `fish_prompt` event + command_string.push_str("emit fish_prompt;"); + command.arg("-l"); + } + _ => { + command.arg("-l"); + } + } - match shell_kind { - // Nushell does not allow non-interactive login shells. - // Instead of doing "-l -i -c ''" - // use "-l -e '; exit'" instead - ShellKind::Nushell => command.arg("-e"), - _ => command.args(["-i", "-c"]), - }; + match shell_kind { + // Nushell does not allow non-interactive login shells. + // Instead of doing "-l -i -c ''" + // use "-l -e '; exit'" instead + ShellKind::Nushell => command.arg("-e"), + _ => command.args(["-i", "-c"]), + }; - // Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag - let dir_str = directory.to_string_lossy(); - let dir_str = if dir_str.starts_with('-') { - format!("./{dir_str}").into() - } else { - dir_str - }; - let quoted_dir = shell_kind - .try_quote(&dir_str) - .context("unexpected null in directory name")?; + // Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag + let dir_str = directory.to_string_lossy(); + let dir_str = if dir_str.starts_with('-') { + format!("./{dir_str}").into() + } else { + dir_str + }; + let quoted_dir = shell_kind + .try_quote(&dir_str) + .context("unexpected null in directory name")?; - // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc) - command_string.push_str(&format!("cd {};", quoted_dir)); - if let Some(prefix) = shell_kind.command_prefix() { - command_string.push(prefix); - } - command_string.push_str(&format!("{} --printenv {}", quoted_zed_path, redir)); + // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc) + command_string.push_str(&format!("cd {};", quoted_dir)); + if let Some(prefix) = shell_kind.command_prefix() { + command_string.push(prefix); + } + command_string.push_str(&format!("{} --printenv {}", quoted_zed_path, redir)); - if let ShellKind::Nushell = shell_kind { - command_string.push_str("; exit"); - } + if let ShellKind::Nushell = shell_kind { + command_string.push_str("; exit"); + } - command.arg(&command_string); + command.arg(&command_string); - super::set_pre_exec_to_start_new_session(&mut command); + super::set_pre_exec_to_start_new_session(&mut command); - let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?; - let env_output = String::from_utf8_lossy(&env_output); + let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?; + let env_output = String::from_utf8_lossy(&env_output); - parse_env_output( - &env_output, - &process_output.status, - || { - format!( - "login shell exited with {} but environment was captured successfully. stderr: {:?}", - process_output.status, - String::from_utf8_lossy(&process_output.stderr), - ) - }, - || { - format!( - "login shell exited with {}. stdout: {:?}, stderr: {:?}", - process_output.status, - String::from_utf8_lossy(&process_output.stdout), - String::from_utf8_lossy(&process_output.stderr), - ) - }, - ) + parse_env_output( + &env_output, + &process_output.status, + || { + format!( + "login shell exited with {} but environment was captured successfully. stderr: {:?}", + process_output.status, + String::from_utf8_lossy(&process_output.stderr), + ) + }, + || { + format!( + "login shell exited with {}. stdout: {:?}, stderr: {:?}", + process_output.status, + String::from_utf8_lossy(&process_output.stdout), + String::from_utf8_lossy(&process_output.stderr), + ) + }, + ) } #[cfg(unix)] async fn spawn_and_read_fd( - mut command: std::process::Command, - child_fd: std::os::fd::RawFd, + mut command: std::process::Command, + child_fd: std::os::fd::RawFd, ) -> anyhow::Result<(Vec, std::process::Output)> { - use command_fds::{CommandFdExt, FdMapping}; - use std::{io::Read, process::Stdio}; + use command_fds::{CommandFdExt, FdMapping}; + use std::{io::Read, process::Stdio}; - let (mut reader, writer) = std::io::pipe()?; + let (mut reader, writer) = std::io::pipe()?; - command.fd_mappings(vec![FdMapping { - parent_fd: writer.into(), - child_fd, - }])?; + command.fd_mappings(vec![FdMapping { + parent_fd: writer.into(), + child_fd, + }])?; - let process = smol::process::Command::from(command) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; + let process = smol::process::Command::from(command) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer)?; - Ok((buffer, process.output().await?)) + Ok((buffer, process.output().await?)) } #[cfg(windows)] async fn capture_windows( - shell_path: &Path, - args: &[String], - directory: &Path, + shell_path: &Path, + args: &[String], + directory: &Path, ) -> Result> { - use std::process::Stdio; + use std::process::Stdio; - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; + let zed_path = + std::env::current_exe().context("Failed to determine current zed executable path.")?; - let shell_kind = ShellKind::new(shell_path, true); - // Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag - let directory_string = directory.display().to_string(); - let directory_string = if directory_string.starts_with('-') { - format!("./{directory_string}") - } else { - directory_string - }; - let zed_path_string = zed_path.display().to_string(); - let quote_for_shell = |value: &str| { - shell_kind - .try_quote(value) - .map(|quoted| quoted.into_owned()) - .context("unexpected null in directory name") - }; - let mut cmd = crate::command::new_command(shell_path); - cmd.args(args); - let quoted_directory = quote_for_shell(&directory_string)?; - let quoted_zed_path = quote_for_shell(&zed_path_string)?; - let cmd = match shell_kind { - ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Xonsh - | ShellKind::Posix => cmd.args([ - "-l", - "-i", - "-c", - &format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path), - ]), - ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([ - "-NonInteractive", - "-NoProfile", - "-Command", - &format!( - "Set-Location {}; & {} --printenv", - quoted_directory, quoted_zed_path - ), - ]), - ShellKind::Elvish => cmd.args([ - "-c", - &format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path), - ]), - ShellKind::Nushell => { - let zed_command = shell_kind - .prepend_command_prefix("ed_zed_path) - .into_owned(); - cmd.args([ - "-c", - &format!("cd {}; {} --printenv", quoted_directory, zed_command), - ]) - } - ShellKind::Cmd => { - let dir = directory_string.trim_end_matches('\\'); - cmd.args(["/d", "/c", "cd", dir, "&&", &zed_path_string, "--printenv"]) - } - } - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let output = cmd - .output() - .await - .with_context(|| format!("command {cmd:?}"))?; - let env_output = String::from_utf8_lossy(&output.stdout); + let shell_kind = ShellKind::new(shell_path, true); + // Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag + let directory_string = directory.display().to_string(); + let directory_string = if directory_string.starts_with('-') { + format!("./{directory_string}") + } else { + directory_string + }; + let zed_path_string = zed_path.display().to_string(); + let quote_for_shell = |value: &str| { + shell_kind + .try_quote(value) + .map(|quoted| quoted.into_owned()) + .context("unexpected null in directory name") + }; + let mut cmd = crate::command::new_command(shell_path); + cmd.args(args); + let quoted_directory = quote_for_shell(&directory_string)?; + let quoted_zed_path = quote_for_shell(&zed_path_string)?; + let cmd = match shell_kind { + ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Xonsh + | ShellKind::Posix => cmd.args([ + "-l", + "-i", + "-c", + &format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path), + ]), + ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([ + "-NonInteractive", + "-NoProfile", + "-Command", + &format!( + "Set-Location {}; & {} --printenv", + quoted_directory, quoted_zed_path + ), + ]), + ShellKind::Elvish => cmd.args([ + "-c", + &format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path), + ]), + ShellKind::Nushell => { + let zed_command = shell_kind + .prepend_command_prefix("ed_zed_path) + .into_owned(); + cmd.args([ + "-c", + &format!("cd {}; {} --printenv", quoted_directory, zed_command), + ]) + } + ShellKind::Cmd => { + let dir = directory_string.trim_end_matches('\\'); + cmd.args(["/d", "/c", "cd", dir, "&&", &zed_path_string, "--printenv"]) + } + } + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let output = cmd + .output() + .await + .with_context(|| format!("command {cmd:?}"))?; + let env_output = String::from_utf8_lossy(&output.stdout); - parse_env_output( - &env_output, - &output.status, - || { - format!( - "Command {cmd:?} exited with {} but environment was captured successfully. stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stderr), - ) - }, - || { - format!( - "Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ) - }, - ) + parse_env_output( + &env_output, + &output.status, + || { + format!( + "Command {cmd:?} exited with {} but environment was captured successfully. stderr: {:?}", + output.status, + String::from_utf8_lossy(&output.stderr), + ) + }, + || { + format!( + "Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ) + }, + ) } #[cfg(test)] mod tests { - use std::process::ExitStatus; + use std::process::ExitStatus; - use super::*; + use super::*; - #[cfg(unix)] - fn exit_status(code: i32) -> ExitStatus { - use std::os::unix::process::ExitStatusExt; + #[cfg(unix)] + fn exit_status(code: i32) -> ExitStatus { + use std::os::unix::process::ExitStatusExt; - ExitStatus::from_raw(code << 8) - } + ExitStatus::from_raw(code << 8) + } - #[cfg(windows)] - fn exit_status(code: u32) -> ExitStatus { - use std::os::windows::process::ExitStatusExt; + #[cfg(windows)] + fn exit_status(code: u32) -> ExitStatus { + use std::os::windows::process::ExitStatusExt; - ExitStatus::from_raw(code) - } + ExitStatus::from_raw(code) + } - #[test] - fn parse_env_output_accepts_valid_env_when_shell_exits_nonzero() { - let env_json = serde_json::json!({ - "PATH": "/usr/bin", - "SHELL": "/bin/zsh", - }); - let env_output = format!("shell startup noise\n{env_json}\nshell shutdown noise"); + #[test] + fn parse_env_output_accepts_valid_env_when_shell_exits_nonzero() { + let env_json = serde_json::json!({ + "PATH": "/usr/bin", + "SHELL": "/bin/zsh", + }); + let env_output = format!("shell startup noise\n{env_json}\nshell shutdown noise"); - let env_map = parse_env_output( - &env_output, - &exit_status(1), - || "shell exited with 1 but environment was captured successfully".to_string(), - || panic!("failed capture error should not be evaluated for valid environment output"), - ) - .expect("valid environment output should be returned despite non-zero shell exit"); - assert_eq!(env_map.get("PATH").map(String::as_str), Some("/usr/bin")); - assert_eq!(env_map.get("SHELL").map(String::as_str), Some("/bin/zsh")); - } + let env_map = parse_env_output( + &env_output, + &exit_status(1), + || "shell exited with 1 but environment was captured successfully".to_string(), + || panic!("failed capture error should not be evaluated for valid environment output"), + ) + .expect("valid environment output should be returned despite non-zero shell exit"); + assert_eq!(env_map.get("PATH").map(String::as_str), Some("/usr/bin")); + assert_eq!(env_map.get("SHELL").map(String::as_str), Some("/bin/zsh")); + } } diff --git a/crates/gpui_zed_util/src/size.rs b/crates/gpui_zed_util/src/size.rs index c6ecebd548..a07769c296 100644 --- a/crates/gpui_zed_util/src/size.rs +++ b/crates/gpui_zed_util/src/size.rs @@ -1,46 +1,46 @@ pub fn format_file_size(size: u64, use_decimal: bool) -> String { - if use_decimal { - if size < 1000 { - format!("{size}B") - } else if size < 1000 * 1000 { - format!("{:.1}KB", size as f64 / 1000.0) - } else { - format!("{:.1}MB", size as f64 / (1000.0 * 1000.0)) - } - } else if size < 1024 { - format!("{size}B") - } else if size < 1024 * 1024 { - format!("{:.1}KiB", size as f64 / 1024.0) - } else { - format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) - } + if use_decimal { + if size < 1000 { + format!("{size}B") + } else if size < 1000 * 1000 { + format!("{:.1}KB", size as f64 / 1000.0) + } else { + format!("{:.1}MB", size as f64 / (1000.0 * 1000.0)) + } + } else if size < 1024 { + format!("{size}B") + } else if size < 1024 * 1024 { + format!("{:.1}KiB", size as f64 / 1024.0) + } else { + format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_format_file_size_decimal() { - assert_eq!(format_file_size(0, true), "0B"); - assert_eq!(format_file_size(999, true), "999B"); - assert_eq!(format_file_size(1000, true), "1.0KB"); - assert_eq!(format_file_size(1500, true), "1.5KB"); - assert_eq!(format_file_size(999999, true), "1000.0KB"); - assert_eq!(format_file_size(1000000, true), "1.0MB"); - assert_eq!(format_file_size(1500000, true), "1.5MB"); - assert_eq!(format_file_size(10000000, true), "10.0MB"); - } + #[test] + fn test_format_file_size_decimal() { + assert_eq!(format_file_size(0, true), "0B"); + assert_eq!(format_file_size(999, true), "999B"); + assert_eq!(format_file_size(1000, true), "1.0KB"); + assert_eq!(format_file_size(1500, true), "1.5KB"); + assert_eq!(format_file_size(999999, true), "1000.0KB"); + assert_eq!(format_file_size(1000000, true), "1.0MB"); + assert_eq!(format_file_size(1500000, true), "1.5MB"); + assert_eq!(format_file_size(10000000, true), "10.0MB"); + } - #[test] - fn test_format_file_size_binary() { - assert_eq!(format_file_size(0, false), "0B"); - assert_eq!(format_file_size(1023, false), "1023B"); - assert_eq!(format_file_size(1024, false), "1.0KiB"); - assert_eq!(format_file_size(1536, false), "1.5KiB"); - assert_eq!(format_file_size(1048575, false), "1024.0KiB"); - assert_eq!(format_file_size(1048576, false), "1.0MiB"); - assert_eq!(format_file_size(1572864, false), "1.5MiB"); - assert_eq!(format_file_size(10485760, false), "10.0MiB"); - } + #[test] + fn test_format_file_size_binary() { + assert_eq!(format_file_size(0, false), "0B"); + assert_eq!(format_file_size(1023, false), "1023B"); + assert_eq!(format_file_size(1024, false), "1.0KiB"); + assert_eq!(format_file_size(1536, false), "1.5KiB"); + assert_eq!(format_file_size(1048575, false), "1024.0KiB"); + assert_eq!(format_file_size(1048576, false), "1.0MiB"); + assert_eq!(format_file_size(1572864, false), "1.5MiB"); + assert_eq!(format_file_size(10485760, false), "10.0MiB"); + } } diff --git a/crates/gpui_zed_util/src/test.rs b/crates/gpui_zed_util/src/test.rs index 717754e333..9b2d90770d 100644 --- a/crates/gpui_zed_util/src/test.rs +++ b/crates/gpui_zed_util/src/test.rs @@ -10,71 +10,71 @@ use std::path::{Path, PathBuf}; use tempfile::TempDir; pub struct TempTree { - _temp_dir: TempDir, - path: PathBuf, + _temp_dir: TempDir, + path: PathBuf, } impl TempTree { - pub fn new(tree: serde_json::Value) -> Self { - let dir = TempDir::new().unwrap(); - let path = std::fs::canonicalize(dir.path()).unwrap(); - write_tree(path.as_path(), tree); + pub fn new(tree: serde_json::Value) -> Self { + let dir = TempDir::new().unwrap(); + let path = std::fs::canonicalize(dir.path()).unwrap(); + write_tree(path.as_path(), tree); - Self { - _temp_dir: dir, - path, - } - } + Self { + _temp_dir: dir, + path, + } + } - pub fn path(&self) -> &Path { - self.path.as_path() - } + pub fn path(&self) -> &Path { + self.path.as_path() + } } fn write_tree(path: &Path, tree: serde_json::Value) { - use serde_json::Value; - use std::fs; + use serde_json::Value; + use std::fs; - if let Value::Object(map) = tree { - for (name, contents) in map { - let mut path = PathBuf::from(path); - path.push(name); - match contents { - Value::Object(_) => { - fs::create_dir(&path).unwrap(); + if let Value::Object(map) = tree { + for (name, contents) in map { + let mut path = PathBuf::from(path); + path.push(name); + match contents { + Value::Object(_) => { + fs::create_dir(&path).unwrap(); - #[cfg(not(target_family = "wasm"))] - if path.file_name() == Some(OsStr::new(".git")) { - git2::Repository::init(path.parent().unwrap()).unwrap(); - } + #[cfg(not(target_family = "wasm"))] + if path.file_name() == Some(OsStr::new(".git")) { + git2::Repository::init(path.parent().unwrap()).unwrap(); + } - write_tree(&path, contents); - } - Value::Null => { - fs::create_dir(&path).unwrap(); - } - Value::String(contents) => { - fs::write(&path, contents).unwrap(); - } - _ => { - panic!("JSON object must contain only objects, strings, or null"); - } - } - } - } else { - panic!("You must pass a JSON object to this helper") - } + write_tree(&path, contents); + } + Value::Null => { + fs::create_dir(&path).unwrap(); + } + Value::String(contents) => { + fs::write(&path, contents).unwrap(); + } + _ => { + panic!("JSON object must contain only objects, strings, or null"); + } + } + } + } else { + panic!("You must pass a JSON object to this helper") + } } pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = (start_char as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text + let mut text = String::new(); + for row in 0..rows { + let c: char = (start_char as u32 + row as u32) as u8 as char; + let mut line = c.to_string().repeat(cols); + if row < rows - 1 { + line.push('\n'); + } + text += &line; + } + text } diff --git a/crates/gpui_zed_util/src/test/assertions.rs b/crates/gpui_zed_util/src/test/assertions.rs index afb1397fa9..909971dd40 100644 --- a/crates/gpui_zed_util/src/test/assertions.rs +++ b/crates/gpui_zed_util/src/test/assertions.rs @@ -1,62 +1,68 @@ pub enum SetEqError { - LeftMissing(T), - RightMissing(T), + LeftMissing(T), + RightMissing(T), } impl SetEqError { - pub fn map R>(self, update: F) -> SetEqError { - match self { - SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)), - SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)), - } - } + pub fn map R>(self, update: F) -> SetEqError { + match self { + SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)), + SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)), + } + } } #[macro_export] macro_rules! set_eq { - ($left:expr,$right:expr) => {{ - use util::test::*; + ($left:expr,$right:expr) => {{ + use util::test::*; - let left = $left; - let right = $right; + let left = $left; + let right = $right; - let mut result = Ok(()); - for right_value in right.iter() { - if !left.contains(right_value) { - result = Err(SetEqError::LeftMissing(right_value.clone())); - break; - } - } + let mut result = Ok(()); + for right_value in right.iter() { + if !left.contains(right_value) { + result = Err(SetEqError::LeftMissing(right_value.clone())); + break; + } + } - if result.is_ok() { - for left_value in left.iter() { - if !right.contains(left_value) { - result = Err(SetEqError::RightMissing(left_value.clone())); - } - } - } + if result.is_ok() { + for left_value in left.iter() { + if !right.contains(left_value) { + result = Err(SetEqError::RightMissing(left_value.clone())); + } + } + } - result - }}; + result + }}; } #[macro_export] macro_rules! assert_set_eq { - ($left:expr,$right:expr) => {{ - use util::test::*; - use util::set_eq; + ($left:expr,$right:expr) => {{ + use util::set_eq; + use util::test::*; - let left = $left; - let right = $right; + let left = $left; + let right = $right; - match set_eq!(&left, &right) { - Err(SetEqError::LeftMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); - }, - Err(SetEqError::RightMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); - }, - _ => {} - } - }}; + match set_eq!(&left, &right) { + Err(SetEqError::LeftMissing(missing)) => { + panic!( + "assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", + &left, &right, &missing + ); + } + Err(SetEqError::RightMissing(missing)) => { + panic!( + "assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", + &left, &right, &missing + ); + } + _ => {} + } + }}; } diff --git a/crates/gpui_zed_util/src/test/marked_text.rs b/crates/gpui_zed_util/src/test/marked_text.rs index 282a477935..7235601a08 100644 --- a/crates/gpui_zed_util/src/test/marked_text.rs +++ b/crates/gpui_zed_util/src/test/marked_text.rs @@ -4,22 +4,22 @@ use std::{cmp::Ordering, ops::Range}; /// Construct a string and a list of offsets within that string using a single /// string containing embedded position markers. pub fn marked_text_offsets_by( - marked_text: &str, - markers: Vec, + marked_text: &str, + markers: Vec, ) -> (String, HashMap>) { - let mut extracted_markers: HashMap> = Default::default(); - let mut unmarked_text = String::new(); + let mut extracted_markers: HashMap> = Default::default(); + let mut unmarked_text = String::new(); - for char in marked_text.chars() { - if markers.contains(&char) { - let char_offsets = extracted_markers.entry(char).or_default(); - char_offsets.push(unmarked_text.len()); - } else { - unmarked_text.push(char); - } - } + for char in marked_text.chars() { + if markers.contains(&char) { + let char_offsets = extracted_markers.entry(char).or_default(); + char_offsets.push(unmarked_text.len()); + } else { + unmarked_text.push(char); + } + } - (unmarked_text, extracted_markers) + (unmarked_text, extracted_markers) } /// Construct a string and a list of ranges within that string using a single @@ -29,56 +29,56 @@ pub fn marked_text_offsets_by( /// /// The returned ranges will be grouped by their range marking characters. pub fn marked_text_ranges_by( - marked_text: &str, - markers: Vec, + marked_text: &str, + markers: Vec, ) -> (String, HashMap>>) { - let all_markers = markers.iter().flat_map(|m| m.markers()).collect(); + let all_markers = markers.iter().flat_map(|m| m.markers()).collect(); - let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers); - let range_lookup = markers - .into_iter() - .map(|marker| { - ( - marker.clone(), - match marker { - TextRangeMarker::Empty(empty_marker_char) => marker_offsets - .remove(&empty_marker_char) - .unwrap_or_default() - .into_iter() - .map(|empty_index| empty_index..empty_index) - .collect::>>(), - TextRangeMarker::Range(start_marker, end_marker) => { - let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); - let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); - assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); - starts - .into_iter() - .zip(ends) - .map(|(start, end)| { - assert!(end >= start, "marked ranges must be disjoint"); - start..end - }) - .collect::>>() - } - TextRangeMarker::ReverseRange(start_marker, end_marker) => { - let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); - let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); - assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); - starts - .into_iter() - .zip(ends) - .map(|(start, end)| { - assert!(end >= start, "marked ranges must be disjoint"); - end..start - }) - .collect::>>() - } - }, - ) - }) - .collect(); + let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers); + let range_lookup = markers + .into_iter() + .map(|marker| { + ( + marker.clone(), + match marker { + TextRangeMarker::Empty(empty_marker_char) => marker_offsets + .remove(&empty_marker_char) + .unwrap_or_default() + .into_iter() + .map(|empty_index| empty_index..empty_index) + .collect::>>(), + TextRangeMarker::Range(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(end >= start, "marked ranges must be disjoint"); + start..end + }) + .collect::>>() + } + TextRangeMarker::ReverseRange(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(end >= start, "marked ranges must be disjoint"); + end..start + }) + .collect::>>() + } + }, + ) + }) + .collect(); - (unmarked_text, range_lookup) + (unmarked_text, range_lookup) } /// Construct a string and a list of ranges within that string using a single @@ -111,171 +111,171 @@ pub fn marked_text_ranges_by( /// source code. #[track_caller] pub fn marked_text_ranges( - marked_text: &str, - ranges_are_directed: bool, + marked_text: &str, + ranges_are_directed: bool, ) -> (String, Vec>) { - let mut unmarked_text = String::with_capacity(marked_text.len()); - let mut ranges = Vec::new(); - let mut prev_marked_ix = 0; - let mut current_range_start = None; - let mut current_range_cursor = None; + let mut unmarked_text = String::with_capacity(marked_text.len()); + let mut ranges = Vec::new(); + let mut prev_marked_ix = 0; + let mut current_range_start = None; + let mut current_range_cursor = None; - let marked_text = marked_text.replace('•', " "); - for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) { - unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]); - let unmarked_len = unmarked_text.len(); - let len = marker.len(); - prev_marked_ix = marked_ix + len; + let marked_text = marked_text.replace('•', " "); + for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) { + unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]); + let unmarked_len = unmarked_text.len(); + let len = marker.len(); + prev_marked_ix = marked_ix + len; - match marker { - "ˇ" => { - if current_range_start.is_some() { - if current_range_cursor.is_some() { - panic!("duplicate point marker 'ˇ' at index {marked_ix}"); - } + match marker { + "ˇ" => { + if current_range_start.is_some() { + if current_range_cursor.is_some() { + panic!("duplicate point marker 'ˇ' at index {marked_ix}"); + } - current_range_cursor = Some(unmarked_len); - } else { - ranges.push(unmarked_len..unmarked_len); - } - } - "«" => { - if current_range_start.is_some() { - panic!("unexpected range start marker '«' at index {marked_ix}"); - } - current_range_start = Some(unmarked_len); - } - "»" => { - let current_range_start = if let Some(start) = current_range_start.take() { - start - } else { - panic!("unexpected range end marker '»' at index {marked_ix}"); - }; + current_range_cursor = Some(unmarked_len); + } else { + ranges.push(unmarked_len..unmarked_len); + } + } + "«" => { + if current_range_start.is_some() { + panic!("unexpected range start marker '«' at index {marked_ix}"); + } + current_range_start = Some(unmarked_len); + } + "»" => { + let current_range_start = if let Some(start) = current_range_start.take() { + start + } else { + panic!("unexpected range end marker '»' at index {marked_ix}"); + }; - let mut reversed = false; - if let Some(current_range_cursor) = current_range_cursor.take() { - if current_range_cursor == current_range_start { - reversed = true; - } else if current_range_cursor != unmarked_len { - panic!("unexpected 'ˇ' marker in the middle of a range"); - } - } else if ranges_are_directed { - panic!("missing 'ˇ' marker to indicate range direction"); - } + let mut reversed = false; + if let Some(current_range_cursor) = current_range_cursor.take() { + if current_range_cursor == current_range_start { + reversed = true; + } else if current_range_cursor != unmarked_len { + panic!("unexpected 'ˇ' marker in the middle of a range"); + } + } else if ranges_are_directed { + panic!("missing 'ˇ' marker to indicate range direction"); + } - ranges.push(if reversed { - unmarked_len..current_range_start - } else { - current_range_start..unmarked_len - }); - } - _ => unreachable!(), - } - } + ranges.push(if reversed { + unmarked_len..current_range_start + } else { + current_range_start..unmarked_len + }); + } + _ => unreachable!(), + } + } - unmarked_text.push_str(&marked_text[prev_marked_ix..]); - (unmarked_text, ranges) + unmarked_text.push_str(&marked_text[prev_marked_ix..]); + (unmarked_text, ranges) } #[track_caller] pub fn marked_text_offsets(marked_text: &str) -> (String, Vec) { - let (text, ranges) = marked_text_ranges(marked_text, false); - ( - text, - ranges - .into_iter() - .map(|range| { - assert_eq!(range.start, range.end); - range.start - }) - .collect(), - ) + let (text, ranges) = marked_text_ranges(marked_text, false); + ( + text, + ranges + .into_iter() + .map(|range| { + assert_eq!(range.start, range.end); + range.start + }) + .collect(), + ) } pub fn generate_marked_text( - unmarked_text: &str, - ranges: &[Range], - indicate_cursors: bool, + unmarked_text: &str, + ranges: &[Range], + indicate_cursors: bool, ) -> String { - let mut marked_text = unmarked_text.to_string(); - for range in ranges.iter().rev() { - if indicate_cursors { - match range.start.cmp(&range.end) { - Ordering::Less => { - marked_text.insert_str(range.end, "ˇ»"); - marked_text.insert(range.start, '«'); - } - Ordering::Equal => { - marked_text.insert(range.start, 'ˇ'); - } - Ordering::Greater => { - marked_text.insert(range.start, '»'); - marked_text.insert_str(range.end, "«ˇ"); - } - } - } else { - match range.start.cmp(&range.end) { - Ordering::Equal => { - marked_text.insert(range.start, 'ˇ'); - } - _ => { - marked_text.insert(range.end, '»'); - marked_text.insert(range.start, '«'); - } - } - } - } - marked_text + let mut marked_text = unmarked_text.to_string(); + for range in ranges.iter().rev() { + if indicate_cursors { + match range.start.cmp(&range.end) { + Ordering::Less => { + marked_text.insert_str(range.end, "ˇ»"); + marked_text.insert(range.start, '«'); + } + Ordering::Equal => { + marked_text.insert(range.start, 'ˇ'); + } + Ordering::Greater => { + marked_text.insert(range.start, '»'); + marked_text.insert_str(range.end, "«ˇ"); + } + } + } else { + match range.start.cmp(&range.end) { + Ordering::Equal => { + marked_text.insert(range.start, 'ˇ'); + } + _ => { + marked_text.insert(range.end, '»'); + marked_text.insert(range.start, '«'); + } + } + } + } + marked_text } #[derive(Clone, Eq, PartialEq, Hash)] pub enum TextRangeMarker { - Empty(char), - Range(char, char), - ReverseRange(char, char), + Empty(char), + Range(char, char), + ReverseRange(char, char), } impl TextRangeMarker { - fn markers(&self) -> Vec { - match self { - Self::Empty(m) => vec![*m], - Self::Range(l, r) => vec![*l, *r], - Self::ReverseRange(l, r) => vec![*l, *r], - } - } + fn markers(&self) -> Vec { + match self { + Self::Empty(m) => vec![*m], + Self::Range(l, r) => vec![*l, *r], + Self::ReverseRange(l, r) => vec![*l, *r], + } + } } impl From for TextRangeMarker { - fn from(marker: char) -> Self { - Self::Empty(marker) - } + fn from(marker: char) -> Self { + Self::Empty(marker) + } } impl From<(char, char)> for TextRangeMarker { - fn from((left_marker, right_marker): (char, char)) -> Self { - Self::Range(left_marker, right_marker) - } + fn from((left_marker, right_marker): (char, char)) -> Self { + Self::Range(left_marker, right_marker) + } } #[cfg(test)] mod tests { - use super::{generate_marked_text, marked_text_ranges}; + use super::{generate_marked_text, marked_text_ranges}; - #[allow(clippy::reversed_empty_ranges)] - #[test] - fn test_marked_text() { - let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true); + #[allow(clippy::reversed_empty_ranges)] + #[test] + fn test_marked_text() { + let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true); - assert_eq!(text, "one two three four five six"); - assert_eq!(ranges.len(), 4); - assert_eq!(ranges[0], 7..4); - assert_eq!(ranges[1], 8..13); - assert_eq!(ranges[2], 18..14); - assert_eq!(ranges[3], 23..23); + assert_eq!(text, "one two three four five six"); + assert_eq!(ranges.len(), 4); + assert_eq!(ranges[0], 7..4); + assert_eq!(ranges[1], 8..13); + assert_eq!(ranges[2], 18..14); + assert_eq!(ranges[3], 23..23); - assert_eq!( - generate_marked_text(&text, &ranges, true), - "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six" - ); - } + assert_eq!( + generate_marked_text(&text, &ranges, true), + "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six" + ); + } } diff --git a/crates/gpui_zed_util/src/time.rs b/crates/gpui_zed_util/src/time.rs index 092d447dab..33456daed7 100644 --- a/crates/gpui_zed_util/src/time.rs +++ b/crates/gpui_zed_util/src/time.rs @@ -1,33 +1,33 @@ use std::time::Duration; pub fn duration_alt_display(duration: Duration) -> String { - let hours = duration.as_secs() / 3600; - let minutes = (duration.as_secs() % 3600) / 60; - let seconds = duration.as_secs() % 60; + let hours = duration.as_secs() / 3600; + let minutes = (duration.as_secs() % 3600) / 60; + let seconds = duration.as_secs() % 60; - if hours > 0 { - format!("{hours}h {minutes}m {seconds}s") - } else if minutes > 0 { - format!("{minutes}m {seconds}s") - } else { - format!("{seconds}s") - } + if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_duration_alt_display() { - use duration_alt_display as f; - assert_eq!("0s", f(Duration::from_secs(0))); - assert_eq!("59s", f(Duration::from_secs(59))); - assert_eq!("1m 0s", f(Duration::from_secs(60))); - assert_eq!("10m 0s", f(Duration::from_secs(600))); - assert_eq!("1h 0m 0s", f(Duration::from_secs(3600))); - assert_eq!("3h 2m 1s", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1))); - assert_eq!("23h 59m 59s", f(Duration::from_secs(3600 * 24 - 1))); - assert_eq!("100h 0m 0s", f(Duration::from_secs(3600 * 100))); - } + #[test] + fn test_duration_alt_display() { + use duration_alt_display as f; + assert_eq!("0s", f(Duration::from_secs(0))); + assert_eq!("59s", f(Duration::from_secs(59))); + assert_eq!("1m 0s", f(Duration::from_secs(60))); + assert_eq!("10m 0s", f(Duration::from_secs(600))); + assert_eq!("1h 0m 0s", f(Duration::from_secs(3600))); + assert_eq!("3h 2m 1s", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1))); + assert_eq!("23h 59m 59s", f(Duration::from_secs(3600 * 24 - 1))); + assert_eq!("100h 0m 0s", f(Duration::from_secs(3600 * 100))); + } } diff --git a/crates/gpui_zed_util/src/util.rs b/crates/gpui_zed_util/src/util.rs index 3c4e3684a9..6cd2d9ec61 100644 --- a/crates/gpui_zed_util/src/util.rs +++ b/crates/gpui_zed_util/src/util.rs @@ -24,9 +24,9 @@ use regex::Regex; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::{ - borrow::Cow, - cmp::{self, Ordering}, - ops::{Range, RangeInclusive}, + borrow::Cow, + cmp::{self, Ordering}, + ops::{Range, RangeInclusive}, }; use unicase::UniCase; @@ -35,129 +35,129 @@ pub use gpui_util::*; pub use take_until::*; pub use self::shell::{ - get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell, + get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell, }; #[inline] pub const fn is_utf8_char_boundary(u8: u8) -> bool { - // This is bit magic equivalent to: b < 128 || b >= 192 - (u8 as i8) >= -0x40 + // This is bit magic equivalent to: b < 128 || b >= 192 + (u8 as i8) >= -0x40 } pub fn truncate(s: &str, max_chars: usize) -> &str { - match s.char_indices().nth(max_chars) { - None => s, - Some((idx, _)) => &s[..idx], - } + match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx], + } } /// Removes characters from the end of the string if its length is greater than `max_chars` and /// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars. pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { - debug_assert!(max_chars >= 5); + debug_assert!(max_chars >= 5); - // If the string's byte length is <= max_chars, walking the string can be skipped since the - // number of chars is <= the number of bytes. - if s.len() <= max_chars { - return s.to_string(); - } - let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars); - match truncation_ix { - Some(index) => s[..index].to_string() + "…", - _ => s.to_string(), - } + // If the string's byte length is <= max_chars, walking the string can be skipped since the + // number of chars is <= the number of bytes. + if s.len() <= max_chars { + return s.to_string(); + } + let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars); + match truncation_ix { + Some(index) => s[..index].to_string() + "…", + _ => s.to_string(), + } } /// Removes characters from the front of the string if its length is greater than `max_chars` and /// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars. pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { - debug_assert!(max_chars >= 5); + debug_assert!(max_chars >= 5); - // If the string's byte length is <= max_chars, walking the string can be skipped since the - // number of chars is <= the number of bytes. - if s.len() <= max_chars { - return s.to_string(); - } - let suffix_char_length = max_chars.saturating_sub(1); - let truncation_ix = s - .char_indices() - .map(|(i, _)| i) - .nth_back(suffix_char_length); - match truncation_ix { - Some(index) if index > 0 => "…".to_string() + &s[index..], - _ => s.to_string(), - } + // If the string's byte length is <= max_chars, walking the string can be skipped since the + // number of chars is <= the number of bytes. + if s.len() <= max_chars { + return s.to_string(); + } + let suffix_char_length = max_chars.saturating_sub(1); + let truncation_ix = s + .char_indices() + .map(|(i, _)| i) + .nth_back(suffix_char_length); + match truncation_ix { + Some(index) if index > 0 => "…".to_string() + &s[index..], + _ => s.to_string(), + } } /// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a /// a newline and "..." to the string, so that `max_lines` are returned. /// Returns string unchanged if its length is smaller than max_lines. pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String { - let mut lines = s.lines().take(max_lines).collect::>(); - if lines.len() > max_lines - 1 { - lines.pop(); - lines.join("\n") + "\n…" - } else { - lines.join("\n") - } + let mut lines = s.lines().take(max_lines).collect::>(); + if lines.len() > max_lines - 1 { + lines.pop(); + lines.join("\n") + "\n…" + } else { + lines.join("\n") + } } /// Truncates the string at a character boundary, such that the result is less than `max_bytes` in /// length. pub fn truncate_to_byte_limit(s: &str, max_bytes: usize) -> &str { - if s.len() < max_bytes { - return s; - } + if s.len() < max_bytes { + return s; + } - for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) { - return &s[..i]; - } - } + for i in (0..max_bytes).rev() { + if s.is_char_boundary(i) { + return &s[..i]; + } + } - "" + "" } /// Takes a prefix of complete lines which fit within the byte limit. If the first line is longer /// than the limit, truncates at a character boundary. pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str { - if s.len() < max_bytes { - return s; - } + if s.len() < max_bytes { + return s; + } - for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' { - // Since the i-th character is \n, valid to slice at i + 1. - return &s[..i + 1]; - } - } + for i in (0..max_bytes).rev() { + if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' { + // Since the i-th character is \n, valid to slice at i + 1. + return &s[..i + 1]; + } + } - truncate_to_byte_limit(s, max_bytes) + truncate_to_byte_limit(s, max_bytes) } #[test] fn test_truncate_lines_to_byte_limit() { - let text = "Line 1\nLine 2\nLine 3\nLine 4"; + let text = "Line 1\nLine 2\nLine 3\nLine 4"; - // Limit that includes all lines - assert_eq!(truncate_lines_to_byte_limit(text, 100), text); + // Limit that includes all lines + assert_eq!(truncate_lines_to_byte_limit(text, 100), text); - // Exactly the first line - assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n"); + // Exactly the first line + assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n"); - // Limit between lines - assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n"); - assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n"); + // Limit between lines + assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n"); + assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n"); - // Limit before first newline - assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line "); + // Limit before first newline + assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line "); - // Test with non-ASCII characters - let text_utf8 = "Line 1\nLíne 2\nLine 3"; - assert_eq!( - truncate_lines_to_byte_limit(text_utf8, 15), - "Line 1\nLíne 2\n" - ); + // Test with non-ASCII characters + let text_utf8 = "Line 1\nLíne 2\nLine 3"; + assert_eq!( + truncate_lines_to_byte_limit(text_utf8, 15), + "Line 1\nLíne 2\n" + ); } /// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and @@ -165,44 +165,44 @@ fn test_truncate_lines_to_byte_limit() { /// both `vec` and `new_items` should already be sorted according to the `cmp` comparator. pub fn extend_sorted(vec: &mut Vec, new_items: I, limit: usize, mut cmp: F) where - I: IntoIterator, - F: FnMut(&T, &T) -> Ordering, + I: IntoIterator, + F: FnMut(&T, &T) -> Ordering, { - let mut start_index = 0; - for new_item in new_items { - if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) { - let index = start_index + i; - if vec.len() < limit { - vec.insert(index, new_item); - } else if index < vec.len() { - vec.pop(); - vec.insert(index, new_item); - } - start_index = index; - } - } + let mut start_index = 0; + for new_item in new_items { + if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) { + let index = start_index + i; + if vec.len() < limit { + vec.insert(index, new_item); + } else if index < vec.len() { + vec.pop(); + vec.insert(index, new_item); + } + start_index = index; + } + } } pub fn truncate_to_bottom_n_sorted_by(items: &mut Vec, limit: usize, compare: &F) where - F: Fn(&T, &T) -> Ordering, + F: Fn(&T, &T) -> Ordering, { - if limit == 0 { - items.clear(); - } - if items.len() <= limit { - items.sort_by(compare); - return; - } - // When limit is near to items.len() it may be more efficient to sort the whole list and - // truncate, rather than always doing selection first as is done below. It's hard to analyze - // where the threshold for this should be since the quickselect style algorithm used by - // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted - - // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary - // unsorted input. - items.select_nth_unstable_by(limit, compare); - items.truncate(limit); - items.sort_by(compare); + if limit == 0 { + items.clear(); + } + if items.len() <= limit { + items.sort_by(compare); + return; + } + // When limit is near to items.len() it may be more efficient to sort the whole list and + // truncate, rather than always doing selection first as is done below. It's hard to analyze + // where the threshold for this should be since the quickselect style algorithm used by + // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted - + // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary + // unsorted input. + items.select_nth_unstable_by(limit, compare); + items.truncate(limit); + items.sort_by(compare); } /// Prevents execution of the application with root privileges on Unix systems. @@ -212,172 +212,172 @@ where /// `ZED_ALLOW_ROOT` environment variable. #[cfg(unix)] pub fn prevent_root_execution() { - let is_root = nix::unistd::geteuid().is_root(); - let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true"); + let is_root = nix::unistd::geteuid().is_root(); + let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true"); - if is_root && !allow_root { - eprintln!( - "\ + if is_root && !allow_root { + eprintln!( + "\ Error: Running Zed as root or via sudo is unsupported. Doing so (even once) may subtly break things for all subsequent non-root usage of Zed. It is untested and not recommended, don't complain when things break. If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment." - ); - std::process::exit(1); - } + ); + std::process::exit(1); + } } #[cfg(unix)] fn load_shell_from_passwd() -> Result<()> { - let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } { - n if n < 0 => 1024, - n => n as usize, - }; - let mut buffer = Vec::with_capacity(buflen); + let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } { + n if n < 0 => 1024, + n => n as usize, + }; + let mut buffer = Vec::with_capacity(buflen); - let mut pwd: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); - let mut result: *mut libc::passwd = std::ptr::null_mut(); + let mut pwd: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); + let mut result: *mut libc::passwd = std::ptr::null_mut(); - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - pwd.as_mut_ptr(), - buffer.as_mut_ptr() as *mut libc::c_char, - buflen, - &mut result, - ) - }; - anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r( + uid, + pwd.as_mut_ptr(), + buffer.as_mut_ptr() as *mut libc::c_char, + buflen, + &mut result, + ) + }; + anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); - // SAFETY: If `getpwuid_r` doesn't error, we have the entry here. - let entry = unsafe { pwd.assume_init() }; + // SAFETY: If `getpwuid_r` doesn't error, we have the entry here. + let entry = unsafe { pwd.assume_init() }; - anyhow::ensure!( - status == 0, - "call to getpwuid_r failed. uid: {}, status: {}", - uid, - status - ); - anyhow::ensure!( - entry.pw_uid == uid, - "passwd entry has different uid ({}) than getuid ({}) returned", - entry.pw_uid, - uid, - ); + anyhow::ensure!( + status == 0, + "call to getpwuid_r failed. uid: {}, status: {}", + uid, + status + ); + anyhow::ensure!( + entry.pw_uid == uid, + "passwd entry has different uid ({}) than getuid ({}) returned", + entry.pw_uid, + uid, + ); - let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() }; - let should_set_shell = std::env::var("SHELL").map_or(true, |shell_env| { - shell_env != shell && !std::path::Path::new(&shell_env).exists() - }); + let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() }; + let should_set_shell = std::env::var("SHELL").map_or(true, |shell_env| { + shell_env != shell && !std::path::Path::new(&shell_env).exists() + }); - if should_set_shell { - log::info!( - "updating SHELL environment variable to value from passwd entry: {:?}", - shell, - ); - unsafe { std::env::set_var("SHELL", shell) }; - } + if should_set_shell { + log::info!( + "updating SHELL environment variable to value from passwd entry: {:?}", + shell, + ); + unsafe { std::env::set_var("SHELL", shell) }; + } - Ok(()) + Ok(()) } /// Returns a shell escaped path for the current zed executable pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result { - use anyhow::Context as _; - use paths::PathExt; - let mut zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - if cfg!(target_os = "linux") - && !zed_path.is_file() - && let Some(truncated) = zed_path - .clone() - .file_name() - .and_then(|s| s.to_str()) - .and_then(|n| n.strip_suffix(" (deleted)")) - { - // Might have been deleted during update; let's use the new binary if there is one. - zed_path.set_file_name(truncated); - } + use anyhow::Context as _; + use paths::PathExt; + let mut zed_path = + std::env::current_exe().context("Failed to determine current zed executable path.")?; + if cfg!(target_os = "linux") + && !zed_path.is_file() + && let Some(truncated) = zed_path + .clone() + .file_name() + .and_then(|s| s.to_str()) + .and_then(|n| n.strip_suffix(" (deleted)")) + { + // Might have been deleted during update; let's use the new binary if there is one. + zed_path.set_file_name(truncated); + } - zed_path - .try_shell_safe(shell_kind) - .context("Failed to shell-escape Zed executable path.") + zed_path + .try_shell_safe(shell_kind) + .context("Failed to shell-escape Zed executable path.") } /// Returns a path for the zed cli executable, this function /// should be called from the zed executable, not zed-cli. pub fn get_zed_cli_path() -> Result { - use anyhow::Context as _; - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - let parent = zed_path - .parent() - .context("Failed to determine parent directory of zed executable path.")?; + use anyhow::Context as _; + let zed_path = + std::env::current_exe().context("Failed to determine current zed executable path.")?; + let parent = zed_path + .parent() + .context("Failed to determine parent directory of zed executable path.")?; - let possible_locations: &[&str] = if cfg!(target_os = "macos") { - // On macOS, the zed executable and zed-cli are inside the app bundle, - // so here ./cli is for both installed and development builds. - &["./cli"] - } else if cfg!(target_os = "windows") { - // bin/zed.exe is for installed builds, ./cli.exe is for development builds. - &["bin/zed.exe", "./cli.exe"] - } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { - // bin is the standard, ./cli is for the target directory in development builds. - &["../bin/zed", "./cli"] - } else { - anyhow::bail!("unsupported platform for determining zed-cli path"); - }; + let possible_locations: &[&str] = if cfg!(target_os = "macos") { + // On macOS, the zed executable and zed-cli are inside the app bundle, + // so here ./cli is for both installed and development builds. + &["./cli"] + } else if cfg!(target_os = "windows") { + // bin/zed.exe is for installed builds, ./cli.exe is for development builds. + &["bin/zed.exe", "./cli.exe"] + } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { + // bin is the standard, ./cli is for the target directory in development builds. + &["../bin/zed", "./cli"] + } else { + anyhow::bail!("unsupported platform for determining zed-cli path"); + }; - possible_locations - .iter() - .find_map(|p| { - parent - .join(p) - .canonicalize() - .ok() - .filter(|p| p != &zed_path) - }) - .with_context(|| { - format!( - "could not find zed-cli from any of: {}", - possible_locations.join(", ") - ) - }) + possible_locations + .iter() + .find_map(|p| { + parent + .join(p) + .canonicalize() + .ok() + .filter(|p| p != &zed_path) + }) + .with_context(|| { + format!( + "could not find zed-cli from any of: {}", + possible_locations.join(", ") + ) + }) } #[cfg(unix)] pub async fn load_login_shell_environment() -> Result<()> { - use anyhow::Context as _; + use anyhow::Context as _; - load_shell_from_passwd().log_err(); + load_shell_from_passwd().log_err(); - // If possible, we want to `cd` in the user's `$HOME` to trigger programs - // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook - // into shell's `cd` command (and hooks) to manipulate env. - // We do this so that we get the env a user would have when spawning a shell - // in home directory. - for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()) - .await - .with_context(|| format!("capturing environment with {:?}", get_system_shell()))? - { - // Skip SHLVL to prevent it from polluting Zed's process environment. - // The login shell used for env capture increments SHLVL, and if we propagate it, - // terminals spawned by Zed will inherit it and increment again, causing SHLVL - // to start at 2 instead of 1 (and increase by 2 on each reload). - if name == "SHLVL" { - continue; - } - unsafe { std::env::set_var(&name, &value) }; - } + // If possible, we want to `cd` in the user's `$HOME` to trigger programs + // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook + // into shell's `cd` command (and hooks) to manipulate env. + // We do this so that we get the env a user would have when spawning a shell + // in home directory. + for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()) + .await + .with_context(|| format!("capturing environment with {:?}", get_system_shell()))? + { + // Skip SHLVL to prevent it from polluting Zed's process environment. + // The login shell used for env capture increments SHLVL, and if we propagate it, + // terminals spawned by Zed will inherit it and increment again, causing SHLVL + // to start at 2 instead of 1 (and increase by 2 on each reload). + if name == "SHLVL" { + continue; + } + unsafe { std::env::set_var(&name, &value) }; + } - log::info!( - "set environment variables from shell:{}, path:{}", - std::env::var("SHELL").unwrap_or_default(), - std::env::var("PATH").unwrap_or_default(), - ); + log::info!( + "set environment variables from shell:{}, path:{}", + std::env::var("SHELL").unwrap_or_default(), + std::env::var("PATH").unwrap_or_default(), + ); - Ok(()) + Ok(()) } /// Configures the process to start a new session, to prevent interactive shells from taking control @@ -385,261 +385,261 @@ pub async fn load_login_shell_environment() -> Result<()> { /// /// For more details: pub fn set_pre_exec_to_start_new_session( - command: &mut std::process::Command, + command: &mut std::process::Command, ) -> &mut std::process::Command { - // safety: code in pre_exec should be signal safe. - // https://man7.org/linux/man-pages/man7/signal-safety.7.html - #[cfg(unix)] - unsafe { - use std::os::unix::process::CommandExt; - command.pre_exec(|| { - libc::setsid(); - Ok(()) - }); - }; - command + // safety: code in pre_exec should be signal safe. + // https://man7.org/linux/man-pages/man7/signal-safety.7.html + #[cfg(unix)] + unsafe { + use std::os::unix::process::CommandExt; + command.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + }; + command } pub fn merge_json_lenient_value_into( - source: serde_json_lenient::Value, - target: &mut serde_json_lenient::Value, + source: serde_json_lenient::Value, + target: &mut serde_json_lenient::Value, ) { - match (source, target) { - (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => { - for (key, value) in source { - if let Some(target) = target.get_mut(&key) { - merge_json_lenient_value_into(value, target); - } else { - target.insert(key, value); - } - } - } + match (source, target) { + (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => { + for (key, value) in source { + if let Some(target) = target.get_mut(&key) { + merge_json_lenient_value_into(value, target); + } else { + target.insert(key, value); + } + } + } - (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => { - for value in source { - target.push(value); - } - } + (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => { + for value in source { + target.push(value); + } + } - (source, target) => *target = source, - } + (source, target) => *target = source, + } } pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { - use serde_json::Value; + use serde_json::Value; - match (source, target) { - (Value::Object(source), Value::Object(target)) => { - for (key, value) in source { - if let Some(target) = target.get_mut(&key) { - merge_json_value_into(value, target); - } else { - target.insert(key, value); - } - } - } + match (source, target) { + (Value::Object(source), Value::Object(target)) => { + for (key, value) in source { + if let Some(target) = target.get_mut(&key) { + merge_json_value_into(value, target); + } else { + target.insert(key, value); + } + } + } - (Value::Array(source), Value::Array(target)) => { - for value in source { - target.push(value); - } - } + (Value::Array(source), Value::Array(target)) => { + for value in source { + target.push(value); + } + } - (source, target) => *target = source, - } + (source, target) => *target = source, + } } pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { - use serde_json::Value; - if let Value::Object(source_object) = source { - let target_object = if let Value::Object(target) = target { - target - } else { - *target = Value::Object(Default::default()); - target.as_object_mut().unwrap() - }; - for (key, value) in source_object { - if let Some(target) = target_object.get_mut(&key) { - merge_non_null_json_value_into(value, target); - } else if !value.is_null() { - target_object.insert(key, value); - } - } - } else if !source.is_null() { - *target = source - } + use serde_json::Value; + if let Value::Object(source_object) = source { + let target_object = if let Value::Object(target) = target { + target + } else { + *target = Value::Object(Default::default()); + target.as_object_mut().unwrap() + }; + for (key, value) in source_object { + if let Some(target) = target_object.get_mut(&key) { + merge_non_null_json_value_into(value, target); + } else if !value.is_null() { + target_object.insert(key, value); + } + } + } else if !source.is_null() { + *target = source + } } pub fn expanded_and_wrapped_usize_range( - range: Range, - additional_before: usize, - additional_after: usize, - wrap_length: usize, + range: Range, + additional_before: usize, + additional_after: usize, + wrap_length: usize, ) -> impl Iterator { - let start_wraps = range.start < additional_before; - let end_wraps = wrap_length < range.end + additional_after; - if start_wraps && end_wraps { - Either::Left(0..wrap_length) - } else if start_wraps { - let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before); - if wrapped_start <= range.end { - Either::Left(0..wrap_length) - } else { - Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length)) - } - } else if end_wraps { - let wrapped_end = range.end + additional_after - wrap_length; - if range.start <= wrapped_end { - Either::Left(0..wrap_length) - } else { - Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length)) - } - } else { - Either::Left((range.start - additional_before)..(range.end + additional_after)) - } + let start_wraps = range.start < additional_before; + let end_wraps = wrap_length < range.end + additional_after; + if start_wraps && end_wraps { + Either::Left(0..wrap_length) + } else if start_wraps { + let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before); + if wrapped_start <= range.end { + Either::Left(0..wrap_length) + } else { + Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length)) + } + } else if end_wraps { + let wrapped_end = range.end + additional_after - wrap_length; + if range.start <= wrapped_end { + Either::Left(0..wrap_length) + } else { + Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length)) + } + } else { + Either::Left((range.start - additional_before)..(range.end + additional_after)) + } } /// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by /// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not /// emitted. If wrap_length is 0, nothing is yielded. pub fn wrapped_usize_outward_from( - start: usize, - additional_before: usize, - additional_after: usize, - wrap_length: usize, + start: usize, + additional_before: usize, + additional_after: usize, + wrap_length: usize, ) -> impl Iterator { - let mut count = 0; - let mut after_offset = 1; - let mut before_offset = 1; + let mut count = 0; + let mut after_offset = 1; + let mut before_offset = 1; - std::iter::from_fn(move || { - count += 1; - if count > wrap_length { - None - } else if count == 1 { - Some(start % wrap_length) - } else if after_offset <= additional_after && after_offset <= before_offset { - let value = (start + after_offset) % wrap_length; - after_offset += 1; - Some(value) - } else if before_offset <= additional_before { - let value = (start + wrap_length - before_offset) % wrap_length; - before_offset += 1; - Some(value) - } else if after_offset <= additional_after { - let value = (start + after_offset) % wrap_length; - after_offset += 1; - Some(value) - } else { - None - } - }) + std::iter::from_fn(move || { + count += 1; + if count > wrap_length { + None + } else if count == 1 { + Some(start % wrap_length) + } else if after_offset <= additional_after && after_offset <= before_offset { + let value = (start + after_offset) % wrap_length; + after_offset += 1; + Some(value) + } else if before_offset <= additional_before { + let value = (start + wrap_length - before_offset) % wrap_length; + before_offset += 1; + Some(value) + } else if after_offset <= additional_after { + let value = (start + after_offset) % wrap_length; + after_offset += 1; + Some(value) + } else { + None + } + }) } #[cfg(any(test, feature = "test-support"))] mod rng { - use rand::prelude::*; + use rand::prelude::*; - pub struct RandomCharIter { - rng: T, - simple_text: bool, - } + pub struct RandomCharIter { + rng: T, + simple_text: bool, + } - impl RandomCharIter { - pub fn new(rng: T) -> Self { - Self { - rng, - simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()), - } - } + impl RandomCharIter { + pub fn new(rng: T) -> Self { + Self { + rng, + simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()), + } + } - pub fn with_simple_text(mut self) -> Self { - self.simple_text = true; - self - } - } + pub fn with_simple_text(mut self) -> Self { + self.simple_text = true; + self + } + } - impl Iterator for RandomCharIter { - type Item = char; + impl Iterator for RandomCharIter { + type Item = char; - fn next(&mut self) -> Option { - if self.simple_text { - return if self.rng.random_range(0..100) < 5 { - Some('\n') - } else { - Some(self.rng.random_range(b'a'..b'z' + 1).into()) - }; - } + fn next(&mut self) -> Option { + if self.simple_text { + return if self.rng.random_range(0..100) < 5 { + Some('\n') + } else { + Some(self.rng.random_range(b'a'..b'z' + 1).into()) + }; + } - match self.rng.random_range(0..100) { - // whitespace - 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(), - // two-byte greek letters - 20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))), - // // three-byte characters - 33..=45 => ['✋', '✅', '❌', '❎', '⭐'] - .choose(&mut self.rng) - .copied(), - // // four-byte characters - 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(), - // ascii letters - _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()), - } - } - } + match self.rng.random_range(0..100) { + // whitespace + 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(), + // two-byte greek letters + 20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))), + // // three-byte characters + 33..=45 => ['✋', '✅', '❌', '❎', '⭐'] + .choose(&mut self.rng) + .copied(), + // // four-byte characters + 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(), + // ascii letters + _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()), + } + } + } } #[cfg(any(test, feature = "test-support"))] pub use rng::RandomCharIter; /// Get an embedded file as a string. pub fn asset_str(path: &str) -> Cow<'static, str> { - match A::get(path).expect(path).data { - Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), - Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), - } + match A::get(path).expect(path).data { + Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), + Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), + } } pub trait RangeExt { - fn sorted(&self) -> Self; - fn to_inclusive(&self) -> RangeInclusive; - fn overlaps(&self, other: &Range) -> bool; - fn contains_inclusive(&self, other: &Range) -> bool; + fn sorted(&self) -> Self; + fn to_inclusive(&self) -> RangeInclusive; + fn overlaps(&self, other: &Range) -> bool; + fn contains_inclusive(&self, other: &Range) -> bool; } impl RangeExt for Range { - fn sorted(&self) -> Self { - cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() - } + fn sorted(&self) -> Self { + cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() + } - fn to_inclusive(&self) -> RangeInclusive { - self.start.clone()..=self.end.clone() - } + fn to_inclusive(&self) -> RangeInclusive { + self.start.clone()..=self.end.clone() + } - fn overlaps(&self, other: &Range) -> bool { - self.start < other.end && other.start < self.end - } + fn overlaps(&self, other: &Range) -> bool { + self.start < other.end && other.start < self.end + } - fn contains_inclusive(&self, other: &Range) -> bool { - self.start <= other.start && other.end <= self.end - } + fn contains_inclusive(&self, other: &Range) -> bool { + self.start <= other.start && other.end <= self.end + } } impl RangeExt for RangeInclusive { - fn sorted(&self) -> Self { - cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone() - } + fn sorted(&self) -> Self { + cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone() + } - fn to_inclusive(&self) -> RangeInclusive { - self.clone() - } + fn to_inclusive(&self) -> RangeInclusive { + self.clone() + } - fn overlaps(&self, other: &Range) -> bool { - self.start() < &other.end && &other.start <= self.end() - } + fn overlaps(&self, other: &Range) -> bool { + self.start() < &other.end && &other.start <= self.end() + } - fn contains_inclusive(&self, other: &Range) -> bool { - self.start() <= &other.start && &other.end <= self.end() - } + fn contains_inclusive(&self, other: &Range) -> bool { + self.start() <= &other.start && &other.end <= self.end() + } } /// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one, @@ -651,424 +651,424 @@ impl RangeExt for RangeInclusive { pub struct NumericPrefixWithSuffix<'a>(Option, &'a str); impl<'a> NumericPrefixWithSuffix<'a> { - pub fn from_numeric_prefixed_str(str: &'a str) -> Self { - let i = str.chars().take_while(|c| c.is_ascii_digit()).count(); - let (prefix, remainder) = str.split_at(i); + pub fn from_numeric_prefixed_str(str: &'a str) -> Self { + let i = str.chars().take_while(|c| c.is_ascii_digit()).count(); + let (prefix, remainder) = str.split_at(i); - let prefix = prefix.parse().ok(); - Self(prefix, remainder) - } + let prefix = prefix.parse().ok(); + Self(prefix, remainder) + } } /// When dealing with equality, we need to consider the case of the strings to achieve strict equality /// to handle cases like "a" < "A" instead of "a" == "A". impl Ord for NumericPrefixWithSuffix<'_> { - fn cmp(&self, other: &Self) -> Ordering { - match (self.0, other.0) { - (None, None) => UniCase::new(self.1) - .cmp(&UniCase::new(other.1)) - .then_with(|| self.1.cmp(other.1).reverse()), - (None, Some(_)) => Ordering::Greater, - (Some(_), None) => Ordering::Less, - (Some(a), Some(b)) => a.cmp(&b).then_with(|| { - UniCase::new(self.1) - .cmp(&UniCase::new(other.1)) - .then_with(|| self.1.cmp(other.1).reverse()) - }), - } - } + fn cmp(&self, other: &Self) -> Ordering { + match (self.0, other.0) { + (None, None) => UniCase::new(self.1) + .cmp(&UniCase::new(other.1)) + .then_with(|| self.1.cmp(other.1).reverse()), + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + (Some(a), Some(b)) => a.cmp(&b).then_with(|| { + UniCase::new(self.1) + .cmp(&UniCase::new(other.1)) + .then_with(|| self.1.cmp(other.1).reverse()) + }), + } + } } impl PartialOrd for NumericPrefixWithSuffix<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } fn emoji_regex() -> &'static Regex { - static EMOJI_REGEX: LazyLock = - LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap()); - &EMOJI_REGEX + static EMOJI_REGEX: LazyLock = + LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap()); + &EMOJI_REGEX } /// Returns true if the given string consists of emojis only. /// E.g. "👨‍👩‍👧‍👧👋" will return true, but "👋!" will return false. pub fn word_consists_of_emojis(s: &str) -> bool { - let mut prev_end = 0; - for capture in emoji_regex().find_iter(s) { - if capture.start() != prev_end { - return false; - } - prev_end = capture.end(); - } - prev_end == s.len() + let mut prev_end = 0; + for capture in emoji_regex().find_iter(s) { + if capture.start() != prev_end { + return false; + } + prev_end = capture.end(); + } + prev_end == s.len() } /// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike /// `str::split`, this is not generic on pattern types and does not return an `Iterator`. pub fn split_str_with_ranges<'s>( - s: &'s str, - pat: &dyn Fn(char) -> bool, + s: &'s str, + pat: &dyn Fn(char) -> bool, ) -> Vec<(Range, &'s str)> { - let mut result = Vec::new(); - let mut start = 0; + let mut result = Vec::new(); + let mut start = 0; - for (i, ch) in s.char_indices() { - if pat(ch) { - if i > start { - result.push((start..i, &s[start..i])); - } - start = i + ch.len_utf8(); - } - } + for (i, ch) in s.char_indices() { + if pat(ch) { + if i > start { + result.push((start..i, &s[start..i])); + } + start = i + ch.len_utf8(); + } + } - if s.len() > start { - result.push((start..s.len(), &s[start..s.len()])); - } + if s.len() > start { + result.push((start..s.len(), &s[start..s.len()])); + } - result + result } pub fn default() -> D { - Default::default() + Default::default() } #[derive(Debug)] pub enum ConnectionResult { - Timeout, - ConnectionReset, - Result(anyhow::Result), + Timeout, + ConnectionReset, + Result(anyhow::Result), } impl ConnectionResult { - pub fn into_response(self) -> anyhow::Result { - match self { - ConnectionResult::Timeout => anyhow::bail!("Request timed out"), - ConnectionResult::ConnectionReset => anyhow::bail!("Server reset the connection"), - ConnectionResult::Result(r) => r, - } - } + pub fn into_response(self) -> anyhow::Result { + match self { + ConnectionResult::Timeout => anyhow::bail!("Request timed out"), + ConnectionResult::ConnectionReset => anyhow::bail!("Server reset the connection"), + ConnectionResult::Result(r) => r, + } + } } impl From> for ConnectionResult { - fn from(result: anyhow::Result) -> Self { - ConnectionResult::Result(result) - } + fn from(result: anyhow::Result) -> Self { + ConnectionResult::Result(result) + } } /// Normalizes a path by resolving `.` and `..` components without /// requiring the path to exist on disk (unlike `canonicalize`). pub fn normalize_path(path: &Path) -> PathBuf { - use std::path::Component; - let mut components = path.components().peekable(); - let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; + use std::path::Component; + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - ret.push(c); - } - } - } - ret + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_extend_sorted() { - let mut vec = vec![]; + #[test] + fn test_extend_sorted() { + let mut vec = vec![]; - extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a)); - assert_eq!(vec, &[21, 17, 13, 8, 1]); + extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a)); + assert_eq!(vec, &[21, 17, 13, 8, 1]); - extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a)); - assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]); + extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a)); + assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]); - extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a)); - assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]); - } + extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a)); + assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]); + } - #[test] - fn test_truncate_to_bottom_n_sorted_by() { - let mut vec: Vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 10, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4, 5]); + #[test] + fn test_truncate_to_bottom_n_sorted_by() { + let mut vec: Vec = vec![5, 2, 3, 4, 1]; + truncate_to_bottom_n_sorted_by(&mut vec, 10, &u32::cmp); + assert_eq!(vec, &[1, 2, 3, 4, 5]); - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 5, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4, 5]); + vec = vec![5, 2, 3, 4, 1]; + truncate_to_bottom_n_sorted_by(&mut vec, 5, &u32::cmp); + assert_eq!(vec, &[1, 2, 3, 4, 5]); - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 4, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4]); + vec = vec![5, 2, 3, 4, 1]; + truncate_to_bottom_n_sorted_by(&mut vec, 4, &u32::cmp); + assert_eq!(vec, &[1, 2, 3, 4]); - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 1, &u32::cmp); - assert_eq!(vec, &[1]); + vec = vec![5, 2, 3, 4, 1]; + truncate_to_bottom_n_sorted_by(&mut vec, 1, &u32::cmp); + assert_eq!(vec, &[1]); - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 0, &u32::cmp); - assert!(vec.is_empty()); - } + vec = vec![5, 2, 3, 4, 1]; + truncate_to_bottom_n_sorted_by(&mut vec, 0, &u32::cmp); + assert!(vec.is_empty()); + } - #[test] - fn test_iife() { - fn option_returning_function() -> Option<()> { - None - } + #[test] + fn test_iife() { + fn option_returning_function() -> Option<()> { + None + } - let foo = maybe!({ - option_returning_function()?; - Some(()) - }); + let foo = maybe!({ + option_returning_function()?; + Some(()) + }); - assert_eq!(foo, None); - } + assert_eq!(foo, None); + } - #[test] - fn test_truncate_and_trailoff() { - assert_eq!(truncate_and_trailoff("", 5), ""); - assert_eq!(truncate_and_trailoff("aaaaaa", 7), "aaaaaa"); - assert_eq!(truncate_and_trailoff("aaaaaa", 6), "aaaaaa"); - assert_eq!(truncate_and_trailoff("aaaaaa", 5), "aaaaa…"); - assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè"); - assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); - assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); - } + #[test] + fn test_truncate_and_trailoff() { + assert_eq!(truncate_and_trailoff("", 5), ""); + assert_eq!(truncate_and_trailoff("aaaaaa", 7), "aaaaaa"); + assert_eq!(truncate_and_trailoff("aaaaaa", 6), "aaaaaa"); + assert_eq!(truncate_and_trailoff("aaaaaa", 5), "aaaaa…"); + assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); + } - #[test] - fn test_truncate_and_remove_front() { - assert_eq!(truncate_and_remove_front("", 5), ""); - assert_eq!(truncate_and_remove_front("aaaaaa", 7), "aaaaaa"); - assert_eq!(truncate_and_remove_front("aaaaaa", 6), "aaaaaa"); - assert_eq!(truncate_and_remove_front("aaaaaa", 5), "…aaaaa"); - assert_eq!(truncate_and_remove_front("èèèèèè", 7), "èèèèèè"); - assert_eq!(truncate_and_remove_front("èèèèèè", 6), "èèèèèè"); - assert_eq!(truncate_and_remove_front("èèèèèè", 5), "…èèèèè"); - } + #[test] + fn test_truncate_and_remove_front() { + assert_eq!(truncate_and_remove_front("", 5), ""); + assert_eq!(truncate_and_remove_front("aaaaaa", 7), "aaaaaa"); + assert_eq!(truncate_and_remove_front("aaaaaa", 6), "aaaaaa"); + assert_eq!(truncate_and_remove_front("aaaaaa", 5), "…aaaaa"); + assert_eq!(truncate_and_remove_front("èèèèèè", 7), "èèèèèè"); + assert_eq!(truncate_and_remove_front("èèèèèè", 6), "èèèèèè"); + assert_eq!(truncate_and_remove_front("èèèèèè", 5), "…èèèèè"); + } - #[test] - fn test_numeric_prefix_str_method() { - let target = "1a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), "a") - ); + #[test] + fn test_numeric_prefix_str_method() { + let target = "1a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(1), "a") + ); - let target = "12ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "ab") - ); + let target = "12ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(12), "ab") + ); - let target = "12_ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "_ab") - ); + let target = "12_ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(12), "_ab") + ); - let target = "1_2ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), "_2ab") - ); + let target = "1_2ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(1), "_2ab") + ); - let target = "1.2"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), ".2") - ); + let target = "1.2"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(1), ".2") + ); - let target = "1.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), ".2_a") - ); + let target = "1.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(1), ".2_a") + ); - let target = "12.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), ".2_a") - ); + let target = "12.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(12), ".2_a") + ); - let target = "12a.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "a.2_a") - ); - } + let target = "12a.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + NumericPrefixWithSuffix(Some(12), "a.2_a") + ); + } - #[test] - fn test_numeric_prefix_with_suffix() { - let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; - sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s)); - assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); + #[test] + fn test_numeric_prefix_with_suffix() { + let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; + sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s)); + assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); - for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), - NumericPrefixWithSuffix(None, numeric_prefix_less), - "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" - ) - } - } + for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), + NumericPrefixWithSuffix(None, numeric_prefix_less), + "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" + ) + } + } - #[test] - fn test_word_consists_of_emojis() { - let words_to_test = vec![ - ("👨‍👩‍👧‍👧👋🥒", true), - ("👋", true), - ("!👋", false), - ("👋!", false), - ("👋 ", false), - (" 👋", false), - ("Test", false), - ]; + #[test] + fn test_word_consists_of_emojis() { + let words_to_test = vec![ + ("👨‍👩‍👧‍👧👋🥒", true), + ("👋", true), + ("!👋", false), + ("👋!", false), + ("👋 ", false), + (" 👋", false), + ("Test", false), + ]; - for (text, expected_result) in words_to_test { - assert_eq!(word_consists_of_emojis(text), expected_result); - } - } + for (text, expected_result) in words_to_test { + assert_eq!(word_consists_of_emojis(text), expected_result); + } + } - #[test] - fn test_truncate_lines_and_trailoff() { - let text = r#"Line 1 + #[test] + fn test_truncate_lines_and_trailoff() { + let text = r#"Line 1 Line 2 Line 3"#; - assert_eq!( - truncate_lines_and_trailoff(text, 2), - r#"Line 1 + assert_eq!( + truncate_lines_and_trailoff(text, 2), + r#"Line 1 …"# - ); + ); - assert_eq!( - truncate_lines_and_trailoff(text, 3), - r#"Line 1 + assert_eq!( + truncate_lines_and_trailoff(text, 3), + r#"Line 1 Line 2 …"# - ); + ); - assert_eq!( - truncate_lines_and_trailoff(text, 4), - r#"Line 1 + assert_eq!( + truncate_lines_and_trailoff(text, 4), + r#"Line 1 Line 2 Line 3"# - ); - } + ); + } - #[test] - fn test_expanded_and_wrapped_usize_range() { - // Neither wrap - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::>(), - (1..5).collect::>() - ); - // Start wraps - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::>(), - ((0..5).chain(7..8)).collect::>() - ); - // Start wraps all the way around - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::>(), - (0..8).collect::>() - ); - // Start wraps all the way around and past 0 - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::>(), - (0..8).collect::>() - ); - // End wraps - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::>(), - (0..1).chain(2..8).collect::>() - ); - // End wraps all the way around - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::>(), - (0..8).collect::>() - ); - // End wraps all the way around and past the end - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::>(), - (0..8).collect::>() - ); - // Both start and end wrap - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::>(), - (0..8).collect::>() - ); - } + #[test] + fn test_expanded_and_wrapped_usize_range() { + // Neither wrap + assert_eq!( + expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::>(), + (1..5).collect::>() + ); + // Start wraps + assert_eq!( + expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::>(), + ((0..5).chain(7..8)).collect::>() + ); + // Start wraps all the way around + assert_eq!( + expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::>(), + (0..8).collect::>() + ); + // Start wraps all the way around and past 0 + assert_eq!( + expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::>(), + (0..8).collect::>() + ); + // End wraps + assert_eq!( + expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::>(), + (0..1).chain(2..8).collect::>() + ); + // End wraps all the way around + assert_eq!( + expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::>(), + (0..8).collect::>() + ); + // End wraps all the way around and past the end + assert_eq!( + expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::>(), + (0..8).collect::>() + ); + // Both start and end wrap + assert_eq!( + expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::>(), + (0..8).collect::>() + ); + } - #[test] - fn test_wrapped_usize_outward_from() { - // No wrapping - assert_eq!( - wrapped_usize_outward_from(4, 2, 2, 10).collect::>(), - vec![4, 5, 3, 6, 2] - ); - // Wrapping at end - assert_eq!( - wrapped_usize_outward_from(8, 2, 3, 10).collect::>(), - vec![8, 9, 7, 0, 6, 1] - ); - // Wrapping at start - assert_eq!( - wrapped_usize_outward_from(1, 3, 2, 10).collect::>(), - vec![1, 2, 0, 3, 9, 8] - ); - // All values wrap around - assert_eq!( - wrapped_usize_outward_from(5, 10, 10, 8).collect::>(), - vec![5, 6, 4, 7, 3, 0, 2, 1] - ); - // None before / after - assert_eq!( - wrapped_usize_outward_from(3, 0, 0, 8).collect::>(), - vec![3] - ); - // Starting point already wrapped - assert_eq!( - wrapped_usize_outward_from(15, 2, 2, 10).collect::>(), - vec![5, 6, 4, 7, 3] - ); - // wrap_length of 0 - assert_eq!( - wrapped_usize_outward_from(4, 2, 2, 0).collect::>(), - Vec::::new() - ); - } + #[test] + fn test_wrapped_usize_outward_from() { + // No wrapping + assert_eq!( + wrapped_usize_outward_from(4, 2, 2, 10).collect::>(), + vec![4, 5, 3, 6, 2] + ); + // Wrapping at end + assert_eq!( + wrapped_usize_outward_from(8, 2, 3, 10).collect::>(), + vec![8, 9, 7, 0, 6, 1] + ); + // Wrapping at start + assert_eq!( + wrapped_usize_outward_from(1, 3, 2, 10).collect::>(), + vec![1, 2, 0, 3, 9, 8] + ); + // All values wrap around + assert_eq!( + wrapped_usize_outward_from(5, 10, 10, 8).collect::>(), + vec![5, 6, 4, 7, 3, 0, 2, 1] + ); + // None before / after + assert_eq!( + wrapped_usize_outward_from(3, 0, 0, 8).collect::>(), + vec![3] + ); + // Starting point already wrapped + assert_eq!( + wrapped_usize_outward_from(15, 2, 2, 10).collect::>(), + vec![5, 6, 4, 7, 3] + ); + // wrap_length of 0 + assert_eq!( + wrapped_usize_outward_from(4, 2, 2, 0).collect::>(), + Vec::::new() + ); + } - #[test] - fn test_split_with_ranges() { - let input = "hi"; - let result = split_str_with_ranges(input, &|c| c == ' '); + #[test] + fn test_split_with_ranges() { + let input = "hi"; + let result = split_str_with_ranges(input, &|c| c == ' '); - assert_eq!(result.len(), 1); - assert_eq!(result[0], (0..2, "hi")); + assert_eq!(result.len(), 1); + assert_eq!(result[0], (0..2, "hi")); - let input = "héllo🦀world"; - let result = split_str_with_ranges(input, &|c| c == '🦀'); + let input = "héllo🦀world"; + let result = split_str_with_ranges(input, &|c| c == '🦀'); - assert_eq!(result.len(), 2); - assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes - assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes - } + assert_eq!(result.len(), 2); + assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes + assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes + } } diff --git a/crates/oak_bridge/examples/surface_bridge.rs b/crates/oak_bridge/examples/surface_bridge.rs index 855a51a593..b7ffaa9029 100644 --- a/crates/oak_bridge/examples/surface_bridge.rs +++ b/crates/oak_bridge/examples/surface_bridge.rs @@ -8,7 +8,10 @@ #![cfg(target_os = "macos")] use core_video::pixel_buffer::CVPixelBuffer; -use gpui::{App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, size, surface}; +use gpui::{ + App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, size, + surface, +}; use oak_bridge::surface::{SurfaceBridge, SurfaceFormat}; use std::sync::Arc; @@ -16,177 +19,188 @@ const WIDTH: u32 = 1280; const HEIGHT: u32 = 720; struct Demo { - queue: wgpu::Queue, - bridge: SurfaceBridge, - src_texture: wgpu::Texture, - frame: u64, - pixel_buffer: Option, - frames: u64, - fps_clock: std::time::Instant, + queue: wgpu::Queue, + bridge: SurfaceBridge, + src_texture: wgpu::Texture, + frame: u64, + pixel_buffer: Option, + frames: u64, + fps_clock: std::time::Instant, } impl Demo { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::METAL, - flags: wgpu::InstanceFlags::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - backend_options: wgpu::BackendOptions::default(), - display: None, - }); - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - ..Default::default() - })) - .expect("no Metal adapter available (CI environments skip this demo)"); - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("oak-bridge-demo"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default(), - experimental_features: wgpu::ExperimentalFeatures::default(), - memory_hints: wgpu::MemoryHints::default(), - trace: wgpu::Trace::Off, - }, - )) - .expect("failed to create wgpu device"); - let device = Arc::new(device); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::METAL, + flags: wgpu::InstanceFlags::default(), + memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + backend_options: wgpu::BackendOptions::default(), + display: None, + }); + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + ..Default::default() + })) + .expect("no Metal adapter available (CI environments skip this demo)"); + let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: Some("oak-bridge-demo"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + experimental_features: wgpu::ExperimentalFeatures::default(), + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::Off, + })) + .expect("failed to create wgpu device"); + let device = Arc::new(device); - // wgpu's Metal backend uses the system default device unless told - // otherwise, so the demo reuses it for the bridge. - let metal_device = metal::Device::system_default().expect("no Metal device"); - let bridge = SurfaceBridge::new(&metal_device, device.clone(), WIDTH, HEIGHT, SurfaceFormat::Bgra8Unorm) - .expect("failed to create surface bridge"); + // wgpu's Metal backend uses the system default device unless told + // otherwise, so the demo reuses it for the bridge. + let metal_device = metal::Device::system_default().expect("no Metal device"); + let bridge = SurfaceBridge::new( + &metal_device, + device.clone(), + WIDTH, + HEIGHT, + SurfaceFormat::Bgra8Unorm, + ) + .expect("failed to create surface bridge"); - let src_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("demo-source"), - size: wgpu::Extent3d { - width: WIDTH, - height: HEIGHT, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: SurfaceFormat::Bgra8Unorm.wgpu(), - usage: wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); + let src_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("demo-source"), + size: wgpu::Extent3d { + width: WIDTH, + height: HEIGHT, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: SurfaceFormat::Bgra8Unorm.wgpu(), + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); - // Kick off the frame loop on the main thread. - let this = cx.weak_entity(); - window.spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { - loop { - cx.background_executor() - .timer(std::time::Duration::from_millis(16)) - .await; - let _ = cx.update(|_window, app| { - if let Some(this) = this.upgrade() { - this.update(app, |this, cx| this.tick(cx)); - } - }); - } - }) - .detach(); - Self { - queue, - bridge, - src_texture, - frame: 0, - pixel_buffer: None, - frames: 0, - fps_clock: std::time::Instant::now(), - } - } + // Kick off the frame loop on the main thread. + let this = cx.weak_entity(); + window + .spawn(cx, async move |cx: &mut gpui::AsyncWindowContext| { + loop { + cx.background_executor() + .timer(std::time::Duration::from_millis(16)) + .await; + let _ = cx.update(|_window, app| { + if let Some(this) = this.upgrade() { + this.update(app, |this, cx| this.tick(cx)); + } + }); + } + }) + .detach(); + Self { + queue, + bridge, + src_texture, + frame: 0, + pixel_buffer: None, + frames: 0, + fps_clock: std::time::Instant::now(), + } + } - fn tick(&mut self, cx: &mut Context) { - // Generate a moving color-bar + gradient test pattern on the CPU and - // upload it (in the real engine this texture comes from a render pass). - let mut bytes = vec![0u8; (WIDTH * HEIGHT * 4) as usize]; - for y in 0..HEIGHT { - for x in 0..WIDTH { - let index = ((y * WIDTH + x) * 4) as usize; - let t = self.frame as f32 / 60.0; - let hue = (x as f32 / WIDTH as f32 + t).fract(); - let stripe = if (x / 128) % 2 == 0 { 1.0 } else { 0.7 }; - let fade = (y as f32 / HEIGHT as f32) * 0.6 + 0.2; - // BGR(A) byte order. - bytes[index] = (255.0 * stripe * fade * (1.0 - hue)).round() as u8; - bytes[index + 1] = (255.0 * stripe * fade * hue).round() as u8; - bytes[index + 2] = (255.0 * stripe * fade * (0.5 + 0.5 * (t * 2.0).sin())).round() as u8; - bytes[index + 3] = 255; - } - } - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &self.src_texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &bytes, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(WIDTH * 4), - rows_per_image: None, - }, - self.src_texture.size(), - ); + fn tick(&mut self, cx: &mut Context) { + // Generate a moving color-bar + gradient test pattern on the CPU and + // upload it (in the real engine this texture comes from a render pass). + let mut bytes = vec![0u8; (WIDTH * HEIGHT * 4) as usize]; + for y in 0..HEIGHT { + for x in 0..WIDTH { + let index = ((y * WIDTH + x) * 4) as usize; + let t = self.frame as f32 / 60.0; + let hue = (x as f32 / WIDTH as f32 + t).fract(); + let stripe = if (x / 128) % 2 == 0 { 1.0 } else { 0.7 }; + let fade = (y as f32 / HEIGHT as f32) * 0.6 + 0.2; + // BGR(A) byte order. + bytes[index] = (255.0 * stripe * fade * (1.0 - hue)).round() as u8; + bytes[index + 1] = (255.0 * stripe * fade * hue).round() as u8; + bytes[index + 2] = + (255.0 * stripe * fade * (0.5 + 0.5 * (t * 2.0).sin())).round() as u8; + bytes[index + 3] = 255; + } + } + self.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.src_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &bytes, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(WIDTH * 4), + rows_per_image: None, + }, + self.src_texture.size(), + ); - // Bridge to an IOSurface-backed CVPixelBuffer (GPU-to-GPU). - match self.bridge.blit_frame(&self.src_texture) { - Ok(pixel_buffer) => { - self.pixel_buffer = Some(pixel_buffer); - } - Err(error) => { - eprintln!("blit failed: {error}"); - } - } - self.frame += 1; - self.frames += 1; + // Bridge to an IOSurface-backed CVPixelBuffer (GPU-to-GPU). + match self.bridge.blit_frame(&self.src_texture) { + Ok(pixel_buffer) => { + self.pixel_buffer = Some(pixel_buffer); + } + Err(error) => { + eprintln!("blit failed: {error}"); + } + } + self.frame += 1; + self.frames += 1; - // FPS meter, once per second. - if self.fps_clock.elapsed() >= std::time::Duration::from_secs(1) { - println!("{:.0} fps ({}x{})", self.frames as f64 / self.fps_clock.elapsed().as_secs_f64(), WIDTH, HEIGHT); - self.frames = 0; - self.fps_clock = std::time::Instant::now(); - } + // FPS meter, once per second. + if self.fps_clock.elapsed() >= std::time::Duration::from_secs(1) { + println!( + "{:.0} fps ({}x{})", + self.frames as f64 / self.fps_clock.elapsed().as_secs_f64(), + WIDTH, + HEIGHT + ); + self.frames = 0; + self.fps_clock = std::time::Instant::now(); + } - cx.notify(); - } + cx.notify(); + } } impl Render for Demo { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - if let Some(pixel_buffer) = self.pixel_buffer.clone() { - div().size_full().child(surface(pixel_buffer)) - } else { - div().size_full().child("waiting for first frame…") - } - } + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + if let Some(pixel_buffer) = self.pixel_buffer.clone() { + div().size_full().child(surface(pixel_buffer)) + } else { + div().size_full().child("waiting for first frame…") + } + } } fn main() { - gpui_platform::application().run(|cx: &mut App| { - cx.init_colors(); - let bounds = Bounds::centered(None, size(px(960.0), px(540.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Demo::new(window, cx)), - ) - .expect("Failed to open window"); + gpui_platform::application().run(|cx: &mut App| { + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(960.0), px(540.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Demo::new(window, cx)), + ) + .expect("Failed to open window"); - cx.activate(true); - cx.on_window_closed(|cx, _| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - }); + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); } diff --git a/crates/oak_bridge/src/surface.rs b/crates/oak_bridge/src/surface.rs index fa22d1e70e..22fc335d10 100644 --- a/crates/oak_bridge/src/surface.rs +++ b/crates/oak_bridge/src/surface.rs @@ -9,12 +9,12 @@ use core_foundation::dictionary::CFDictionary; use core_foundation::string::CFString; use core_video::image_buffer::CVImageBufferRef; use core_video::pixel_buffer::{ - CVPixelBuffer, kCVPixelBufferIOSurfacePropertiesKey, kCVPixelBufferMetalCompatibilityKey, + CVPixelBuffer, kCVPixelBufferIOSurfacePropertiesKey, kCVPixelBufferMetalCompatibilityKey, }; use media::core_video::CVMetalTextureCache; use metal::{ - CommandQueue, MTLDevice, MTLOrigin, MTLPixelFormat, MTLSize, TextureRef, - foreign_types::{ForeignType, ForeignTypeRef}, + CommandQueue, MTLDevice, MTLOrigin, MTLPixelFormat, MTLSize, TextureRef, + foreign_types::{ForeignType, ForeignTypeRef}, }; use std::ffi::c_void; use std::sync::Arc; @@ -22,38 +22,38 @@ use std::sync::Arc; /// The pixel formats the bridge can carry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SurfaceFormat { - /// 8-bit BGRA (the common video format). - Bgra8Unorm, - /// 16-bit float RGBA (HDR frames). - Rgba16Float, + /// 8-bit BGRA (the common video format). + Bgra8Unorm, + /// 16-bit float RGBA (HDR frames). + Rgba16Float, } impl SurfaceFormat { - /// The wgpu format for the engine's texture. - pub fn wgpu(self) -> wgpu::TextureFormat { - match self { - SurfaceFormat::Bgra8Unorm => wgpu::TextureFormat::Bgra8Unorm, - SurfaceFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float, - } - } + /// The wgpu format for the engine's texture. + pub fn wgpu(self) -> wgpu::TextureFormat { + match self { + SurfaceFormat::Bgra8Unorm => wgpu::TextureFormat::Bgra8Unorm, + SurfaceFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float, + } + } - /// The Metal format used when aliasing the CVPixelBuffer. - fn metal(self) -> MTLPixelFormat { - match self { - SurfaceFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm, - SurfaceFormat::Rgba16Float => MTLPixelFormat::RGBA16Float, - } - } + /// The Metal format used when aliasing the CVPixelBuffer. + fn metal(self) -> MTLPixelFormat { + match self { + SurfaceFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm, + SurfaceFormat::Rgba16Float => MTLPixelFormat::RGBA16Float, + } + } - /// The CoreVideo pixel format (`OSType`). - fn ostype(self) -> u32 { - match self { - // kCVPixelFormatType_32BGRA - SurfaceFormat::Bgra8Unorm => 0x42475241, - // kCVPixelFormatType_64RGBAHalf - SurfaceFormat::Rgba16Float => 0x000000b4, - } - } + /// The CoreVideo pixel format (`OSType`). + fn ostype(self) -> u32 { + match self { + // kCVPixelFormatType_32BGRA + SurfaceFormat::Bgra8Unorm => 0x42475241, + // kCVPixelFormatType_64RGBAHalf + SurfaceFormat::Rgba16Float => 0x000000b4, + } + } } /// Bridges engine wgpu textures to IOSurface-backed `CVPixelBuffer`s. @@ -62,321 +62,321 @@ impl SurfaceFormat { /// IOSurface-backed pixel buffer and hands it back for `window.paint_surface`. /// `readback_frame` is the any-backend CPU fallback. pub struct SurfaceBridge { - texture_cache: CVMetalTextureCache, - command_queue: CommandQueue, - wgpu_device: Arc, - width: u32, - height: u32, - format: SurfaceFormat, - /// The reused IOSurface-backed pixel buffer. - pixel_buffer: Option, - /// The pixel buffer's Metal alias, kept alive for the blit. - target: Option, - /// A CPU copy for the readback path. - cpu_bytes: Option>, - /// The readback staging buffer between [`Self::stage_readback`] and - /// [`Self::finish_readback`]. - staging_buffer: Option, - readback_bytes_per_row: u32, + texture_cache: CVMetalTextureCache, + command_queue: CommandQueue, + wgpu_device: Arc, + width: u32, + height: u32, + format: SurfaceFormat, + /// The reused IOSurface-backed pixel buffer. + pixel_buffer: Option, + /// The pixel buffer's Metal alias, kept alive for the blit. + target: Option, + /// A CPU copy for the readback path. + cpu_bytes: Option>, + /// The readback staging buffer between [`Self::stage_readback`] and + /// [`Self::finish_readback`]. + staging_buffer: Option, + readback_bytes_per_row: u32, } impl SurfaceBridge { - /// Create a bridge on the given Metal device and wgpu device. - /// - /// `metal_device` must be the same device `wgpu_device` was created on. - pub fn new( - metal_device: &metal::Device, - wgpu_device: Arc, - width: u32, - height: u32, - format: SurfaceFormat, - ) -> Result { - ensure!(width > 0 && height > 0, "surface must have positive size"); - let texture_cache = - unsafe { CVMetalTextureCache::new(metal_device.as_ptr() as *mut MTLDevice) }?; - let command_queue = metal_device.new_command_queue(); - Ok(Self { - texture_cache, - command_queue, - wgpu_device, - width, - height, - format, - pixel_buffer: None, - target: None, - cpu_bytes: None, - staging_buffer: None, - readback_bytes_per_row: 0, - }) - } + /// Create a bridge on the given Metal device and wgpu device. + /// + /// `metal_device` must be the same device `wgpu_device` was created on. + pub fn new( + metal_device: &metal::Device, + wgpu_device: Arc, + width: u32, + height: u32, + format: SurfaceFormat, + ) -> Result { + ensure!(width > 0 && height > 0, "surface must have positive size"); + let texture_cache = + unsafe { CVMetalTextureCache::new(metal_device.as_ptr() as *mut MTLDevice) }?; + let command_queue = metal_device.new_command_queue(); + Ok(Self { + texture_cache, + command_queue, + wgpu_device, + width, + height, + format, + pixel_buffer: None, + target: None, + cpu_bytes: None, + staging_buffer: None, + readback_bytes_per_row: 0, + }) + } - /// The frame size. - pub fn size(&self) -> (u32, u32) { - (self.width, self.height) - } + /// The frame size. + pub fn size(&self) -> (u32, u32) { + (self.width, self.height) + } - /// Create (or reuse) the IOSurface-backed pixel buffer and its Metal - /// alias, so `blit_frame` can copy into it. - fn ensure_target(&mut self) -> Result<(CVPixelBuffer, &TextureRef)> { - if let (Some(pb), Some(_)) = (&self.pixel_buffer, &self.target) { - return Ok((pb.clone(), self.target.as_ref().unwrap().as_texture_ref())); - } + /// Create (or reuse) the IOSurface-backed pixel buffer and its Metal + /// alias, so `blit_frame` can copy into it. + fn ensure_target(&mut self) -> Result<(CVPixelBuffer, &TextureRef)> { + if let (Some(pb), Some(_)) = (&self.pixel_buffer, &self.target) { + return Ok((pb.clone(), self.target.as_ref().unwrap().as_texture_ref())); + } - let pixel_buffer = create_iosurface_pixel_buffer(self.width, self.height, self.format)?; + let pixel_buffer = create_iosurface_pixel_buffer(self.width, self.height, self.format)?; - let metal_texture = unsafe { - self.texture_cache.create_texture_from_image( - pixel_buffer.as_concrete_TypeRef() as CVImageBufferRef, - std::ptr::null(), - self.format.metal(), - self.width as usize, - self.height as usize, - 0, - )? - }; - self.pixel_buffer = Some(pixel_buffer.clone()); - self.target = Some(metal_texture); - Ok((pixel_buffer, self.target.as_ref().unwrap().as_texture_ref())) - } + let metal_texture = unsafe { + self.texture_cache.create_texture_from_image( + pixel_buffer.as_concrete_TypeRef() as CVImageBufferRef, + std::ptr::null(), + self.format.metal(), + self.width as usize, + self.height as usize, + 0, + )? + }; + self.pixel_buffer = Some(pixel_buffer.clone()); + self.target = Some(metal_texture); + Ok((pixel_buffer, self.target.as_ref().unwrap().as_texture_ref())) + } - /// Copy `src` into the IOSurface-backed pixel buffer (GPU-to-GPU, no CPU - /// round trip) and return the buffer for display. - /// - /// The engine must render `src` on the same device this bridge was - /// created with, with the same format and size, and must have submitted - /// its work: this method waits for all of the wgpu device's submitted - /// work to finish before issuing the Metal blit (synchronous but - /// correct; the CPU readback is the fallback if this is too slow). - pub fn blit_frame(&mut self, src: &wgpu::Texture) -> Result { - ensure!( - src.size() == wgpu::Extent3d { - width: self.width, - height: self.height, - depth_or_array_layers: 1, - } && src.format() == self.format.wgpu(), - "engine texture must match the bridge format and size" - ); - // Wait for the engine's render pass (submitted by the host on the - // wgpu queue) to finish before reading it from our own queue. - let _ = self.wgpu_device.poll(wgpu::PollType::Wait { - submission_index: None, - timeout: None, - }); + /// Copy `src` into the IOSurface-backed pixel buffer (GPU-to-GPU, no CPU + /// round trip) and return the buffer for display. + /// + /// The engine must render `src` on the same device this bridge was + /// created with, with the same format and size, and must have submitted + /// its work: this method waits for all of the wgpu device's submitted + /// work to finish before issuing the Metal blit (synchronous but + /// correct; the CPU readback is the fallback if this is too slow). + pub fn blit_frame(&mut self, src: &wgpu::Texture) -> Result { + ensure!( + src.size() + == wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + } && src.format() == self.format.wgpu(), + "engine texture must match the bridge format and size" + ); + // Wait for the engine's render pass (submitted by the host on the + // wgpu queue) to finish before reading it from our own queue. + let _ = self.wgpu_device.poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }); - // Copy the queue and size out before `ensure_target` so the returned - // `target` borrow (which lives as long as `self`) does not conflict - // with field access below. - let queue = self.command_queue.clone(); - let (width, height) = (self.width, self.height); + // Copy the queue and size out before `ensure_target` so the returned + // `target` borrow (which lives as long as `self`) does not conflict + // with field access below. + let queue = self.command_queue.clone(); + let (width, height) = (self.width, self.height); - let (pixel_buffer, target) = self.ensure_target()?; + let (pixel_buffer, target) = self.ensure_target()?; - // The engine's texture as a Metal texture. - let hal_texture = unsafe { - src.as_hal::() - .expect("bridge requires the wgpu Metal backend") - }; - let hal_texture = &*hal_texture; - // The hal texture's first field is `raw: Retained>` at offset 0 (repr(Rust) keeps field order); read its - // first word to recover the raw MTLTexture pointer. (The - // ProtocolObject itself is a ZST, so it cannot be dereferenced.) - // The hal texture layout (verified empirically against - // wgpu-hal 29.0.4's Metal backend) places the MTLTexture pointer at - // offset 8, after a small enum tag at offset 0. This is fragile by - // nature; the CPU readback path is the robust alternative. - let base = hal_texture as *const _ as *const u8; - let obj_ptr = unsafe { *(base.add(8) as *const *const c_void) }; - let source = unsafe { TextureRef::from_ptr(obj_ptr as *mut metal::MTLTexture) }; + // The engine's texture as a Metal texture. + let hal_texture = unsafe { + src.as_hal::() + .expect("bridge requires the wgpu Metal backend") + }; + let hal_texture = &*hal_texture; + // The hal texture's first field is `raw: Retained>` at offset 0 (repr(Rust) keeps field order); read its + // first word to recover the raw MTLTexture pointer. (The + // ProtocolObject itself is a ZST, so it cannot be dereferenced.) + // The hal texture layout (verified empirically against + // wgpu-hal 29.0.4's Metal backend) places the MTLTexture pointer at + // offset 8, after a small enum tag at offset 0. This is fragile by + // nature; the CPU readback path is the robust alternative. + let base = hal_texture as *const _ as *const u8; + let obj_ptr = unsafe { *(base.add(8) as *const *const c_void) }; + let source = unsafe { TextureRef::from_ptr(obj_ptr as *mut metal::MTLTexture) }; - let command_buffer = queue.new_command_buffer(); - let encoder = command_buffer.new_blit_command_encoder(); - encoder.copy_from_texture( - &source, - 0, - 0, - MTLOrigin { x: 0, y: 0, z: 0 }, - MTLSize::new(width as u64, height as u64, 1), - target, - 0, - 0, - MTLOrigin { x: 0, y: 0, z: 0 }, - ); - encoder.end_encoding(); - command_buffer.commit(); - command_buffer.wait_until_completed(); - Ok(pixel_buffer) - } + let command_buffer = queue.new_command_buffer(); + let encoder = command_buffer.new_blit_command_encoder(); + encoder.copy_from_texture( + &source, + 0, + 0, + MTLOrigin { x: 0, y: 0, z: 0 }, + MTLSize::new(width as u64, height as u64, 1), + target, + 0, + 0, + MTLOrigin { x: 0, y: 0, z: 0 }, + ); + encoder.end_encoding(); + command_buffer.commit(); + command_buffer.wait_until_completed(); + Ok(pixel_buffer) + } - /// CPU readback fallback, phase 1: copy `src` into a staging buffer. - /// - /// After calling this, submit the encoder, then call - /// [`Self::finish_readback`] to map and wrap the bytes. Works with any - /// backend; kept as a fallback because it round-trips through the CPU. - pub fn stage_readback( - &mut self, - encoder: &mut wgpu::CommandEncoder, - src: &wgpu::Texture, - ) -> Result<()> { - let bytes_per_row = align_to_64(self.width * self.format.bytes_per_pixel()); - let total = bytes_per_row as u64 * self.height as u64; - self.staging_buffer = Some(self.wgpu_device.create_buffer(&wgpu::BufferDescriptor { - label: Some("oak-bridge-readback"), - size: total, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - })); - let buffer = self.staging_buffer.as_ref().unwrap(); - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture: src, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row), - rows_per_image: None, - }, - }, - src.size(), - ); - self.readback_bytes_per_row = bytes_per_row; - Ok(()) - } + /// CPU readback fallback, phase 1: copy `src` into a staging buffer. + /// + /// After calling this, submit the encoder, then call + /// [`Self::finish_readback`] to map and wrap the bytes. Works with any + /// backend; kept as a fallback because it round-trips through the CPU. + pub fn stage_readback( + &mut self, + encoder: &mut wgpu::CommandEncoder, + src: &wgpu::Texture, + ) -> Result<()> { + let bytes_per_row = align_to_64(self.width * self.format.bytes_per_pixel()); + let total = bytes_per_row as u64 * self.height as u64; + self.staging_buffer = Some(self.wgpu_device.create_buffer(&wgpu::BufferDescriptor { + label: Some("oak-bridge-readback"), + size: total, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + })); + let buffer = self.staging_buffer.as_ref().unwrap(); + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: src, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + }, + src.size(), + ); + self.readback_bytes_per_row = bytes_per_row; + Ok(()) + } - /// CPU readback fallback, phase 2: map the staged buffer (the encoder - /// from [`Self::stage_readback`] must already be submitted) and wrap the - /// bytes in a `CVPixelBuffer`. - pub fn finish_readback(&mut self) -> Result { - let buffer = self - .staging_buffer - .take() - .ok_or_else(|| anyhow!("readback was not staged"))?; - let total = buffer.size() as usize; - let slice = buffer.slice(..); - let (tx, rx) = std::sync::mpsc::channel(); - slice.map_async(wgpu::MapMode::Read, move |result| { - let _ = tx.send(result); - }); - let _ = self.wgpu_device.poll(wgpu::PollType::Wait { - submission_index: None, - timeout: None, - }); - rx.recv() - .map_err(|_| anyhow!("readback map failed"))??; + /// CPU readback fallback, phase 2: map the staged buffer (the encoder + /// from [`Self::stage_readback`] must already be submitted) and wrap the + /// bytes in a `CVPixelBuffer`. + pub fn finish_readback(&mut self) -> Result { + let buffer = self + .staging_buffer + .take() + .ok_or_else(|| anyhow!("readback was not staged"))?; + let total = buffer.size() as usize; + let slice = buffer.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |result| { + let _ = tx.send(result); + }); + let _ = self.wgpu_device.poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }); + rx.recv().map_err(|_| anyhow!("readback map failed"))??; - let data = slice.get_mapped_range(); - let mut bytes = vec![0u8; total]; - bytes.copy_from_slice(&data); - drop(data); - buffer.unmap(); + let data = slice.get_mapped_range(); + let mut bytes = vec![0u8; total]; + bytes.copy_from_slice(&data); + drop(data); + buffer.unmap(); - self.cpu_bytes = Some(bytes.into_boxed_slice()); - let owned = self.cpu_bytes.as_ref().unwrap().clone(); - let callback_ref: Box> = Box::new(owned.into_vec()); - let release_con = Box::into_raw(callback_ref) as *mut c_void; - let pb = unsafe { - CVPixelBuffer::new_with_bytes( - self.format.ostype(), - self.width as usize, - self.height as usize, - release_con as *mut c_void, - self.readback_bytes_per_row as usize, - free_bytes, - release_con, - None, - ) - }; - pb.map_err(|status| anyhow!("CVPixelBufferCreateWithBytes failed: {status}")) - } + self.cpu_bytes = Some(bytes.into_boxed_slice()); + let owned = self.cpu_bytes.as_ref().unwrap().clone(); + let callback_ref: Box> = Box::new(owned.into_vec()); + let release_con = Box::into_raw(callback_ref) as *mut c_void; + let pb = unsafe { + CVPixelBuffer::new_with_bytes( + self.format.ostype(), + self.width as usize, + self.height as usize, + release_con as *mut c_void, + self.readback_bytes_per_row as usize, + free_bytes, + release_con, + None, + ) + }; + pb.map_err(|status| anyhow!("CVPixelBufferCreateWithBytes failed: {status}")) + } } impl SurfaceFormat { - /// Bytes per pixel of this format. - fn bytes_per_pixel(self) -> u32 { - match self { - SurfaceFormat::Bgra8Unorm => 4, - SurfaceFormat::Rgba16Float => 8, - } - } + /// Bytes per pixel of this format. + fn bytes_per_pixel(self) -> u32 { + match self { + SurfaceFormat::Bgra8Unorm => 4, + SurfaceFormat::Rgba16Float => 8, + } + } } /// 64-byte alignment used by CoreVideo row buffers. fn align_to_64(value: u32) -> u32 { - (value + 63) & !63 + (value + 63) & !63 } /// Release callback for the readback path: frees the boxed byte vector. extern "C" fn free_bytes(release_ref_con: *mut c_void, _base_address: *const *const c_void) { - if !release_ref_con.is_null() { - unsafe { - drop(Box::from_raw(release_ref_con as *mut Vec)); - } - } + if !release_ref_con.is_null() { + unsafe { + drop(Box::from_raw(release_ref_con as *mut Vec)); + } + } } /// Create an IOSurface-backed `CVPixelBuffer` (the IOSurface is created /// internally by CoreVideo, which is what makes the buffer shareable with /// Metal without any CPU copy). fn create_iosurface_pixel_buffer( - width: u32, - height: u32, - format: SurfaceFormat, + width: u32, + height: u32, + format: SurfaceFormat, ) -> Result { - let io_properties = - CFDictionary::::from_CFType_pairs(&[]); - let attributes = CFDictionary::from_CFType_pairs(&[ - ( - unsafe { CFString::wrap_under_get_rule(kCVPixelBufferIOSurfacePropertiesKey) }, - io_properties.as_CFType(), - ), - ( - unsafe { CFString::wrap_under_get_rule(kCVPixelBufferMetalCompatibilityKey) }, - CFBoolean::true_value().as_CFType(), - ), - ]); - CVPixelBuffer::new( - format.ostype(), - width as usize, - height as usize, - Some(&attributes), - ) - .map_err(|status| anyhow!("CVPixelBufferCreate failed: {status}")) + let io_properties = + CFDictionary::::from_CFType_pairs(&[]); + let attributes = CFDictionary::from_CFType_pairs(&[ + ( + unsafe { CFString::wrap_under_get_rule(kCVPixelBufferIOSurfacePropertiesKey) }, + io_properties.as_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kCVPixelBufferMetalCompatibilityKey) }, + CFBoolean::true_value().as_CFType(), + ), + ]); + CVPixelBuffer::new( + format.ostype(), + width as usize, + height as usize, + Some(&attributes), + ) + .map_err(|status| anyhow!("CVPixelBufferCreate failed: {status}")) } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn formats_map_consistently() { - for format in [SurfaceFormat::Bgra8Unorm, SurfaceFormat::Rgba16Float] { - assert_eq!(format.metal(), format.wgpu().into_metal_pixel_format()); - assert!(format.ostype() != 0); - } - } + #[test] + fn formats_map_consistently() { + for format in [SurfaceFormat::Bgra8Unorm, SurfaceFormat::Rgba16Float] { + assert_eq!(format.metal(), format.wgpu().into_metal_pixel_format()); + assert!(format.ostype() != 0); + } + } - #[test] - fn row_alignment() { - assert_eq!(align_to_64(64), 64); - assert_eq!(align_to_64(66), 128); - assert_eq!(align_to_64(0), 0); - } + #[test] + fn row_alignment() { + assert_eq!(align_to_64(64), 64); + assert_eq!(align_to_64(66), 128); + assert_eq!(align_to_64(0), 0); + } - trait IntoMetalPixelFormat { - fn into_metal_pixel_format(self) -> MTLPixelFormat; - } - impl IntoMetalPixelFormat for wgpu::TextureFormat { - fn into_metal_pixel_format(self) -> MTLPixelFormat { - match self { - wgpu::TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm, - wgpu::TextureFormat::Rgba16Float => MTLPixelFormat::RGBA16Float, - _ => MTLPixelFormat::Invalid, - } - } - } + trait IntoMetalPixelFormat { + fn into_metal_pixel_format(self) -> MTLPixelFormat; + } + impl IntoMetalPixelFormat for wgpu::TextureFormat { + fn into_metal_pixel_format(self) -> MTLPixelFormat { + match self { + wgpu::TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm, + wgpu::TextureFormat::Rgba16Float => MTLPixelFormat::RGBA16Float, + _ => MTLPixelFormat::Invalid, + } + } + } } diff --git a/tooling/perf/src/implementation.rs b/tooling/perf/src/implementation.rs index c151dda91f..9353d36477 100644 --- a/tooling/perf/src/implementation.rs +++ b/tooling/perf/src/implementation.rs @@ -6,141 +6,141 @@ use serde::{Deserialize, Serialize}; use std::{num::NonZero, time::Duration}; pub mod consts { - //! Preset identifiers and constants so that the profiler and proc macro agree - //! on their communication protocol. + //! Preset identifiers and constants so that the profiler and proc macro agree + //! on their communication protocol. - /// The suffix on the actual test function. - pub const SUF_NORMAL: &str = "__ZED_PERF_FN"; - /// The suffix on an extra function which prints metadata about a test to stdout. - pub const SUF_MDATA: &str = "__ZED_PERF_MDATA"; - /// The env var in which we pass the iteration count to our tests. - pub const ITER_ENV_VAR: &str = "ZED_PERF_ITER"; - /// The prefix printed on all benchmark test metadata lines, to distinguish it from - /// possible output by the test harness itself. - pub const MDATA_LINE_PREF: &str = "ZED_MDATA_"; - /// The version number for the data returned from the test metadata function. - /// Increment on non-backwards-compatible changes. - pub const MDATA_VER: u32 = 0; - /// The default weight, if none is specified. - pub const WEIGHT_DEFAULT: u8 = 50; - /// How long a test must have run to be assumed to be reliable-ish. - pub const NOISE_CUTOFF: std::time::Duration = std::time::Duration::from_millis(250); + /// The suffix on the actual test function. + pub const SUF_NORMAL: &str = "__ZED_PERF_FN"; + /// The suffix on an extra function which prints metadata about a test to stdout. + pub const SUF_MDATA: &str = "__ZED_PERF_MDATA"; + /// The env var in which we pass the iteration count to our tests. + pub const ITER_ENV_VAR: &str = "ZED_PERF_ITER"; + /// The prefix printed on all benchmark test metadata lines, to distinguish it from + /// possible output by the test harness itself. + pub const MDATA_LINE_PREF: &str = "ZED_MDATA_"; + /// The version number for the data returned from the test metadata function. + /// Increment on non-backwards-compatible changes. + pub const MDATA_VER: u32 = 0; + /// The default weight, if none is specified. + pub const WEIGHT_DEFAULT: u8 = 50; + /// How long a test must have run to be assumed to be reliable-ish. + pub const NOISE_CUTOFF: std::time::Duration = std::time::Duration::from_millis(250); - /// Identifier for the iteration count of a test metadata. - pub const ITER_COUNT_LINE_NAME: &str = "iter_count"; - /// Identifier for the weight of a test metadata. - pub const WEIGHT_LINE_NAME: &str = "weight"; - /// Identifier for importance in test metadata. - pub const IMPORTANCE_LINE_NAME: &str = "importance"; - /// Identifier for the test metadata version. - pub const VERSION_LINE_NAME: &str = "version"; + /// Identifier for the iteration count of a test metadata. + pub const ITER_COUNT_LINE_NAME: &str = "iter_count"; + /// Identifier for the weight of a test metadata. + pub const WEIGHT_LINE_NAME: &str = "weight"; + /// Identifier for importance in test metadata. + pub const IMPORTANCE_LINE_NAME: &str = "importance"; + /// Identifier for the test metadata version. + pub const VERSION_LINE_NAME: &str = "version"; - /// Where to save json run information. - pub const RUNS_DIR: &str = ".perf-runs"; + /// Where to save json run information. + pub const RUNS_DIR: &str = ".perf-runs"; } /// How relevant a benchmark is. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum Importance { - /// Regressions shouldn't be accepted without good reason. - Critical = 4, - /// Regressions should be paid extra attention. - Important = 3, - /// No extra attention should be paid to regressions, but they might still - /// be indicative of something happening. - #[default] - Average = 2, - /// Unclear if regressions are likely to be meaningful, but still worth keeping - /// an eye on. Lowest level that's checked by default by the profiler. - Iffy = 1, - /// Regressions are likely to be spurious or don't affect core functionality. - /// Only relevant if a lot of them happen, or as supplemental evidence for a - /// higher-importance benchmark regressing. Not checked by default. - Fluff = 0, + /// Regressions shouldn't be accepted without good reason. + Critical = 4, + /// Regressions should be paid extra attention. + Important = 3, + /// No extra attention should be paid to regressions, but they might still + /// be indicative of something happening. + #[default] + Average = 2, + /// Unclear if regressions are likely to be meaningful, but still worth keeping + /// an eye on. Lowest level that's checked by default by the profiler. + Iffy = 1, + /// Regressions are likely to be spurious or don't affect core functionality. + /// Only relevant if a lot of them happen, or as supplemental evidence for a + /// higher-importance benchmark regressing. Not checked by default. + Fluff = 0, } impl std::fmt::Display for Importance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Importance::Critical => f.write_str("critical"), - Importance::Important => f.write_str("important"), - Importance::Average => f.write_str("average"), - Importance::Iffy => f.write_str("iffy"), - Importance::Fluff => f.write_str("fluff"), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Importance::Critical => f.write_str("critical"), + Importance::Important => f.write_str("important"), + Importance::Average => f.write_str("average"), + Importance::Iffy => f.write_str("iffy"), + Importance::Fluff => f.write_str("fluff"), + } + } } /// Why or when did this test fail? #[derive(Clone, Debug, Serialize, Deserialize)] pub enum FailKind { - /// Failed while triaging it to determine the iteration count. - Triage, - /// Failed while profiling it. - Profile, - /// Failed due to an incompatible version for the test. - VersionMismatch, - /// Could not parse metadata for a test. - BadMetadata, - /// Skipped due to filters applied on the perf run. - Skipped, + /// Failed while triaging it to determine the iteration count. + Triage, + /// Failed while profiling it. + Profile, + /// Failed due to an incompatible version for the test. + VersionMismatch, + /// Could not parse metadata for a test. + BadMetadata, + /// Skipped due to filters applied on the perf run. + Skipped, } impl std::fmt::Display for FailKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FailKind::Triage => f.write_str("errored in triage"), - FailKind::Profile => f.write_str("errored while profiling"), - FailKind::VersionMismatch => f.write_str("test version mismatch"), - FailKind::BadMetadata => f.write_str("bad test metadata"), - FailKind::Skipped => f.write_str("skipped"), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FailKind::Triage => f.write_str("errored in triage"), + FailKind::Profile => f.write_str("errored while profiling"), + FailKind::VersionMismatch => f.write_str("test version mismatch"), + FailKind::BadMetadata => f.write_str("bad test metadata"), + FailKind::Skipped => f.write_str("skipped"), + } + } } /// Information about a given perf test. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TestMdata { - /// A version number for when the test was generated. If this is greater - /// than the version this test handler expects, one of the following will - /// happen in an unspecified manner: - /// - The test is skipped silently. - /// - The handler exits with an error message indicating the version mismatch - /// or inability to parse the metadata. - /// - /// INVARIANT: If `version` <= `MDATA_VER`, this tool *must* be able to - /// correctly parse the output of this test. - pub version: u32, - /// How many iterations to pass this test if this is preset, or how many - /// iterations a test ended up running afterwards if determined at runtime. - pub iterations: Option>, - /// The importance of this particular test. See the docs on `Importance` for - /// details. - pub importance: Importance, - /// The weight of this particular test within its importance category. Used - /// when comparing across runs. - pub weight: u8, + /// A version number for when the test was generated. If this is greater + /// than the version this test handler expects, one of the following will + /// happen in an unspecified manner: + /// - The test is skipped silently. + /// - The handler exits with an error message indicating the version mismatch + /// or inability to parse the metadata. + /// + /// INVARIANT: If `version` <= `MDATA_VER`, this tool *must* be able to + /// correctly parse the output of this test. + pub version: u32, + /// How many iterations to pass this test if this is preset, or how many + /// iterations a test ended up running afterwards if determined at runtime. + pub iterations: Option>, + /// The importance of this particular test. See the docs on `Importance` for + /// details. + pub importance: Importance, + /// The weight of this particular test within its importance category. Used + /// when comparing across runs. + pub weight: u8, } /// The actual timings of a test, as measured by Hyperfine. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Timings { - /// Mean runtime for `self.iter_total` runs of this test. - pub mean: Duration, - /// Standard deviation for the above. - pub stddev: Duration, + /// Mean runtime for `self.iter_total` runs of this test. + pub mean: Duration, + /// Standard deviation for the above. + pub stddev: Duration, } impl Timings { - /// How many iterations does this test seem to do per second? - #[expect( - clippy::cast_precision_loss, - reason = "We only care about a couple sig figs anyways" - )] - #[must_use] - pub fn iters_per_sec(&self, total_iters: NonZero) -> f64 { - (1000. / self.mean.as_millis() as f64) * total_iters.get() as f64 - } + /// How many iterations does this test seem to do per second? + #[expect( + clippy::cast_precision_loss, + reason = "We only care about a couple sig figs anyways" + )] + #[must_use] + pub fn iters_per_sec(&self, total_iters: NonZero) -> f64 { + (1000. / self.mean.as_millis() as f64) * total_iters.get() as f64 + } } /// Aggregate results, meant to be used for a given importance category. Each @@ -150,301 +150,301 @@ type CategoryInfo = HashMap, u8)>; /// Aggregate output of all tests run by this handler. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Output { - /// A list of test outputs. Format is `(test_name, mdata, timings)`. - /// The latter being `Ok(_)` indicates the test succeeded. - /// - /// INVARIANT: If the test succeeded, the second field is `Some(mdata)` and - /// `mdata.iterations` is `Some(_)`. - tests: Vec<(String, Option, Result)>, + /// A list of test outputs. Format is `(test_name, mdata, timings)`. + /// The latter being `Ok(_)` indicates the test succeeded. + /// + /// INVARIANT: If the test succeeded, the second field is `Some(mdata)` and + /// `mdata.iterations` is `Some(_)`. + tests: Vec<(String, Option, Result)>, } impl Output { - /// Instantiates an empty "output". Useful for merging. - #[must_use] - pub fn blank() -> Self { - Output { tests: Vec::new() } - } + /// Instantiates an empty "output". Useful for merging. + #[must_use] + pub fn blank() -> Self { + Output { tests: Vec::new() } + } - /// Reports a success and adds it to this run's `Output`. - pub fn success( - &mut self, - name: impl AsRef, - mut mdata: TestMdata, - iters: NonZero, - timings: Timings, - ) { - mdata.iterations = Some(iters); - self.tests - .push((name.as_ref().to_string(), Some(mdata), Ok(timings))); - } + /// Reports a success and adds it to this run's `Output`. + pub fn success( + &mut self, + name: impl AsRef, + mut mdata: TestMdata, + iters: NonZero, + timings: Timings, + ) { + mdata.iterations = Some(iters); + self.tests + .push((name.as_ref().to_string(), Some(mdata), Ok(timings))); + } - /// Reports a failure and adds it to this run's `Output`. If this test was tried - /// with some number of iterations (i.e. this was not a version mismatch or skipped - /// test), it should be reported also. - /// - /// Using the `fail!()` macro is usually more convenient. - pub fn failure( - &mut self, - name: impl AsRef, - mut mdata: Option, - attempted_iters: Option>, - kind: FailKind, - ) { - if let Some(ref mut mdata) = mdata { - mdata.iterations = attempted_iters; - } - self.tests - .push((name.as_ref().to_string(), mdata, Err(kind))); - } + /// Reports a failure and adds it to this run's `Output`. If this test was tried + /// with some number of iterations (i.e. this was not a version mismatch or skipped + /// test), it should be reported also. + /// + /// Using the `fail!()` macro is usually more convenient. + pub fn failure( + &mut self, + name: impl AsRef, + mut mdata: Option, + attempted_iters: Option>, + kind: FailKind, + ) { + if let Some(ref mut mdata) = mdata { + mdata.iterations = attempted_iters; + } + self.tests + .push((name.as_ref().to_string(), mdata, Err(kind))); + } - /// True if no tests executed this run. - #[must_use] - pub fn is_empty(&self) -> bool { - self.tests.is_empty() - } + /// True if no tests executed this run. + #[must_use] + pub fn is_empty(&self) -> bool { + self.tests.is_empty() + } - /// Sorts the runs in the output in the order that we want them printed. - pub fn sort(&mut self) { - self.tests.sort_unstable_by(|a, b| match (a, b) { - // Tests where we got no metadata go at the end. - ((_, Some(_), _), (_, None, _)) => std::cmp::Ordering::Greater, - ((_, None, _), (_, Some(_), _)) => std::cmp::Ordering::Less, - // Then sort by importance, then weight. - ((_, Some(a_mdata), _), (_, Some(b_mdata), _)) => { - let c = a_mdata.importance.cmp(&b_mdata.importance); - if matches!(c, std::cmp::Ordering::Equal) { - a_mdata.weight.cmp(&b_mdata.weight) - } else { - c - } - } - // Lastly by name. - ((a_name, ..), (b_name, ..)) => a_name.cmp(b_name), - }); - } + /// Sorts the runs in the output in the order that we want them printed. + pub fn sort(&mut self) { + self.tests.sort_unstable_by(|a, b| match (a, b) { + // Tests where we got no metadata go at the end. + ((_, Some(_), _), (_, None, _)) => std::cmp::Ordering::Greater, + ((_, None, _), (_, Some(_), _)) => std::cmp::Ordering::Less, + // Then sort by importance, then weight. + ((_, Some(a_mdata), _), (_, Some(b_mdata), _)) => { + let c = a_mdata.importance.cmp(&b_mdata.importance); + if matches!(c, std::cmp::Ordering::Equal) { + a_mdata.weight.cmp(&b_mdata.weight) + } else { + c + } + } + // Lastly by name. + ((a_name, ..), (b_name, ..)) => a_name.cmp(b_name), + }); + } - /// Merges the output of two runs, appending a prefix to the results of the new run. - /// To be used in conjunction with `Output::blank()`, or else only some tests will have - /// a prefix set. - pub fn merge<'a>(&mut self, other: Self, pref_other: impl Into>) { - let pref = if let Some(pref) = pref_other.into() { - "crates/".to_string() + pref + "::" - } else { - String::new() - }; - self.tests = std::mem::take(&mut self.tests) - .into_iter() - .chain( - other - .tests - .into_iter() - .map(|(name, md, tm)| (pref.clone() + &name, md, tm)), - ) - .collect(); - } + /// Merges the output of two runs, appending a prefix to the results of the new run. + /// To be used in conjunction with `Output::blank()`, or else only some tests will have + /// a prefix set. + pub fn merge<'a>(&mut self, other: Self, pref_other: impl Into>) { + let pref = if let Some(pref) = pref_other.into() { + "crates/".to_string() + pref + "::" + } else { + String::new() + }; + self.tests = std::mem::take(&mut self.tests) + .into_iter() + .chain( + other + .tests + .into_iter() + .map(|(name, md, tm)| (pref.clone() + &name, md, tm)), + ) + .collect(); + } - /// Evaluates the performance of `self` against `baseline`. The latter is taken - /// as the comparison point, i.e. a positive resulting `PerfReport` means that - /// `self` performed better. - /// - /// # Panics - /// `self` and `baseline` are assumed to have the iterations field on all - /// `TestMdata`s set to `Some(_)` if the `TestMdata` is present itself. - #[must_use] - pub fn compare_perf(self, baseline: Self) -> PerfReport { - let self_categories = self.collapse(); - let mut other_categories = baseline.collapse(); + /// Evaluates the performance of `self` against `baseline`. The latter is taken + /// as the comparison point, i.e. a positive resulting `PerfReport` means that + /// `self` performed better. + /// + /// # Panics + /// `self` and `baseline` are assumed to have the iterations field on all + /// `TestMdata`s set to `Some(_)` if the `TestMdata` is present itself. + #[must_use] + pub fn compare_perf(self, baseline: Self) -> PerfReport { + let self_categories = self.collapse(); + let mut other_categories = baseline.collapse(); - let deltas = self_categories - .into_iter() - .filter_map(|(cat, self_data)| { - // Only compare categories where both meow - // runs have data. / - let mut other_data = other_categories.remove(&cat)?; - let mut max = f64::MIN; - let mut min = f64::MAX; + let deltas = self_categories + .into_iter() + .filter_map(|(cat, self_data)| { + // Only compare categories where both meow + // runs have data. / + let mut other_data = other_categories.remove(&cat)?; + let mut max = f64::MIN; + let mut min = f64::MAX; - // Running totals for averaging out tests. - let mut r_total_numerator = 0.; - let mut r_total_denominator = 0; - // Yeah this is O(n^2), but realistically it'll hardly be a bottleneck. - for (name, (s_timings, s_iters, weight)) in self_data { - // Only use the new weights if they conflict. - let Some((o_timings, o_iters, _)) = other_data.remove(&name) else { - continue; - }; - let shift = - (o_timings.iters_per_sec(o_iters) / s_timings.iters_per_sec(s_iters)) - 1.; - if shift > max { - max = shift; - } - if shift < min { - min = shift; - } - r_total_numerator += shift * f64::from(weight); - r_total_denominator += u32::from(weight); - } - // There were no runs here! - if r_total_denominator == 0 { - None - } else { - let mean = r_total_numerator / f64::from(r_total_denominator); - // TODO: also aggregate standard deviation? That's harder to keep - // meaningful, though, since we dk which tests are correlated. - Some((cat, PerfDelta { max, mean, min })) - } - }) - .collect(); + // Running totals for averaging out tests. + let mut r_total_numerator = 0.; + let mut r_total_denominator = 0; + // Yeah this is O(n^2), but realistically it'll hardly be a bottleneck. + for (name, (s_timings, s_iters, weight)) in self_data { + // Only use the new weights if they conflict. + let Some((o_timings, o_iters, _)) = other_data.remove(&name) else { + continue; + }; + let shift = + (o_timings.iters_per_sec(o_iters) / s_timings.iters_per_sec(s_iters)) - 1.; + if shift > max { + max = shift; + } + if shift < min { + min = shift; + } + r_total_numerator += shift * f64::from(weight); + r_total_denominator += u32::from(weight); + } + // There were no runs here! + if r_total_denominator == 0 { + None + } else { + let mean = r_total_numerator / f64::from(r_total_denominator); + // TODO: also aggregate standard deviation? That's harder to keep + // meaningful, though, since we dk which tests are correlated. + Some((cat, PerfDelta { max, mean, min })) + } + }) + .collect(); - PerfReport { deltas } - } + PerfReport { deltas } + } - /// Collapses the `PerfReport` into a `HashMap` over `Importance`, with - /// each importance category having its tests contained. - fn collapse(self) -> HashMap { - let mut categories = HashMap::>::default(); - for entry in self.tests { - if let Some(mdata) = entry.1 - && let Ok(timings) = entry.2 - { - if let Some(handle) = categories.get_mut(&mdata.importance) { - handle.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); - } else { - let mut new = HashMap::default(); - new.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); - categories.insert(mdata.importance, new); - } - } - } + /// Collapses the `PerfReport` into a `HashMap` over `Importance`, with + /// each importance category having its tests contained. + fn collapse(self) -> HashMap { + let mut categories = HashMap::>::default(); + for entry in self.tests { + if let Some(mdata) = entry.1 + && let Ok(timings) = entry.2 + { + if let Some(handle) = categories.get_mut(&mdata.importance) { + handle.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); + } else { + let mut new = HashMap::default(); + new.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); + categories.insert(mdata.importance, new); + } + } + } - categories - } + categories + } } impl std::fmt::Display for Output { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Don't print the header for an empty run. - if self.tests.is_empty() { - return Ok(()); - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Don't print the header for an empty run. + if self.tests.is_empty() { + return Ok(()); + } - // We want to print important tests at the top, then alphabetical. - let mut sorted = self.clone(); - sorted.sort(); - // Markdown header for making a nice little table :> - writeln!( - f, - "| Command | Iter/sec | Mean [ms] | SD [ms] | Iterations | Importance (weight) |", - )?; - writeln!(f, "|:---|---:|---:|---:|---:|---:|")?; - for (name, metadata, timings) in &sorted.tests { - match metadata { - Some(metadata) => match timings { - // Happy path. - Ok(timings) => { - // If the test succeeded, then metadata.iterations is Some(_). - writeln!( - f, - "| {} | {:.2} | {} | {:.2} | {} | {} ({}) |", - name, - timings.iters_per_sec(metadata.iterations.unwrap()), - { - // Very small mean runtimes will give inaccurate - // results. Should probably also penalise weight. - let mean = timings.mean.as_secs_f64() * 1000.; - if mean < consts::NOISE_CUTOFF.as_secs_f64() * 1000. / 8. { - format!("{mean:.2} (unreliable)") - } else { - format!("{mean:.2}") - } - }, - timings.stddev.as_secs_f64() * 1000., - metadata.iterations.unwrap(), - metadata.importance, - metadata.weight, - )?; - } - // We have (some) metadata, but the test errored. - Err(err) => writeln!( - f, - "| ({}) {} | N/A | N/A | N/A | {} | {} ({}) |", - err, - name, - metadata - .iterations - .map_or_else(|| "N/A".to_owned(), |i| format!("{i}")), - metadata.importance, - metadata.weight - )?, - }, - // No metadata, couldn't even parse the test output. - None => writeln!( - f, - "| ({}) {} | N/A | N/A | N/A | N/A | N/A |", - timings.as_ref().unwrap_err(), - name - )?, - } - } - Ok(()) - } + // We want to print important tests at the top, then alphabetical. + let mut sorted = self.clone(); + sorted.sort(); + // Markdown header for making a nice little table :> + writeln!( + f, + "| Command | Iter/sec | Mean [ms] | SD [ms] | Iterations | Importance (weight) |", + )?; + writeln!(f, "|:---|---:|---:|---:|---:|---:|")?; + for (name, metadata, timings) in &sorted.tests { + match metadata { + Some(metadata) => match timings { + // Happy path. + Ok(timings) => { + // If the test succeeded, then metadata.iterations is Some(_). + writeln!( + f, + "| {} | {:.2} | {} | {:.2} | {} | {} ({}) |", + name, + timings.iters_per_sec(metadata.iterations.unwrap()), + { + // Very small mean runtimes will give inaccurate + // results. Should probably also penalise weight. + let mean = timings.mean.as_secs_f64() * 1000.; + if mean < consts::NOISE_CUTOFF.as_secs_f64() * 1000. / 8. { + format!("{mean:.2} (unreliable)") + } else { + format!("{mean:.2}") + } + }, + timings.stddev.as_secs_f64() * 1000., + metadata.iterations.unwrap(), + metadata.importance, + metadata.weight, + )?; + } + // We have (some) metadata, but the test errored. + Err(err) => writeln!( + f, + "| ({}) {} | N/A | N/A | N/A | {} | {} ({}) |", + err, + name, + metadata + .iterations + .map_or_else(|| "N/A".to_owned(), |i| format!("{i}")), + metadata.importance, + metadata.weight + )?, + }, + // No metadata, couldn't even parse the test output. + None => writeln!( + f, + "| ({}) {} | N/A | N/A | N/A | N/A | N/A |", + timings.as_ref().unwrap_err(), + name + )?, + } + } + Ok(()) + } } /// The difference in performance between two runs within a given importance /// category. struct PerfDelta { - /// The biggest improvement / least bad regression. - max: f64, - /// The weighted average change in test times. - mean: f64, - /// The worst regression / smallest improvement. - min: f64, + /// The biggest improvement / least bad regression. + max: f64, + /// The weighted average change in test times. + mean: f64, + /// The worst regression / smallest improvement. + min: f64, } /// Shim type for reporting all performance deltas across importance categories. pub struct PerfReport { - /// Inner (group, diff) pairing. - deltas: HashMap, + /// Inner (group, diff) pairing. + deltas: HashMap, } impl std::fmt::Display for PerfReport { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.deltas.is_empty() { - return write!(f, "(no matching tests)"); - } - let sorted = self.deltas.iter().collect::>(); - writeln!(f, "| Category | Max | Mean | Min |")?; - // We don't want to print too many newlines at the end, so handle newlines - // a little jankily like this. - write!(f, "|:---|---:|---:|---:|")?; - for (cat, delta) in sorted.into_iter().rev() { - const SIGN_POS: &str = "↑"; - const SIGN_NEG: &str = "↓"; - const SIGN_NEUTRAL_POS: &str = "±↑"; - const SIGN_NEUTRAL_NEG: &str = "±↓"; + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.deltas.is_empty() { + return write!(f, "(no matching tests)"); + } + let sorted = self.deltas.iter().collect::>(); + writeln!(f, "| Category | Max | Mean | Min |")?; + // We don't want to print too many newlines at the end, so handle newlines + // a little jankily like this. + write!(f, "|:---|---:|---:|---:|")?; + for (cat, delta) in sorted.into_iter().rev() { + const SIGN_POS: &str = "↑"; + const SIGN_NEG: &str = "↓"; + const SIGN_NEUTRAL_POS: &str = "±↑"; + const SIGN_NEUTRAL_NEG: &str = "±↓"; - let prettify = |time: f64| { - let sign = if time > 0.05 { - SIGN_POS - } else if time > 0. { - SIGN_NEUTRAL_POS - } else if time > -0.05 { - SIGN_NEUTRAL_NEG - } else { - SIGN_NEG - }; - format!("{} {:.1}%", sign, time.abs() * 100.) - }; + let prettify = |time: f64| { + let sign = if time > 0.05 { + SIGN_POS + } else if time > 0. { + SIGN_NEUTRAL_POS + } else if time > -0.05 { + SIGN_NEUTRAL_NEG + } else { + SIGN_NEG + }; + format!("{} {:.1}%", sign, time.abs() * 100.) + }; - // Pretty-print these instead of just using the float display impl. - write!( - f, - "\n| {cat} | {} | {} | {} |", - prettify(delta.max), - prettify(delta.mean), - prettify(delta.min) - )?; - } - Ok(()) - } + // Pretty-print these instead of just using the float display impl. + write!( + f, + "\n| {cat} | {} | {} | {} |", + prettify(delta.max), + prettify(delta.mean), + prettify(delta.min) + )?; + } + Ok(()) + } } diff --git a/tooling/perf/src/main.rs b/tooling/perf/src/main.rs index 5359a4a0ce..4a0e7bd9de 100644 --- a/tooling/perf/src/main.rs +++ b/tooling/perf/src/main.rs @@ -51,13 +51,13 @@ mod implementation; use implementation::{FailKind, Importance, Output, TestMdata, Timings, consts}; use std::{ - fs::OpenOptions, - io::{Read, Write}, - num::NonZero, - path::{Path, PathBuf}, - process::{Command, Stdio}, - sync::atomic::{AtomicBool, Ordering}, - time::{Duration, Instant}, + fs::OpenOptions, + io::{Read, Write}, + num::NonZero, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, }; /// How many iterations to attempt the first time a test is run. @@ -70,233 +70,233 @@ static QUIET: AtomicBool = AtomicBool::new(false); /// Report a failure into the output and skip an iteration. macro_rules! fail { - ($output:ident, $name:expr, $kind:expr) => {{ - $output.failure($name, None, None, $kind); - continue; - }}; - ($output:ident, $name:expr, $mdata:expr, $kind:expr) => {{ - $output.failure($name, Some($mdata), None, $kind); - continue; - }}; - ($output:ident, $name:expr, $mdata:expr, $count:expr, $kind:expr) => {{ - $output.failure($name, Some($mdata), Some($count), $kind); - continue; - }}; + ($output:ident, $name:expr, $kind:expr) => {{ + $output.failure($name, None, None, $kind); + continue; + }}; + ($output:ident, $name:expr, $mdata:expr, $kind:expr) => {{ + $output.failure($name, Some($mdata), None, $kind); + continue; + }}; + ($output:ident, $name:expr, $mdata:expr, $count:expr, $kind:expr) => {{ + $output.failure($name, Some($mdata), Some($count), $kind); + continue; + }}; } /// How does this perf run return its output? enum OutputKind<'a> { - /// Print markdown to the terminal. - Markdown, - /// Save JSON to a file. - Json(&'a Path), + /// Print markdown to the terminal. + Markdown, + /// Save JSON to a file. + Json(&'a Path), } impl OutputKind<'_> { - /// Logs the output of a run as per the `OutputKind`. - fn log(&self, output: &Output, t_bin: &str) { - match self { - OutputKind::Markdown => println!("{output}"), - OutputKind::Json(ident) => { - // We're going to be in tooling/perf/$whatever. - let wspace_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("..") - .join(".."); - let runs_dir = PathBuf::from(&wspace_dir).join(consts::RUNS_DIR); - std::fs::create_dir_all(&runs_dir).unwrap(); - assert!( - !ident.to_string_lossy().is_empty(), - "FATAL: Empty filename specified!" - ); - // Get the test binary's crate's name; a path like - // target/release-fast/deps/gpui-061ff76c9b7af5d7 - // would be reduced to just "gpui". - let test_bin_stripped = Path::new(t_bin) - .file_name() - .unwrap() - .to_str() - .unwrap() - .rsplit_once('-') - .unwrap() - .0; - let mut file_path = runs_dir.join(ident); - file_path - .as_mut_os_string() - .push(format!(".{test_bin_stripped}.json")); - let mut out_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&file_path) - .unwrap(); - out_file - .write_all(&serde_json::to_vec(&output).unwrap()) - .unwrap(); - if !QUIET.load(Ordering::Relaxed) { - eprintln!("JSON output written to {}", file_path.display()); - } - } - } - } + /// Logs the output of a run as per the `OutputKind`. + fn log(&self, output: &Output, t_bin: &str) { + match self { + OutputKind::Markdown => println!("{output}"), + OutputKind::Json(ident) => { + // We're going to be in tooling/perf/$whatever. + let wspace_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join(".."); + let runs_dir = PathBuf::from(&wspace_dir).join(consts::RUNS_DIR); + std::fs::create_dir_all(&runs_dir).unwrap(); + assert!( + !ident.to_string_lossy().is_empty(), + "FATAL: Empty filename specified!" + ); + // Get the test binary's crate's name; a path like + // target/release-fast/deps/gpui-061ff76c9b7af5d7 + // would be reduced to just "gpui". + let test_bin_stripped = Path::new(t_bin) + .file_name() + .unwrap() + .to_str() + .unwrap() + .rsplit_once('-') + .unwrap() + .0; + let mut file_path = runs_dir.join(ident); + file_path + .as_mut_os_string() + .push(format!(".{test_bin_stripped}.json")); + let mut out_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&file_path) + .unwrap(); + out_file + .write_all(&serde_json::to_vec(&output).unwrap()) + .unwrap(); + if !QUIET.load(Ordering::Relaxed) { + eprintln!("JSON output written to {}", file_path.display()); + } + } + } + } } /// Runs a given metadata-returning function from a test handler, parsing its /// output into a `TestMdata`. fn parse_mdata(t_bin: &str, mdata_fn: &str) -> Result { - let mut cmd = Command::new(t_bin); - cmd.args([mdata_fn, "--exact", "--nocapture"]); - let out = cmd - .output() - .expect("FATAL: Could not run test binary {t_bin}"); - assert!(out.status.success()); - let stdout = String::from_utf8_lossy(&out.stdout); - let mut version = None; - let mut iterations = None; - let mut importance = Importance::default(); - let mut weight = consts::WEIGHT_DEFAULT; - for line in stdout - .lines() - .filter_map(|l| l.strip_prefix(consts::MDATA_LINE_PREF)) - { - let mut items = line.split_whitespace(); - // For v0, we know the ident always comes first, then one field. - match items.next().ok_or(FailKind::BadMetadata)? { - consts::VERSION_LINE_NAME => { - let v = items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)?; - if v > consts::MDATA_VER { - return Err(FailKind::VersionMismatch); - } - version = Some(v); - } - consts::ITER_COUNT_LINE_NAME => { - // This should never be zero! - iterations = Some( - items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)? - .try_into() - .map_err(|_| FailKind::BadMetadata)?, - ); - } - consts::IMPORTANCE_LINE_NAME => { - importance = match items.next().ok_or(FailKind::BadMetadata)? { - "critical" => Importance::Critical, - "important" => Importance::Important, - "average" => Importance::Average, - "iffy" => Importance::Iffy, - "fluff" => Importance::Fluff, - _ => return Err(FailKind::BadMetadata), - }; - } - consts::WEIGHT_LINE_NAME => { - weight = items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)?; - } - _ => unreachable!(), - } - } + let mut cmd = Command::new(t_bin); + cmd.args([mdata_fn, "--exact", "--nocapture"]); + let out = cmd + .output() + .expect("FATAL: Could not run test binary {t_bin}"); + assert!(out.status.success()); + let stdout = String::from_utf8_lossy(&out.stdout); + let mut version = None; + let mut iterations = None; + let mut importance = Importance::default(); + let mut weight = consts::WEIGHT_DEFAULT; + for line in stdout + .lines() + .filter_map(|l| l.strip_prefix(consts::MDATA_LINE_PREF)) + { + let mut items = line.split_whitespace(); + // For v0, we know the ident always comes first, then one field. + match items.next().ok_or(FailKind::BadMetadata)? { + consts::VERSION_LINE_NAME => { + let v = items + .next() + .ok_or(FailKind::BadMetadata)? + .parse::() + .map_err(|_| FailKind::BadMetadata)?; + if v > consts::MDATA_VER { + return Err(FailKind::VersionMismatch); + } + version = Some(v); + } + consts::ITER_COUNT_LINE_NAME => { + // This should never be zero! + iterations = Some( + items + .next() + .ok_or(FailKind::BadMetadata)? + .parse::() + .map_err(|_| FailKind::BadMetadata)? + .try_into() + .map_err(|_| FailKind::BadMetadata)?, + ); + } + consts::IMPORTANCE_LINE_NAME => { + importance = match items.next().ok_or(FailKind::BadMetadata)? { + "critical" => Importance::Critical, + "important" => Importance::Important, + "average" => Importance::Average, + "iffy" => Importance::Iffy, + "fluff" => Importance::Fluff, + _ => return Err(FailKind::BadMetadata), + }; + } + consts::WEIGHT_LINE_NAME => { + weight = items + .next() + .ok_or(FailKind::BadMetadata)? + .parse::() + .map_err(|_| FailKind::BadMetadata)?; + } + _ => unreachable!(), + } + } - Ok(TestMdata { - version: version.ok_or(FailKind::BadMetadata)?, - // Iterations may be determined by us and thus left unspecified. - iterations, - // In principle this should always be set, but just for the sake of - // stability allow the potentially-breaking change of not reporting the - // importance without erroring. Maybe we want to change this. - importance, - // Same with weight. - weight, - }) + Ok(TestMdata { + version: version.ok_or(FailKind::BadMetadata)?, + // Iterations may be determined by us and thus left unspecified. + iterations, + // In principle this should always be set, but just for the sake of + // stability allow the potentially-breaking change of not reporting the + // importance without erroring. Maybe we want to change this. + importance, + // Same with weight. + weight, + }) } /// Compares the perf results of two profiles as per the arguments passed in. fn compare_profiles(args: &[String]) { - let mut save_to = None; - let mut ident_idx = 0; - args.first().inspect(|a| { - if a.starts_with("--save") { - save_to = Some( - a.strip_prefix("--save=") - .expect("FATAL: save param formatted incorrectly"), - ); - ident_idx = 1; - } - }); - let ident_new = args - .get(ident_idx) - .expect("FATAL: missing identifier for new run"); - let ident_old = args - .get(ident_idx + 1) - .expect("FATAL: missing identifier for old run"); - let wspace_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let runs_dir = PathBuf::from(&wspace_dir) - .join("..") - .join("..") - .join(consts::RUNS_DIR); + let mut save_to = None; + let mut ident_idx = 0; + args.first().inspect(|a| { + if a.starts_with("--save") { + save_to = Some( + a.strip_prefix("--save=") + .expect("FATAL: save param formatted incorrectly"), + ); + ident_idx = 1; + } + }); + let ident_new = args + .get(ident_idx) + .expect("FATAL: missing identifier for new run"); + let ident_old = args + .get(ident_idx + 1) + .expect("FATAL: missing identifier for old run"); + let wspace_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let runs_dir = PathBuf::from(&wspace_dir) + .join("..") + .join("..") + .join(consts::RUNS_DIR); - // Use the blank outputs initially, so we can merge into these with prefixes. - let mut outputs_new = Output::blank(); - let mut outputs_old = Output::blank(); + // Use the blank outputs initially, so we can merge into these with prefixes. + let mut outputs_new = Output::blank(); + let mut outputs_old = Output::blank(); - for e in runs_dir.read_dir().unwrap() { - let Ok(entry) = e else { - continue; - }; - let Ok(metadata) = entry.metadata() else { - continue; - }; - if metadata.is_file() { - let Ok(name) = entry.file_name().into_string() else { - continue; - }; + for e in runs_dir.read_dir().unwrap() { + let Ok(entry) = e else { + continue; + }; + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_file() { + let Ok(name) = entry.file_name().into_string() else { + continue; + }; - // A little helper to avoid code duplication. Reads the `output` from - // a json file, then merges it into what we have so far. - let read_into = |output: &mut Output| { - let mut elems = name.split('.').skip(1); - let prefix = elems.next().unwrap(); - assert_eq!("json", elems.next().unwrap()); - assert!(elems.next().is_none()); - let mut buffer = Vec::new(); - let _ = OpenOptions::new() - .read(true) - .open(entry.path()) - .unwrap() - .read_to_end(&mut buffer) - .unwrap(); - let o_other: Output = serde_json::from_slice(&buffer).unwrap(); - output.merge(o_other, prefix); - }; + // A little helper to avoid code duplication. Reads the `output` from + // a json file, then merges it into what we have so far. + let read_into = |output: &mut Output| { + let mut elems = name.split('.').skip(1); + let prefix = elems.next().unwrap(); + assert_eq!("json", elems.next().unwrap()); + assert!(elems.next().is_none()); + let mut buffer = Vec::new(); + let _ = OpenOptions::new() + .read(true) + .open(entry.path()) + .unwrap() + .read_to_end(&mut buffer) + .unwrap(); + let o_other: Output = serde_json::from_slice(&buffer).unwrap(); + output.merge(o_other, prefix); + }; - if name.starts_with(ident_old) { - read_into(&mut outputs_old); - } else if name.starts_with(ident_new) { - read_into(&mut outputs_new); - } - } - } + if name.starts_with(ident_old) { + read_into(&mut outputs_old); + } else if name.starts_with(ident_new) { + read_into(&mut outputs_new); + } + } + } - let res = outputs_new.compare_perf(outputs_old); - if let Some(filename) = save_to { - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(filename) - .expect("FATAL: couldn't save run results to file"); - file.write_all(format!("{res}").as_bytes()).unwrap(); - } else { - println!("{res}"); - } + let res = outputs_new.compare_perf(outputs_old); + if let Some(filename) = save_to { + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(filename) + .expect("FATAL: couldn't save run results to file"); + file.write_all(format!("{res}").as_bytes()).unwrap(); + } else { + println!("{res}"); + } } /// Runs a test binary, filtering out tests which aren't marked for perf triage @@ -304,82 +304,82 @@ fn compare_profiles(args: &[String]) { /// /// The output of this is an iterator over `test_fn_name, test_mdata_name`. fn get_tests(t_bin: &str) -> impl ExactSizeIterator { - let mut cmd = Command::new(t_bin); - // --format=json is nightly-only :( - cmd.args(["--list", "--format=terse"]); - let out = cmd - .output() - .expect("FATAL: Could not run test binary {t_bin}"); - assert!( - out.status.success(), - "FATAL: Cannot do perf check - test binary {t_bin} returned an error" - ); - if !QUIET.load(Ordering::Relaxed) { - eprintln!("Test binary ran successfully; starting profile..."); - } - // Parse the test harness output to look for tests we care about. - let stdout = String::from_utf8_lossy(&out.stdout); - let mut test_list: Vec<_> = stdout - .lines() - .filter_map(|line| { - // This should split only in two; e.g., - // "app::test::test_arena: test" => "app::test::test_arena:", "test" - let line: Vec<_> = line.split_whitespace().collect(); - match line[..] { - // Final byte of t_name is ":", which we need to ignore. - [t_name, kind] => (kind == "test").then(|| &t_name[..t_name.len() - 1]), - _ => None, - } - }) - // Exclude tests that aren't marked for perf triage based on suffix. - .filter(|t_name| { - t_name.ends_with(consts::SUF_NORMAL) || t_name.ends_with(consts::SUF_MDATA) - }) - .collect(); + let mut cmd = Command::new(t_bin); + // --format=json is nightly-only :( + cmd.args(["--list", "--format=terse"]); + let out = cmd + .output() + .expect("FATAL: Could not run test binary {t_bin}"); + assert!( + out.status.success(), + "FATAL: Cannot do perf check - test binary {t_bin} returned an error" + ); + if !QUIET.load(Ordering::Relaxed) { + eprintln!("Test binary ran successfully; starting profile..."); + } + // Parse the test harness output to look for tests we care about. + let stdout = String::from_utf8_lossy(&out.stdout); + let mut test_list: Vec<_> = stdout + .lines() + .filter_map(|line| { + // This should split only in two; e.g., + // "app::test::test_arena: test" => "app::test::test_arena:", "test" + let line: Vec<_> = line.split_whitespace().collect(); + match line[..] { + // Final byte of t_name is ":", which we need to ignore. + [t_name, kind] => (kind == "test").then(|| &t_name[..t_name.len() - 1]), + _ => None, + } + }) + // Exclude tests that aren't marked for perf triage based on suffix. + .filter(|t_name| { + t_name.ends_with(consts::SUF_NORMAL) || t_name.ends_with(consts::SUF_MDATA) + }) + .collect(); - // Pulling itertools just for .dedup() would be quite a big dependency that's - // not used elsewhere, so do this on a vec instead. - test_list.sort_unstable(); - test_list.dedup(); + // Pulling itertools just for .dedup() would be quite a big dependency that's + // not used elsewhere, so do this on a vec instead. + test_list.sort_unstable(); + test_list.dedup(); - // Tests should come in pairs with their mdata fn! - assert!( - test_list.len().is_multiple_of(2), - "Malformed tests in test binary {t_bin}" - ); + // Tests should come in pairs with their mdata fn! + assert!( + test_list.len().is_multiple_of(2), + "Malformed tests in test binary {t_bin}" + ); - let out = test_list - .as_chunks_mut::<2>() - .0 - .iter_mut() - .map(|pair| { - // Be resilient against changes to these constants. - if consts::SUF_NORMAL < consts::SUF_MDATA { - (pair[0].to_owned(), pair[1].to_owned()) - } else { - (pair[1].to_owned(), pair[0].to_owned()) - } - }) - .collect::>(); - out.into_iter() + let out = test_list + .as_chunks_mut::<2>() + .0 + .iter_mut() + .map(|pair| { + // Be resilient against changes to these constants. + if consts::SUF_NORMAL < consts::SUF_MDATA { + (pair[0].to_owned(), pair[1].to_owned()) + } else { + (pair[1].to_owned(), pair[0].to_owned()) + } + }) + .collect::>(); + out.into_iter() } /// Runs the specified test `count` times, returning the time taken if the test /// succeeded. #[inline] fn spawn_and_iterate(t_bin: &str, t_name: &str, count: NonZero) -> Option { - let mut cmd = Command::new(t_bin); - cmd.args([t_name, "--exact"]); - cmd.env(consts::ITER_ENV_VAR, format!("{count}")); - // Don't let the child muck up our stdin/out/err. - cmd.stdin(Stdio::null()); - cmd.stdout(Stdio::null()); - cmd.stderr(Stdio::null()); - let pre = Instant::now(); - // Discard the output beyond ensuring success. - let out = cmd.spawn().unwrap().wait(); - let post = Instant::now(); - out.iter().find_map(|s| s.success().then_some(post - pre)) + let mut cmd = Command::new(t_bin); + cmd.args([t_name, "--exact"]); + cmd.env(consts::ITER_ENV_VAR, format!("{count}")); + // Don't let the child muck up our stdin/out/err. + cmd.stdin(Stdio::null()); + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + let pre = Instant::now(); + // Discard the output beyond ensuring success. + let out = cmd.spawn().unwrap().wait(); + let post = Instant::now(); + out.iter().find_map(|s| s.success().then_some(post - pre)) } /// Triage a test to determine the correct number of iterations that it should run. @@ -392,193 +392,193 @@ fn spawn_and_iterate(t_bin: &str, t_name: &str, count: NonZero) -> Option /// This will panic if `step(usize)` is not monotonically increasing, or if the test /// binary is invalid. fn triage_test( - t_bin: &str, - t_name: &str, - thresh: Duration, - mut step: impl FnMut(NonZero) -> Option>, + t_bin: &str, + t_name: &str, + thresh: Duration, + mut step: impl FnMut(NonZero) -> Option>, ) -> Option> { - let mut iter_count = DEFAULT_ITER_COUNT; - // It's possible that the first loop of a test might be an outlier (e.g. it's - // doing some caching), in which case we want to skip it. - let duration_once = spawn_and_iterate(t_bin, t_name, NonZero::new(1).unwrap())?; - loop { - let duration = spawn_and_iterate(t_bin, t_name, iter_count)?; - if duration.saturating_sub(duration_once) > thresh { - break Some(iter_count); - } - let new = step(iter_count)?; - assert!( - new > iter_count, - "FATAL: step must be monotonically increasing" - ); - iter_count = new; - } + let mut iter_count = DEFAULT_ITER_COUNT; + // It's possible that the first loop of a test might be an outlier (e.g. it's + // doing some caching), in which case we want to skip it. + let duration_once = spawn_and_iterate(t_bin, t_name, NonZero::new(1).unwrap())?; + loop { + let duration = spawn_and_iterate(t_bin, t_name, iter_count)?; + if duration.saturating_sub(duration_once) > thresh { + break Some(iter_count); + } + let new = step(iter_count)?; + assert!( + new > iter_count, + "FATAL: step must be monotonically increasing" + ); + iter_count = new; + } } /// Try to find the hyperfine binary the user has installed. fn hyp_binary() -> Option { - const HYP_PATH: &str = "hyperfine"; - const HYP_HOME: &str = "~/.cargo/bin/hyperfine"; - if Command::new(HYP_PATH).output().is_err() { - if Command::new(HYP_HOME).output().is_err() { - None - } else { - Some(Command::new(HYP_HOME)) - } - } else { - Some(Command::new(HYP_PATH)) - } + const HYP_PATH: &str = "hyperfine"; + const HYP_HOME: &str = "~/.cargo/bin/hyperfine"; + if Command::new(HYP_PATH).output().is_err() { + if Command::new(HYP_HOME).output().is_err() { + None + } else { + Some(Command::new(HYP_HOME)) + } + } else { + Some(Command::new(HYP_PATH)) + } } /// Profiles a given test with hyperfine, returning the mean and standard deviation /// for its runtime. If the test errors, returns `None` instead. fn hyp_profile(t_bin: &str, t_name: &str, iterations: NonZero) -> Option { - let mut perf_cmd = hyp_binary().expect("Couldn't find the Hyperfine binary on the system"); + let mut perf_cmd = hyp_binary().expect("Couldn't find the Hyperfine binary on the system"); - // Warm up the cache and print markdown output to stdout, which we parse. - perf_cmd.args([ - "--style", - "none", - "--warmup", - "1", - "--export-markdown", - "-", - // Parse json instead... - "--time-unit", - "millisecond", - &format!("{t_bin} --exact {t_name}"), - ]); - perf_cmd.env(consts::ITER_ENV_VAR, format!("{iterations}")); - let p_out = perf_cmd.output().unwrap(); - if !p_out.status.success() { - return None; - } + // Warm up the cache and print markdown output to stdout, which we parse. + perf_cmd.args([ + "--style", + "none", + "--warmup", + "1", + "--export-markdown", + "-", + // Parse json instead... + "--time-unit", + "millisecond", + &format!("{t_bin} --exact {t_name}"), + ]); + perf_cmd.env(consts::ITER_ENV_VAR, format!("{iterations}")); + let p_out = perf_cmd.output().unwrap(); + if !p_out.status.success() { + return None; + } - let cmd_output = String::from_utf8_lossy(&p_out.stdout); - // Can't use .last() since we have a trailing newline. Sigh. - let results_line = cmd_output.lines().nth(3).unwrap(); - // Grab the values out of the pretty-print. - // TODO: Parse json instead. - let mut res_iter = results_line.split_whitespace(); - // Durations are given in milliseconds, so account for that. - let mean = Duration::from_secs_f64(res_iter.nth(5).unwrap().parse::().unwrap() / 1000.); - let stddev = Duration::from_secs_f64(res_iter.nth(1).unwrap().parse::().unwrap() / 1000.); + let cmd_output = String::from_utf8_lossy(&p_out.stdout); + // Can't use .last() since we have a trailing newline. Sigh. + let results_line = cmd_output.lines().nth(3).unwrap(); + // Grab the values out of the pretty-print. + // TODO: Parse json instead. + let mut res_iter = results_line.split_whitespace(); + // Durations are given in milliseconds, so account for that. + let mean = Duration::from_secs_f64(res_iter.nth(5).unwrap().parse::().unwrap() / 1000.); + let stddev = Duration::from_secs_f64(res_iter.nth(1).unwrap().parse::().unwrap() / 1000.); - Some(Timings { mean, stddev }) + Some(Timings { mean, stddev }) } fn main() { - let args = std::env::args().collect::>(); - // We get passed the test we need to run as the 1st argument after our own name. - let t_bin = args - .get(1) - .expect("FATAL: No test binary or command; this shouldn't be manually invoked!"); + let args = std::env::args().collect::>(); + // We get passed the test we need to run as the 1st argument after our own name. + let t_bin = args + .get(1) + .expect("FATAL: No test binary or command; this shouldn't be manually invoked!"); - // We're being asked to compare two results, not run the profiler. - if t_bin == "compare" { - compare_profiles(&args[2..]); - return; - } + // We're being asked to compare two results, not run the profiler. + if t_bin == "compare" { + compare_profiles(&args[2..]); + return; + } - // Minimum test importance we care about this run. - let mut thresh = Importance::Iffy; - // Where to print the output of this run. - let mut out_kind = OutputKind::Markdown; + // Minimum test importance we care about this run. + let mut thresh = Importance::Iffy; + // Where to print the output of this run. + let mut out_kind = OutputKind::Markdown; - for arg in args.iter().skip(2) { - match arg.as_str() { - "--critical" => thresh = Importance::Critical, - "--important" => thresh = Importance::Important, - "--average" => thresh = Importance::Average, - "--iffy" => thresh = Importance::Iffy, - "--fluff" => thresh = Importance::Fluff, - "--quiet" => QUIET.store(true, Ordering::Relaxed), - s if s.starts_with("--json") => { - out_kind = OutputKind::Json(Path::new( - s.strip_prefix("--json=") - .expect("FATAL: Invalid json parameter; pass --json=ident"), - )); - } - _ => (), - } - } - if !QUIET.load(Ordering::Relaxed) { - eprintln!("Starting perf check"); - } + for arg in args.iter().skip(2) { + match arg.as_str() { + "--critical" => thresh = Importance::Critical, + "--important" => thresh = Importance::Important, + "--average" => thresh = Importance::Average, + "--iffy" => thresh = Importance::Iffy, + "--fluff" => thresh = Importance::Fluff, + "--quiet" => QUIET.store(true, Ordering::Relaxed), + s if s.starts_with("--json") => { + out_kind = OutputKind::Json(Path::new( + s.strip_prefix("--json=") + .expect("FATAL: Invalid json parameter; pass --json=ident"), + )); + } + _ => (), + } + } + if !QUIET.load(Ordering::Relaxed) { + eprintln!("Starting perf check"); + } - let mut output = Output::default(); + let mut output = Output::default(); - // Spawn and profile an instance of each perf-sensitive test, via hyperfine. - // Each test is a pair of (test, metadata-returning-fn), so grab both. We also - // know the list is sorted. - let i = get_tests(t_bin); - let len = i.len(); - for (idx, (ref t_name, ref t_mdata)) in i.enumerate() { - if !QUIET.load(Ordering::Relaxed) { - eprint!("\rProfiling test {}/{}", idx + 1, len); - } - // Pretty-printable stripped name for the test. - let t_name_pretty = t_name.replace(consts::SUF_NORMAL, ""); + // Spawn and profile an instance of each perf-sensitive test, via hyperfine. + // Each test is a pair of (test, metadata-returning-fn), so grab both. We also + // know the list is sorted. + let i = get_tests(t_bin); + let len = i.len(); + for (idx, (ref t_name, ref t_mdata)) in i.enumerate() { + if !QUIET.load(Ordering::Relaxed) { + eprint!("\rProfiling test {}/{}", idx + 1, len); + } + // Pretty-printable stripped name for the test. + let t_name_pretty = t_name.replace(consts::SUF_NORMAL, ""); - // Get the metadata this test reports for us. - let t_mdata = match parse_mdata(t_bin, t_mdata) { - Ok(mdata) => mdata, - Err(err) => fail!(output, t_name_pretty, err), - }; + // Get the metadata this test reports for us. + let t_mdata = match parse_mdata(t_bin, t_mdata) { + Ok(mdata) => mdata, + Err(err) => fail!(output, t_name_pretty, err), + }; - if t_mdata.importance < thresh { - fail!(output, t_name_pretty, t_mdata, FailKind::Skipped); - } + if t_mdata.importance < thresh { + fail!(output, t_name_pretty, t_mdata, FailKind::Skipped); + } - // Time test execution to see how many iterations we need to do in order - // to account for random noise. This is skipped for tests with fixed - // iteration counts. - let final_iter_count = t_mdata.iterations.or_else(|| { - triage_test(t_bin, t_name, consts::NOISE_CUTOFF, |c| { - if let Some(c) = c.checked_mul(ITER_COUNT_MUL) { - Some(c) - } else { - // This should almost never happen, but maybe..? - eprintln!( - "WARNING: Ran nearly usize::MAX iterations of test {t_name_pretty}; skipping" - ); - None - } - }) - }); + // Time test execution to see how many iterations we need to do in order + // to account for random noise. This is skipped for tests with fixed + // iteration counts. + let final_iter_count = t_mdata.iterations.or_else(|| { + triage_test(t_bin, t_name, consts::NOISE_CUTOFF, |c| { + if let Some(c) = c.checked_mul(ITER_COUNT_MUL) { + Some(c) + } else { + // This should almost never happen, but maybe..? + eprintln!( + "WARNING: Ran nearly usize::MAX iterations of test {t_name_pretty}; skipping" + ); + None + } + }) + }); - // Don't profile failing tests. - let Some(final_iter_count) = final_iter_count else { - fail!(output, t_name_pretty, t_mdata, FailKind::Triage); - }; + // Don't profile failing tests. + let Some(final_iter_count) = final_iter_count else { + fail!(output, t_name_pretty, t_mdata, FailKind::Triage); + }; - // Now profile! - if let Some(timings) = hyp_profile(t_bin, t_name, final_iter_count) { - output.success(t_name_pretty, t_mdata, final_iter_count, timings); - } else { - fail!( - output, - t_name_pretty, - t_mdata, - final_iter_count, - FailKind::Profile - ); - } - } - if !QUIET.load(Ordering::Relaxed) { - if output.is_empty() { - eprintln!("Nothing to do."); - } else { - // If stdout and stderr are on the same terminal, move us after the - // output from above. - eprintln!(); - } - } + // Now profile! + if let Some(timings) = hyp_profile(t_bin, t_name, final_iter_count) { + output.success(t_name_pretty, t_mdata, final_iter_count, timings); + } else { + fail!( + output, + t_name_pretty, + t_mdata, + final_iter_count, + FailKind::Profile + ); + } + } + if !QUIET.load(Ordering::Relaxed) { + if output.is_empty() { + eprintln!("Nothing to do."); + } else { + // If stdout and stderr are on the same terminal, move us after the + // output from above. + eprintln!(); + } + } - // No need making an empty json file on every empty test bin. - if output.is_empty() { - return; - } + // No need making an empty json file on every empty test bin. + if output.is_empty() { + return; + } - out_kind.log(&output, t_bin); + out_kind.log(&output, t_bin); }