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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-28 04:32:26 -04:00
1 parent 45e128e069
commit 529394e57b
8 files changed
+992 -17

No files matched your search

+2
View File
@@ -13,6 +13,7 @@ mod survey;
#[cfg(all(test, feature = "test-support"))]
mod tests;
mod units;
mod web;
pub use calculator::Calculator;
pub use captcha::{Captcha, CaptchaState};
@@ -27,3 +28,4 @@ pub use scanner::QrCodeScanner;
pub use stopwatch::Stopwatch;
pub use survey::{Answer, Question, Survey};
pub use units::UnitConverter;
pub use web::{IframeEmbed, WebSource, WebView};
+226
View File
@@ -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");
}
}