Jason Lee 2024-07-18 18:00:23 +08:00 committed by GitHub
parent 91e123e942
commit f6146220e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 998 additions and 83 deletions

710
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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" }

View 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

View 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

View 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

View 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

View file

@ -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.",

View file

@ -12,3 +12,7 @@ anyhow = "1"
workspace.workspace = true
charts-rs = "0.3"
image = "0.25.1"
wry = "0"
[lints]
workspace = true

View file

@ -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;
}

View 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()),
)
}
}

View file

@ -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

View file

@ -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()
}

View file

@ -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
View 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();
}
});
});
}
}