Misc · Web: a web view proven with wry, and an iframe embed
misc::WebView lays wry 0.57's child WKWebView over the window at its box, following it from a canvas in prepaint and hiding it out of view; it loads an address or the owner's HTML, which makes it the HTML preview. misc::IframeEmbed frames a page under its address with Open in browser. EmbedCard points to chat::LinkPreviewCard, ShareSheet to buttons::ShareButton; PrintButton is blocked, gpui 0.2.2 has no print API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
45e128e069
commit
529394e57b
8 files changed
+992
-17
No files matched your search
@@ -10,6 +10,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money).
|
- Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money).
|
||||||
- Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. `rqrr` 0.11 ((MIT OR Apache-2.0) AND ISC, default features off) reads QR codes from the host's frames.
|
- Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. `rqrr` 0.11 ((MIT OR Apache-2.0) AND ISC, default features off) reads QR codes from the host's frames.
|
||||||
- macOS extras (tray icon, Dock badge) call AppKit through `cocoa` 0.26 and `objc` 0.2, the crates gpui already links.
|
- macOS extras (tray icon, Dock badge) call AppKit through `cocoa` 0.26 and `objc` 0.2, the crates gpui already links.
|
||||||
|
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
|
||||||
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off) for the grid, its parser and the pseudo-terminal; `futures` carries its events.
|
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off) for the grid, its parser and the pseudo-terminal; `futures` carries its events.
|
||||||
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
||||||
- Markdown: `pulldown-cmark` 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math.
|
- Markdown: `pulldown-cmark` 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math.
|
||||||
@@ -216,3 +217,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- 2026-09-26: Terminal proven with `alacritty_terminal` 0.26: its grid, parser and pseudo-terminal, drawn by Ely. A replayed grid serves recorded output and tests.
|
- 2026-09-26: Terminal proven with `alacritty_terminal` 0.26: its grid, parser and pseudo-terminal, drawn by Ely. A replayed grid serves recorded output and tests.
|
||||||
- 2026-09-27: ThemeSwitcher is `settings::ThemeSelector`: `src/theme` sits under every chapter and cannot draw with buttons.
|
- 2026-09-27: ThemeSwitcher is `settings::ThemeSelector`: `src/theme` sits under every chapter and cannot draw with buttons.
|
||||||
- 2026-09-28: QRCodeScanner proven with `rqrr` 0.11: the host hands in frames as `RenderImage`s, read off the main thread; gpui 0.2.2 has no camera.
|
- 2026-09-28: QRCodeScanner proven with `rqrr` 0.11: the host hands in frames as `RenderImage`s, read off the main thread; gpui 0.2.2 has no camera.
|
||||||
|
- 2026-09-28: WebView proven with `wry` 0.57: a child WKWebView over the window, placed from a canvas in prepaint at its box each frame and hidden once the box leaves the view. It draws above everything gpui paints and gpui's clipping does not reach it; gpui's test windows have no native handle, so the capture proves it.
|
||||||
Generated
+695
-11
File diff suppressed because it is too large.
Load diff
@@ -42,6 +42,7 @@ block = "0.1"
|
|||||||
cocoa = "0.26"
|
cocoa = "0.26"
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
wry = { version = "0.57", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
rustix = { version = "1", default-features = false, features = ["std", "fs"] }
|
rustix = { version = "1", default-features = false, features = ["std", "fs"] }
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ use crate::ui::{section, specimen, specimens};
|
|||||||
mod asking;
|
mod asking;
|
||||||
mod codes;
|
mod codes;
|
||||||
mod notices;
|
mod notices;
|
||||||
|
mod web;
|
||||||
|
|
||||||
pub const PAGE: Page = Page {
|
pub const PAGE: Page = Page {
|
||||||
number: 42,
|
number: 42,
|
||||||
slug: "misc",
|
slug: "misc",
|
||||||
title: "Misc",
|
title: "Misc",
|
||||||
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha, a poll, a survey, a quiz, flashcards, licenses, terms and a cookie banner.",
|
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha, a poll, a survey, a quiz, flashcards, licenses, terms, a cookie banner and web views.",
|
||||||
render,
|
render,
|
||||||
script: &[
|
script: &[
|
||||||
Step::DownAt("stopwatch-start", 30.0, 62.0),
|
Step::DownAt("stopwatch-start", 30.0, 62.0),
|
||||||
@@ -81,6 +82,7 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
|||||||
.child(codes::render(window, cx))
|
.child(codes::render(window, cx))
|
||||||
.child(asking::render(window, cx))
|
.child(asking::render(window, cx))
|
||||||
.child(notices::render(window, cx))
|
.child(notices::render(window, cx))
|
||||||
|
.child(web::render(cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use ely_gpui_component::misc::{IframeEmbed, WebSource, WebView};
|
||||||
|
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
|
||||||
|
|
||||||
|
use crate::ui::{blocked, section};
|
||||||
|
|
||||||
|
const PAGE: &str = r#"<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark; font: 15px -apple-system, sans-serif; }
|
||||||
|
body { margin: 0; height: 100vh; display: grid; place-items: center;
|
||||||
|
background: linear-gradient(135deg, #5b8cff33, #b35bff33); }
|
||||||
|
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||||
|
p { margin: 0; opacity: .7; }
|
||||||
|
</style>
|
||||||
|
<main>
|
||||||
|
<h1>Hello from WebKit</h1>
|
||||||
|
<p>HTML the owner hands in, drawn by a native web view.</p>
|
||||||
|
</main>"#;
|
||||||
|
|
||||||
|
pub fn render(cx: &App) -> impl IntoElement + use<> {
|
||||||
|
div()
|
||||||
|
.child(
|
||||||
|
section(
|
||||||
|
"WebView / HTMLPreview",
|
||||||
|
"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(
|
||||||
|
div()
|
||||||
|
.w(px(480.0))
|
||||||
|
.h(px(200.0))
|
||||||
|
.child(WebView::new("web", WebSource::Html(PAGE.into()))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
section(
|
||||||
|
"IframeEmbed / EmbedCard",
|
||||||
|
"A page from another address in a frame of its own, with Open in browser. EmbedCard is chat::LinkPreviewCard, shown on the AI Chat page.",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.child(div().w(px(480.0)).child(IframeEmbed::new(
|
||||||
|
"iframe",
|
||||||
|
"https://example.com",
|
||||||
|
16.0 / 9.0,
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
section(
|
||||||
|
"PrintButton · ShareSheet",
|
||||||
|
"ShareSheet is buttons::ShareButton, shown on the Buttons & Actions page.",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.child(blocked(
|
||||||
|
"PrintButton: gpui 0.2.2 has no print API.",
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ mod survey;
|
|||||||
#[cfg(all(test, feature = "test-support"))]
|
#[cfg(all(test, feature = "test-support"))]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod units;
|
mod units;
|
||||||
|
mod web;
|
||||||
|
|
||||||
pub use calculator::Calculator;
|
pub use calculator::Calculator;
|
||||||
pub use captcha::{Captcha, CaptchaState};
|
pub use captcha::{Captcha, CaptchaState};
|
||||||
@@ -27,3 +28,4 @@ pub use scanner::QrCodeScanner;
|
|||||||
pub use stopwatch::Stopwatch;
|
pub use stopwatch::Stopwatch;
|
||||||
pub use survey::{Answer, Question, Survey};
|
pub use survey::{Answer, Question, Survey};
|
||||||
pub use units::UnitConverter;
|
pub use units::UnitConverter;
|
||||||
|
pub use web::{IframeEmbed, WebSource, WebView};
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
use gpui::{
|
||||||
|
AnyElement, App, Bounds, ElementId, IntoElement, ParentElement, Pixels, RenderOnce,
|
||||||
|
SharedString, Styled, Window, div,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
buttons::IconButton,
|
||||||
|
primitives::{Icon, IconName, checked_ratio, framed},
|
||||||
|
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||||
|
typography::{Ellipsis, literal},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// What a web view shows: a page at an address, or the owner's HTML.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum WebSource {
|
||||||
|
Url(SharedString),
|
||||||
|
Html(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a box lies, at least in part, inside what the window shows of its region.
|
||||||
|
pub(crate) fn in_view(bounds: Bounds<Pixels>, shown: Bounds<Pixels>) -> 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.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WebView {
|
||||||
|
id: ElementId,
|
||||||
|
source: WebSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebView {
|
||||||
|
pub fn new(id: impl Into<ElementId>, source: WebSource) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WebView {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return mac::page(self.id, self.source, window, cx);
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let _ = window;
|
||||||
|
let theme = cx.theme();
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_color(theme.colors.fg_muted)
|
||||||
|
.child("Web views need macOS for now.")
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod mac {
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, Bounds, ElementId, IntoElement, ParentElement, Pixels, Styled, Window,
|
||||||
|
canvas, div,
|
||||||
|
};
|
||||||
|
use wry::{
|
||||||
|
Rect, WebViewBuilder,
|
||||||
|
dpi::{LogicalPosition, LogicalSize},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WebSource, in_view};
|
||||||
|
|
||||||
|
/// The native view and what it shows.
|
||||||
|
struct Page {
|
||||||
|
view: wry::WebView,
|
||||||
|
shown: WebSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect(bounds: Bounds<Pixels>) -> Rect {
|
||||||
|
Rect {
|
||||||
|
position: LogicalPosition::new(f32::from(bounds.origin.x), f32::from(bounds.origin.y))
|
||||||
|
.into(),
|
||||||
|
size: LogicalSize::new(f32::from(bounds.size.width), f32::from(bounds.size.height))
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn page(
|
||||||
|
id: ElementId,
|
||||||
|
source: WebSource,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> AnyElement {
|
||||||
|
let page = window.use_keyed_state((id.clone(), "page"), cx, {
|
||||||
|
let (id, source) = (id.clone(), source.clone());
|
||||||
|
move |window, _| {
|
||||||
|
let builder = match &source {
|
||||||
|
WebSource::Url(url) => WebViewBuilder::new().with_url(url.to_string()),
|
||||||
|
WebSource::Html(html) => WebViewBuilder::new().with_html(html.to_string()),
|
||||||
|
};
|
||||||
|
let view = builder
|
||||||
|
.build_as_child(&*window)
|
||||||
|
.unwrap_or_else(|error| panic!("web view {id:?}: wry built no view: {error}"));
|
||||||
|
log::info!("web view {id:?}: built");
|
||||||
|
Page {
|
||||||
|
view,
|
||||||
|
shown: source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if page.read(cx).shown != source {
|
||||||
|
log::info!("web view {id:?}: loads anew");
|
||||||
|
let loaded = match &source {
|
||||||
|
WebSource::Url(url) => page.read(cx).view.load_url(url),
|
||||||
|
WebSource::Html(html) => page.read(cx).view.load_html(html),
|
||||||
|
};
|
||||||
|
loaded.unwrap_or_else(|error| panic!("web view {id:?}: load failed: {error}"));
|
||||||
|
page.update(cx, |page, _| page.shown = source);
|
||||||
|
}
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
move |bounds, window, cx| {
|
||||||
|
let view = &page.read(cx).view;
|
||||||
|
view.set_bounds(rect(bounds))
|
||||||
|
.unwrap_or_else(|error| panic!("web view {id:?}: bounds: {error}"));
|
||||||
|
view.set_visible(in_view(bounds, window.content_mask().bounds))
|
||||||
|
.unwrap_or_else(|error| panic!("web view {id:?}: visibility: {error}"));
|
||||||
|
},
|
||||||
|
|_, _, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page from another address in a frame of its own: the address over the page, with Open in browser. The page is a `WebView` in the frame's shape.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct IframeEmbed {
|
||||||
|
id: ElementId,
|
||||||
|
url: SharedString,
|
||||||
|
ratio: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IframeEmbed {
|
||||||
|
/// `ratio` is the frame's width over its height.
|
||||||
|
pub fn new(id: impl Into<ElementId>, url: impl Into<SharedString>, ratio: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
url: url.into(),
|
||||||
|
ratio: checked_ratio(ratio),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for IframeEmbed {
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let colors = &theme.colors;
|
||||||
|
let (id, url) = (self.id, self.url);
|
||||||
|
let opened = url.clone();
|
||||||
|
let frame: AnyElement = framed(self.ratio, cx)
|
||||||
|
.child(WebView::new(
|
||||||
|
(id.clone(), "page"),
|
||||||
|
WebSource::Url(url.clone()),
|
||||||
|
))
|
||||||
|
.into_any_element();
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.rounded(theme.radius(Radius::Lg))
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.bg(colors.surface)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_3()
|
||||||
|
.py_1()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
|
.text_color(colors.fg_muted)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Globe)
|
||||||
|
.size(IconSize::Sm)
|
||||||
|
.color(colors.fg_subtle),
|
||||||
|
)
|
||||||
|
.child(literal(div()).flex_1().min_w_0().child(Ellipsis::new(url)))
|
||||||
|
.child(
|
||||||
|
IconButton::new((id, "open"), IconName::ExternalLink)
|
||||||
|
.tooltip("Open in browser")
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
log::info!("iframe embed: opened {opened}");
|
||||||
|
cx.open_url(&opened)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use gpui::{Bounds, point, px, size};
|
||||||
|
|
||||||
|
use super::in_view;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_box_is_in_view_while_it_meets_what_the_window_shows() {
|
||||||
|
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(320.0), shown), "below the view");
|
||||||
|
assert!(!in_view(at(-120.0), shown), "above it");
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -340,11 +340,11 @@
|
|||||||
|
|
||||||
## T42f Misc · Web
|
## T42f Misc · Web
|
||||||
|
|
||||||
- [ ] WebView — prove: `wry`
|
- [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
|
||||||
- [ ] IframeEmbed / EmbedCard
|
- [x] IframeEmbed / EmbedCard → `misc::IframeEmbed`, a page in a frame of its own under its address, with Open in browser; EmbedCard is `chat::LinkPreviewCard`
|
||||||
- [ ] HTMLPreview → `misc::WebView`
|
- [x] HTMLPreview → `misc::WebView`
|
||||||
- [ ] PrintButton — blocked: gpui 0.2.2 has no print API
|
- [x] PrintButton — blocked: gpui 0.2.2 has no print API
|
||||||
- [ ] ShareSheet
|
- [x] ShareSheet → `buttons::ShareButton`
|
||||||
|
|
||||||
## T42g Misc · Export and import
|
## T42g Misc · Export and import
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user