436 lines
14 KiB
Rust
436 lines
14 KiB
Rust
use crate::dispatcher::WebDispatcher;
|
|
use crate::display::WebDisplay;
|
|
use crate::keyboard::WebKeyboardLayout;
|
|
use crate::window::WebWindow;
|
|
use anyhow::Result;
|
|
use futures::channel::oneshot;
|
|
use gpui::{
|
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
|
|
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
|
|
ThermalState, WindowAppearance, WindowParams,
|
|
};
|
|
use gpui_wgpu::WgpuContext;
|
|
use std::{
|
|
borrow::Cow,
|
|
cell::{Cell, RefCell},
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::Arc,
|
|
};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
static BUNDLED_FONTS: &[&[u8]] = &[
|
|
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
|
|
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
|
|
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
|
|
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
|
|
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
|
|
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
|
|
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
|
|
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
|
|
];
|
|
|
|
pub struct WebPlatform {
|
|
browser_window: web_sys::Window,
|
|
background_executor: BackgroundExecutor,
|
|
foreground_executor: ForegroundExecutor,
|
|
text_system: Arc<dyn PlatformTextSystem>,
|
|
active_window: RefCell<Option<AnyWindowHandle>>,
|
|
active_display: Rc<dyn PlatformDisplay>,
|
|
callbacks: RefCell<WebPlatformCallbacks>,
|
|
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
|
|
cursor_visible: Rc<Cell<bool>>,
|
|
last_cursor_css: Rc<Cell<&'static str>>,
|
|
_cursor_restore_listeners: Vec<EventListenerHandle>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct WebPlatformCallbacks {
|
|
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
|
quit: Option<Box<dyn FnMut()>>,
|
|
reopen: Option<Box<dyn FnMut()>>,
|
|
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
|
|
will_open_app_menu: Option<Box<dyn FnMut()>>,
|
|
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
|
keyboard_layout_change: Option<Box<dyn FnMut()>>,
|
|
thermal_state_change: Option<Box<dyn FnMut()>>,
|
|
}
|
|
|
|
impl WebPlatform {
|
|
pub fn new(allow_multi_threading: bool) -> Self {
|
|
let browser_window =
|
|
web_sys::window().expect("must be running in a browser window context");
|
|
let dispatcher = Arc::new(WebDispatcher::new(
|
|
browser_window.clone(),
|
|
allow_multi_threading,
|
|
));
|
|
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
|
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
|
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
|
|
"IBM Plex Sans",
|
|
));
|
|
let fonts = BUNDLED_FONTS
|
|
.iter()
|
|
.map(|bytes| Cow::Borrowed(*bytes))
|
|
.collect();
|
|
if let Err(error) = text_system.add_fonts(fonts) {
|
|
log::error!("failed to load bundled fonts: {error:#}");
|
|
}
|
|
let text_system: Arc<dyn PlatformTextSystem> = text_system;
|
|
let active_display: Rc<dyn PlatformDisplay> =
|
|
Rc::new(WebDisplay::new(browser_window.clone()));
|
|
|
|
let cursor_visible = Rc::new(Cell::new(true));
|
|
let last_cursor_css = Rc::new(Cell::new("default"));
|
|
let cursor_restore_listeners = cursor_restore_listeners(
|
|
&browser_window,
|
|
cursor_visible.clone(),
|
|
last_cursor_css.clone(),
|
|
);
|
|
|
|
Self {
|
|
browser_window,
|
|
background_executor,
|
|
foreground_executor,
|
|
text_system,
|
|
active_window: RefCell::new(None),
|
|
active_display,
|
|
callbacks: RefCell::new(WebPlatformCallbacks::default()),
|
|
wgpu_context: Rc::new(RefCell::new(None)),
|
|
cursor_visible,
|
|
last_cursor_css,
|
|
_cursor_restore_listeners: cursor_restore_listeners,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Platform for WebPlatform {
|
|
fn background_executor(&self) -> BackgroundExecutor {
|
|
self.background_executor.clone()
|
|
}
|
|
|
|
fn foreground_executor(&self) -> ForegroundExecutor {
|
|
self.foreground_executor.clone()
|
|
}
|
|
|
|
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
|
self.text_system.clone()
|
|
}
|
|
|
|
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
|
let wgpu_context = self.wgpu_context.clone();
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
match WgpuContext::new_web().await {
|
|
Ok(context) => {
|
|
log::info!("WebGPU context initialized successfully");
|
|
*wgpu_context.borrow_mut() = Some(context);
|
|
on_finish_launching();
|
|
}
|
|
Err(err) => {
|
|
log::error!("Failed to initialize WebGPU context: {err:#}");
|
|
on_finish_launching();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn quit(&self) {
|
|
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
|
|
}
|
|
|
|
fn restart(&self, _binary_path: Option<PathBuf>) {}
|
|
|
|
fn activate(&self, _ignoring_other_apps: bool) {}
|
|
|
|
fn hide(&self) {}
|
|
|
|
fn hide_other_apps(&self) {}
|
|
|
|
fn unhide_other_apps(&self) {}
|
|
|
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
|
vec![self.active_display.clone()]
|
|
}
|
|
|
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
|
Some(self.active_display.clone())
|
|
}
|
|
|
|
fn active_window(&self) -> Option<AnyWindowHandle> {
|
|
*self.active_window.borrow()
|
|
}
|
|
|
|
fn open_window(
|
|
&self,
|
|
handle: AnyWindowHandle,
|
|
params: WindowParams,
|
|
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
|
let context_ref = self.wgpu_context.borrow();
|
|
let context = context_ref.as_ref().ok_or_else(|| {
|
|
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
|
|
})?;
|
|
|
|
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
|
|
*self.active_window.borrow_mut() = Some(handle);
|
|
Ok(Box::new(window))
|
|
}
|
|
|
|
fn window_appearance(&self) -> WindowAppearance {
|
|
let Ok(Some(media_query)) = self
|
|
.browser_window
|
|
.match_media("(prefers-color-scheme: dark)")
|
|
else {
|
|
return WindowAppearance::Light;
|
|
};
|
|
if media_query.matches() {
|
|
WindowAppearance::Dark
|
|
} else {
|
|
WindowAppearance::Light
|
|
}
|
|
}
|
|
|
|
fn open_url(&self, url: &str) {
|
|
if let Err(error) = self.browser_window.open_with_url(url) {
|
|
log::warn!("Failed to open URL '{url}': {error:?}");
|
|
}
|
|
}
|
|
|
|
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
|
|
self.callbacks.borrow_mut().open_urls = Some(callback);
|
|
}
|
|
|
|
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
|
|
Task::ready(Ok(()))
|
|
}
|
|
|
|
fn prompt_for_paths(
|
|
&self,
|
|
_options: PathPromptOptions,
|
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
|
let (tx, rx) = oneshot::channel();
|
|
tx.send(Err(anyhow::anyhow!(
|
|
"prompt_for_paths is not supported on the web"
|
|
)))
|
|
.ok();
|
|
rx
|
|
}
|
|
|
|
fn prompt_for_new_path(
|
|
&self,
|
|
_directory: &Path,
|
|
_suggested_name: Option<&str>,
|
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
|
let (sender, receiver) = oneshot::channel();
|
|
sender
|
|
.send(Err(anyhow::anyhow!(
|
|
"prompt_for_new_path is not supported on the web"
|
|
)))
|
|
.ok();
|
|
receiver
|
|
}
|
|
|
|
fn can_select_mixed_files_and_dirs(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn reveal_path(&self, _path: &Path) {}
|
|
|
|
fn open_with_system(&self, _path: &Path) {}
|
|
|
|
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
|
self.callbacks.borrow_mut().quit = Some(callback);
|
|
}
|
|
|
|
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
|
|
self.callbacks.borrow_mut().reopen = Some(callback);
|
|
}
|
|
|
|
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
|
|
|
|
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
|
|
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
|
self.callbacks.borrow_mut().app_menu_action = Some(callback);
|
|
}
|
|
|
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
|
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
|
|
}
|
|
|
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
|
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
|
|
}
|
|
|
|
fn thermal_state(&self) -> ThermalState {
|
|
ThermalState::Nominal
|
|
}
|
|
|
|
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
|
|
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
|
|
}
|
|
|
|
fn compositor_name(&self) -> &'static str {
|
|
"Web"
|
|
}
|
|
|
|
fn app_path(&self) -> Result<PathBuf> {
|
|
Err(anyhow::anyhow!("app_path is not available on the web"))
|
|
}
|
|
|
|
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
|
|
Err(anyhow::anyhow!(
|
|
"path_for_auxiliary_executable is not available on the web"
|
|
))
|
|
}
|
|
|
|
fn set_cursor_style(&self, style: CursorStyle) {
|
|
let css_cursor = match style {
|
|
CursorStyle::Arrow => "default",
|
|
CursorStyle::IBeam => "text",
|
|
CursorStyle::Crosshair => "crosshair",
|
|
CursorStyle::ClosedHand => "grabbing",
|
|
CursorStyle::OpenHand => "grab",
|
|
CursorStyle::PointingHand => "pointer",
|
|
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
|
|
"ew-resize"
|
|
}
|
|
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
|
|
"ns-resize"
|
|
}
|
|
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
|
|
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
|
|
CursorStyle::ResizeColumn => "col-resize",
|
|
CursorStyle::ResizeRow => "row-resize",
|
|
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
|
CursorStyle::OperationNotAllowed => "not-allowed",
|
|
CursorStyle::DragLink => "alias",
|
|
CursorStyle::DragCopy => "copy",
|
|
CursorStyle::ContextualMenu => "context-menu",
|
|
};
|
|
|
|
self.last_cursor_css.set(css_cursor);
|
|
if self.cursor_visible.get() {
|
|
set_body_cursor(&self.browser_window, css_cursor);
|
|
}
|
|
}
|
|
|
|
fn hide_cursor_until_mouse_moves(&self) {
|
|
if !self.cursor_visible.replace(false) {
|
|
return;
|
|
}
|
|
set_body_cursor(&self.browser_window, "none");
|
|
}
|
|
|
|
fn is_cursor_visible(&self) -> bool {
|
|
self.cursor_visible.get()
|
|
}
|
|
|
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
|
None
|
|
}
|
|
|
|
fn write_to_clipboard(&self, _item: ClipboardItem) {}
|
|
|
|
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
|
Task::ready(Err(anyhow::anyhow!(
|
|
"credential storage is not available on the web"
|
|
)))
|
|
}
|
|
|
|
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
|
Task::ready(Ok(None))
|
|
}
|
|
|
|
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
|
|
Task::ready(Err(anyhow::anyhow!(
|
|
"credential storage is not available on the web"
|
|
)))
|
|
}
|
|
|
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
|
Box::new(WebKeyboardLayout)
|
|
}
|
|
|
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
Rc::new(DummyKeyboardMapper)
|
|
}
|
|
|
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
|
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
|
|
}
|
|
}
|
|
|
|
struct EventListenerHandle {
|
|
target: web_sys::EventTarget,
|
|
event_name: &'static str,
|
|
closure: Closure<dyn FnMut(JsValue)>,
|
|
}
|
|
|
|
impl Drop for EventListenerHandle {
|
|
fn drop(&mut self) {
|
|
self.target
|
|
.remove_event_listener_with_callback(
|
|
self.event_name,
|
|
self.closure.as_ref().unchecked_ref(),
|
|
)
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
fn cursor_restore_listeners(
|
|
browser_window: &web_sys::Window,
|
|
cursor_visible: Rc<Cell<bool>>,
|
|
last_cursor_css: Rc<Cell<&'static str>>,
|
|
) -> Vec<EventListenerHandle> {
|
|
let mut handles = Vec::new();
|
|
let Some(document) = browser_window.document() else {
|
|
return handles;
|
|
};
|
|
|
|
let make_restore_handler = |browser_window: web_sys::Window| {
|
|
let cursor_visible = cursor_visible.clone();
|
|
let last_cursor_css = last_cursor_css.clone();
|
|
Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
|
|
if !cursor_visible.replace(true) {
|
|
set_body_cursor(&browser_window, last_cursor_css.get());
|
|
}
|
|
})
|
|
};
|
|
|
|
let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {
|
|
let closure = make_restore_handler(browser_window.clone());
|
|
target
|
|
.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
|
|
.ok();
|
|
handles.push(EventListenerHandle {
|
|
target: target.clone(),
|
|
event_name,
|
|
closure,
|
|
});
|
|
};
|
|
|
|
let document_target: &web_sys::EventTarget = document.as_ref();
|
|
let window_target: &web_sys::EventTarget = browser_window.as_ref();
|
|
|
|
add_listener(document_target, "mousemove");
|
|
add_listener(document_target, "mouseenter");
|
|
add_listener(window_target, "blur");
|
|
add_listener(document_target, "visibilitychange");
|
|
|
|
handles
|
|
}
|
|
|
|
fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) {
|
|
if let Some(document) = browser_window.document()
|
|
&& let Some(body) = document.body()
|
|
&& let Err(error) = body.style().set_property("cursor", css_cursor)
|
|
{
|
|
log::warn!("Failed to set cursor style: {error:?}");
|
|
}
|
|
}
|