diff --git a/AGENTS.md b/AGENTS.md index c7f67f1..d3b9803 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - A wall clock reads `Timestamp::now` and wakes its view at each whole second through a keyed task, only while the owner passes no moment; a stopwatch reads the executor's clock, so tests advance it (`misc::Clock`, `misc::Stopwatch`). - A number shown to so many significant digits goes through `typography::format::significant`, which rounds through Rust's exponent form, so no binary noise shows at any size (`misc::Calculator`, `misc::UnitConverter`). - A component that reads the host's frames reads one at a time off the main thread and passes over frames that come meanwhile; when a read ends it redraws, so the newest frame is read next (`misc::QrCodeScanner`). +- A native view laid over the window takes AppKit's first responder on a press, and gpui's view never asks for it back, so typed text would go to the native view for good. The component hands the keyboard back on every press gpui gets and when the view leaves (`misc::WebView`, through wry's `focus_parent`); `Step::NativeKey` proves it, since gpui's own dispatch skips AppKit. - A map keeps places in `f64`: at zoom 19 the world is 134 million pixels across. Its wheel pans and Command or Control with the wheel zooms about the pointer, as the canvas's does. Its tiles stay in gpui's asset cache, as every `img()` picture does. Its popup is an overlay that is not modal: it takes focus when it shows, and the frame it stops showing, by any way out, hands focus back. Its layers, lines and areas unwrap across the date line and repeat on every world in view. Clustered pins gather by squares of the world, not of the view, so a pan never regroups them; a zoom may, so it hands a focused pin's or cluster's focus to the map. ## Decisions diff --git a/examples/gallery/pages/misc/mod.rs b/examples/gallery/pages/misc/mod.rs index f1c807a..524f765 100644 --- a/examples/gallery/pages/misc/mod.rs +++ b/examples/gallery/pages/misc/mod.rs @@ -66,6 +66,13 @@ pub const PAGE: Page = Page { Step::UpAt("cookies", 230.0, 84.0), Step::HoverAt("cookies", 20.0, 20.0), Step::Shot("cookies-choosing"), + Step::DownAt("web", 240.0, 100.0), + Step::UpAt("web", 240.0, 100.0), + Step::DownAt("converter", 24.0, 60.0), + Step::UpAt("converter", 24.0, 60.0), + Step::Key("cmd-a"), + Step::NativeKey("5"), + Step::Shot("keys-back"), ], }; diff --git a/examples/gallery/pages/misc/web.rs b/examples/gallery/pages/misc/web.rs index 452c449..181e586 100644 --- a/examples/gallery/pages/misc/web.rs +++ b/examples/gallery/pages/misc/web.rs @@ -1,6 +1,7 @@ use ely_gpui_component::misc::{IframeEmbed, WebSource, WebView}; use gpui::{App, IntoElement, ParentElement, Styled, div, px}; +use crate::probe::probe; use crate::ui::{blocked, section}; const PAGE: &str = r#" @@ -25,12 +26,13 @@ pub fn render(cx: &App) -> impl IntoElement + use<> { "A native web view laid over its box: an address, or HTML the owner hands in, which makes it an HTML preview. It draws above everything gpui paints and follows the box each frame.", cx, ) - .child( + .child(probe( + "web", div() .w(px(480.0)) .h(px(200.0)) .child(WebView::new("web", WebSource::Html(PAGE.into()))), - ), + )), ) .child( section( diff --git a/examples/gallery/script.rs b/examples/gallery/script.rs index 5404008..a0d90f4 100644 --- a/examples/gallery/script.rs +++ b/examples/gallery/script.rs @@ -46,7 +46,7 @@ pub enum Step { ExpectClosed(&'static str), /// Photographs this app's frontmost popup, such as the share picker. ShotPopup(&'static str), - /// Posts a key to AppKit itself; only `escape` is known. + /// Posts a key down to AppKit itself; `escape` and `5` are known. NativeKey(&'static str), /// Cancels the open file panel, whose content runs out of process. CancelPanel, @@ -210,9 +210,13 @@ pub async fn play( log::info!("script: wrote {}", file.display()); } Step::NativeKey(key) => { - anyhow::ensure!(key == "escape", "native key {key} is not known"); + let (text, code) = match key { + "escape" => ("\u{1b}", 53), + "5" => ("5", 23), + other => anyhow::bail!("native key {other} is not known"), + }; let number = window.update(cx, |_, window, _| number(window))??; - post_key(number, true, "\u{1b}", 53)?; + post_key(number, true, text, code)?; } Step::CancelPanel => cancel_panel()?, Step::ExpectClosed(key) => { diff --git a/src/misc/web.rs b/src/misc/web.rs index f6f82f7..caf6627 100644 --- a/src/misc/web.rs +++ b/src/misc/web.rs @@ -18,11 +18,12 @@ pub enum WebSource { } /// Whether a box lies, at least in part, inside what the window shows of its region. +#[cfg(any(target_os = "macos", test))] pub(crate) fn in_view(bounds: Bounds, shown: Bounds) -> bool { bounds.intersects(&shown) } -/// A web page in a native web view laid over this box: an address, or the owner's HTML. The page draws above everything gpui paints, dialogs too, and gpui's clipping does not reach it; it follows the box each frame and hides while the box is out of view. macOS only; elsewhere the box says so. +/// A web page in a native web view laid over this box: an address, or the owner's HTML. The page draws above everything gpui paints, dialogs too, and gpui's clipping does not reach it; it follows the box each frame and hides while the box is out of view. A press on the page gives it the keys; a press anywhere gpui draws, or the view leaving, gives them back. macOS only; elsewhere the box says so. #[derive(IntoElement)] pub struct WebView { id: ElementId, @@ -61,8 +62,8 @@ impl RenderOnce for WebView { #[cfg(target_os = "macos")] mod mac { use gpui::{ - AnyElement, App, Bounds, ElementId, IntoElement, ParentElement, Pixels, Styled, Window, - canvas, div, + AnyElement, App, Bounds, DispatchPhase, ElementId, IntoElement, MouseDownEvent, + ParentElement, Pixels, Styled, Window, canvas, div, }; use wry::{ Rect, WebViewBuilder, @@ -77,6 +78,14 @@ mod mac { shown: WebSource, } + impl Drop for Page { + fn drop(&mut self) { + if let Err(error) = self.view.focus_parent() { + log::error!("web view: keys stay with the page as it goes: {error}"); + } + } + } + fn rect(bounds: Bounds) -> Rect { Rect { position: LogicalPosition::new(f32::from(bounds.origin.x), f32::from(bounds.origin.y)) @@ -118,6 +127,7 @@ mod mac { loaded.unwrap_or_else(|error| panic!("web view {id:?}: load failed: {error}")); page.update(cx, |page, _| page.shown = source); } + let pressed = page.clone(); div() .relative() .size_full() @@ -130,7 +140,20 @@ mod mac { view.set_visible(in_view(bounds, window.content_mask().bounds)) .unwrap_or_else(|error| panic!("web view {id:?}: visibility: {error}")); }, - |_, _, _, _| {}, + move |_, _, window, _| { + window.on_mouse_event(move |_: &MouseDownEvent, phase, _, cx| { + if phase == DispatchPhase::Capture { + log::debug!("web view: keys go back to gpui"); + pressed + .read(cx) + .view + .focus_parent() + .unwrap_or_else(|error| { + panic!("web view: keys stay with the page: {error}") + }); + } + }); + }, ) .absolute() .top_0() @@ -167,6 +190,7 @@ impl RenderOnce for IframeEmbed { let (id, url) = (self.id, self.url); let opened = url.clone(); let frame: AnyElement = framed(self.ratio, cx) + .rounded_b(theme.radius(Radius::Lg)) .child(WebView::new( (id.clone(), "page"), WebSource::Url(url.clone()), @@ -220,6 +244,10 @@ mod tests { let shown = Bounds::new(point(px(0.0), px(0.0)), size(px(400.0), px(300.0))); let at = |y: f32| Bounds::new(point(px(20.0), px(y)), size(px(200.0), px(100.0))); assert!(in_view(at(250.0), shown), "part of it shows"); + assert!( + in_view(at(-50.0), shown), + "its top lies above the view, its body in it" + ); assert!(!in_view(at(320.0), shown), "below the view"); assert!(!in_view(at(-120.0), shown), "above it"); } diff --git a/tasks/ch31-43.md b/tasks/ch31-43.md index 63a7b4b..08d16e1 100644 --- a/tasks/ch31-43.md +++ b/tasks/ch31-43.md @@ -340,7 +340,7 @@ ## T42f Misc · Web -- [x] WebView — proven: `wry` 0.57 lays a child WKWebView over the window → `misc::WebView`, an address or the owner's HTML, following its box each frame and hidden out of view; it draws above gpui and ignores its clipping. macOS only +- [x] WebView — proven: `wry` 0.57 lays a child WKWebView over the window → `misc::WebView`, an address or the owner's HTML, following its box each frame and hidden out of view; it draws above gpui and ignores its clipping, and a press anywhere gpui draws, or the view leaving, hands the keyboard back. macOS only - [x] IframeEmbed / EmbedCard → `misc::IframeEmbed`, a page in a frame of its own under its address, with Open in browser; EmbedCard is `chat::LinkPreviewCard` - [x] HTMLPreview → `misc::WebView` - [x] PrintButton — blocked: gpui 0.2.2 has no print API