Add WebView (#2)
- https://github.com/huacnlee/zed/pull/6 - https://github.com/zed-industries/zed/pull/13730
This commit is contained in:
parent
91e123e942
commit
f6146220e4
14 changed files with 998 additions and 83 deletions
710
Cargo.lock
generated
710
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,8 +5,7 @@ default-members = ["crates/app"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
# gpui = { path = "/Users/jason/github/zed/crates/gpui" }
|
||||
gpui = { git = "https://github.com/zed-industries/zed.git" }
|
||||
gpui = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform-window" }
|
||||
ui = { path = "crates/ui" }
|
||||
story = { path = "crates/story" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
|
|
|
|||
4
assets/icons/arrow-left.svg
Normal file
4
assets/icons/arrow-left.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left">
|
||||
<path d="m12 19-7-7 7-7"/>
|
||||
<path d="M19 12H5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
4
assets/icons/arrow-right.svg
Normal file
4
assets/icons/arrow-right.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
3
assets/icons/chevron-left.svg
Normal file
3
assets/icons/chevron-left.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left">
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
3
assets/icons/chevron-right.svg
Normal file
3
assets/icons/chevron-right.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right">
|
||||
<path d="m9 18 6-6-6-6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
|
|
@ -3,7 +3,7 @@ use prelude::FluentBuilder as _;
|
|||
use story::{
|
||||
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
|
||||
PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory,
|
||||
TableStory, TooltipStory,
|
||||
TableStory, TooltipStory, WebViewStory,
|
||||
};
|
||||
use workspace::{dock::DockPosition, TitleBar, Workspace};
|
||||
|
||||
|
|
@ -50,21 +50,23 @@ impl StoryWorkspace {
|
|||
)
|
||||
.detach();
|
||||
|
||||
StoryContainer::add_panel(
|
||||
StoryContainer::add_pane(
|
||||
"Input",
|
||||
"A control that allows the user to input text.",
|
||||
InputStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
DockPosition::Right,
|
||||
px(350.0),
|
||||
cx,
|
||||
);
|
||||
)
|
||||
.detach();
|
||||
|
||||
StoryContainer::add_panel(
|
||||
StoryContainer::add_pane(
|
||||
"Checkbox",
|
||||
"A control that allows the user to toggle between checked and not checked.",
|
||||
CheckboxStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
DockPosition::Bottom,
|
||||
px(200.),
|
||||
cx,
|
||||
);
|
||||
)
|
||||
.detach();
|
||||
|
||||
StoryContainer::add_pane(
|
||||
"Switch",
|
||||
|
|
@ -128,6 +130,14 @@ impl StoryWorkspace {
|
|||
)
|
||||
.detach();
|
||||
|
||||
StoryContainer::add_panel(
|
||||
WebViewStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
DockPosition::Right,
|
||||
px(450.),
|
||||
cx,
|
||||
);
|
||||
|
||||
StoryContainer::add_pane(
|
||||
"Table",
|
||||
"Powerful table and datagrids built.",
|
||||
|
|
|
|||
|
|
@ -12,3 +12,7 @@ anyhow = "1"
|
|||
workspace.workspace = true
|
||||
charts-rs = "0.3"
|
||||
image = "0.25.1"
|
||||
wry = "0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod scrollable_story;
|
|||
mod switch_story;
|
||||
mod table_story;
|
||||
mod tooltip_story;
|
||||
mod webview_story;
|
||||
|
||||
pub use button_story::ButtonStory;
|
||||
pub use checkbox_story::CheckboxStory;
|
||||
|
|
@ -27,6 +28,7 @@ pub use scrollable_story::ScrollableStory;
|
|||
pub use switch_story::SwitchStory;
|
||||
pub use table_story::TableStory;
|
||||
pub use tooltip_story::TooltipStory;
|
||||
pub use webview_story::WebViewStory;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter,
|
||||
|
|
@ -46,14 +48,6 @@ pub fn init(cx: &mut AppContext) {
|
|||
input_story::init(cx);
|
||||
}
|
||||
|
||||
pub fn story_case(
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
cx: &mut WindowContext,
|
||||
) -> StoryContainer {
|
||||
StoryContainer::new(name, description, cx)
|
||||
}
|
||||
|
||||
pub fn section(title: impl Into<SharedString>, cx: &WindowContext) -> Div {
|
||||
use ui::theme::ActiveTheme;
|
||||
let theme = cx.theme();
|
||||
|
|
@ -104,7 +98,7 @@ impl Item for StoryContainer {
|
|||
Label::new(self.name.clone()).into_any_element()
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {
|
||||
fn deactivated(&mut self, _cx: &mut ViewContext<Self>) {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
105
crates/story/src/webview_story.rs
Normal file
105
crates/story/src/webview_story.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use gpui::{
|
||||
div, ClickEvent, FocusHandle, FocusableView, ParentElement as _, Render, Styled as _, View,
|
||||
ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
use ui::{
|
||||
button::Button,
|
||||
h_flex,
|
||||
input::{TextEvent, TextInput},
|
||||
theme::ActiveTheme,
|
||||
v_flex,
|
||||
webview::WebView,
|
||||
Clickable, IconName,
|
||||
};
|
||||
|
||||
pub struct WebViewStory {
|
||||
focus_handle: FocusHandle,
|
||||
webview: View<WebView>,
|
||||
address_input: View<TextInput>,
|
||||
}
|
||||
|
||||
impl WebViewStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let webview = cx.new_view(|cx| WebView::new(cx));
|
||||
|
||||
let address_input = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx);
|
||||
input.set_text("https://github.com/explore", cx);
|
||||
input
|
||||
});
|
||||
|
||||
let url = address_input.read(cx).text();
|
||||
webview.update(cx, |view, _| {
|
||||
view.load_url(&url);
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let this = WebViewStory {
|
||||
focus_handle,
|
||||
webview,
|
||||
address_input: address_input.clone(),
|
||||
};
|
||||
|
||||
cx.subscribe(
|
||||
&address_input,
|
||||
|this: &mut Self, input, event: &TextEvent, cx| match event {
|
||||
TextEvent::PressEnter => {
|
||||
let url = input.read(cx).text();
|
||||
this.webview.update(cx, |view, _| {
|
||||
view.load_url(&url);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide(&self, cx: &mut WindowContext) {
|
||||
self.webview.update(cx, |webview, _| webview.hide())
|
||||
}
|
||||
|
||||
fn go_back(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.webview.update(cx, |webview, _| {
|
||||
webview.back().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for WebViewStory {
|
||||
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WebViewStory {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_3()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("go-back", cx)
|
||||
.icon(IconName::ArrowLeft)
|
||||
.on_click(cx.listener(Self::go_back)),
|
||||
)
|
||||
.child(self.address_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(self.webview.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ anyhow = "1"
|
|||
log = "0.4"
|
||||
serde = "1.0.203"
|
||||
serde_json = "1"
|
||||
wry = "0"
|
||||
|
||||
smallvec = "1.13.2"
|
||||
windows = "0.57.0"
|
||||
unicode-segmentation = "1.11.0"
|
||||
|
|
@ -24,6 +24,12 @@ usvg = { version = "0.41.0", default-features = false }
|
|||
taffy = "0.4.3"
|
||||
paste = "1"
|
||||
once_cell = "1.19.0"
|
||||
raw-window-handle = "0.6.2"
|
||||
winit = "0.30.3"
|
||||
objc = "0.2"
|
||||
objc_id = "0.1"
|
||||
cocoa = "0.25"
|
||||
wry = "0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ pub enum IconName {
|
|||
CircleX,
|
||||
Loader,
|
||||
LoaderCircle,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
|
|
@ -45,6 +49,10 @@ impl IconName {
|
|||
IconName::CircleX => "icons/circle-x.svg",
|
||||
IconName::Loader => "icons/loader.svg",
|
||||
IconName::LoaderCircle => "icons/loader-circle.svg",
|
||||
IconName::ArrowLeft => "icons/arrow-left.svg",
|
||||
IconName::ArrowRight => "icons/arrow-right.svg",
|
||||
IconName::ChevronLeft => "icons/chevron-left.svg",
|
||||
IconName::ChevronRight => "icons/chevron-right.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ pub mod tab;
|
|||
pub mod table;
|
||||
pub mod theme;
|
||||
pub mod tooltip;
|
||||
pub mod webview;
|
||||
|
||||
pub use clickable::Clickable;
|
||||
pub use disableable::Disableable;
|
||||
|
|
@ -52,4 +53,5 @@ pub fn init(cx: &mut gpui::AppContext) {
|
|||
popover::init(cx);
|
||||
popup_menu::init(cx);
|
||||
table::init(cx);
|
||||
webview::init(cx)
|
||||
}
|
||||
|
|
|
|||
187
crates/ui/src/webview.rs
Normal file
187
crates/ui/src/webview.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use std::{ops::Deref, rc::Rc};
|
||||
|
||||
use wry::{
|
||||
dpi::{self, LogicalSize},
|
||||
Rect,
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
div, AppContext, Bounds, ContentMask, DismissEvent, Element, ElementId, EventEmitter,
|
||||
FocusHandle, FocusableView, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId,
|
||||
MouseDownEvent, ParentElement as _, Pixels, Render, Size, Style, Styled as _, View,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
pub fn init(_cx: &AppContext) {}
|
||||
|
||||
pub struct WebView {
|
||||
focus_handle: FocusHandle,
|
||||
webview: Rc<wry::WebView>,
|
||||
visable: bool,
|
||||
}
|
||||
|
||||
impl WebView {
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let window_handle = cx.raw_window_handle();
|
||||
|
||||
let webview = wry::WebView::new_as_child(&window_handle)
|
||||
.expect("failed to create webview to child window");
|
||||
let _ = webview.set_bounds(Rect::default());
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
visable: true,
|
||||
webview: Rc::new(webview),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
let _ = self.webview.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
let _ = self.webview.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.visable
|
||||
}
|
||||
|
||||
/// Go back in the webview history.
|
||||
pub fn back(&mut self) -> anyhow::Result<()> {
|
||||
Ok(self.webview.evaluate_script("history.back();")?)
|
||||
}
|
||||
|
||||
pub fn load_url(&mut self, url: &str) {
|
||||
self.webview.load_url(url).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WebView {
|
||||
type Target = wry::WebView;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.webview
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for WebView {
|
||||
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for WebView {}
|
||||
|
||||
impl Render for WebView {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
let view = cx.view().clone();
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(WebViewElement::new(self.webview.clone(), view, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// A webview element can display a wry webview.
|
||||
pub struct WebViewElement {
|
||||
parent: View<WebView>,
|
||||
view: Rc<wry::WebView>,
|
||||
}
|
||||
|
||||
impl WebViewElement {
|
||||
/// Create a new webview element from a wry WebView.
|
||||
pub fn new(view: Rc<wry::WebView>, parent: View<WebView>, _: &mut WindowContext) -> Self {
|
||||
Self { view, parent }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for WebViewElement {
|
||||
type Element = WebViewElement;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for WebViewElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.flex_grow = 0.0;
|
||||
style.flex_shrink = 1.;
|
||||
style.size = Size::full();
|
||||
// If the parent view is no longer visible, we don't need to layout the webview
|
||||
|
||||
let id = cx.request_layout(style, []);
|
||||
(id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
if !self.parent.read(cx).visible() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.view
|
||||
.set_bounds(Rect {
|
||||
size: dpi::Size::Logical(LogicalSize {
|
||||
width: (bounds.size.width.0).into(),
|
||||
height: (bounds.size.height.0).into(),
|
||||
}),
|
||||
position: dpi::Position::Logical(dpi::LogicalPosition::new(
|
||||
bounds.origin.x.into(),
|
||||
bounds.origin.y.into(),
|
||||
)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Create a hitbox to handle mouse event
|
||||
Some(cx.insert_hitbox(bounds, false))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds);
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
let webview = self.view.clone();
|
||||
cx.on_mouse_event(move |event: &MouseDownEvent, _, cx| {
|
||||
if !bounds.contains(&event.position) {
|
||||
// Click white space to blur the input focus
|
||||
webview
|
||||
.evaluate_script(
|
||||
r#"
|
||||
document.querySelectorAll("input,textarea").forEach(input => input.blur());
|
||||
"#,
|
||||
)
|
||||
.expect("failed to evaluate_script to blur input");
|
||||
} else {
|
||||
cx.blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue