use super::{Edit, Report};
use gpui::{App, RenderImage, Window};
use std::{
cell::{Cell, OnceCell},
sync::Arc,
};
use wry::WebViewExtMacOS;
mod cursor;
mod press;
pub(super) const BINDS_EDITS: bool = true;
pub(super) struct State {
cursor: OnceCell<cursor::Watch>,
press: OnceCell<Option<press::Monitor>>,
}
impl State {
pub(super) fn new(_cx: &App) -> Self {
Self {
cursor: OnceCell::new(),
press: OnceCell::new(),
}
}
pub(super) fn attach(&self, view: &wry::WebView, reports: &async_channel::Sender<Report>) {
let page = view.webview();
let _ = self.cursor.set(cursor::Watch::new(&page));
let _ = self.press.set(press::Monitor::new(&page, reports.clone()));
}
pub(super) fn parked(&self) {
if let Some(cursor) = self.cursor.get() {
cursor.release();
}
}
pub(super) fn watch_keys(&self, _window: &Window) {}
pub(super) fn holds_keys(&self, view: &wry::WebView) -> bool {
holds(&view.webview())
}
}
pub(super) fn start(_window: &Window, _cx: &mut App) {}
pub(super) fn build(
builder: wry::WebViewBuilder<'_>,
window: &Window,
) -> Option<wry::Result<wry::WebView>> {
Some(builder.build_as_child(window))
}
pub(super) fn reparent(view: &wry::WebView, window: &Window) -> bool {
use objc2_app_kit::NSView;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
let Ok(handle) = HasWindowHandle::window_handle(window) else {
return false;
};
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
return false;
};
let parent = unsafe { handle.ns_view.cast::<NSView>().as_ref() };
parent.addSubview(&view.webview());
true
}
pub(super) fn give_keys(view: &wry::WebView) {
release(&view.webview());
}
fn release(page: &objc2_app_kit::NSView) {
if let (Some(window), Some(parent)) = (page.window(), unsafe { page.superview() }) {
window.makeFirstResponder(Some(&parent));
}
}
fn holds(page: &objc2_app_kit::NSView) -> bool {
use objc2_app_kit::NSView;
let Some(window) = page.window() else {
return false;
};
window
.firstResponder()
.and_then(|responder| responder.downcast::<NSView>().ok())
.is_some_and(|responder| responder.isDescendantOf(page))
}
pub(super) fn edit(edit: Edit) {
use objc2::{MainThreadMarker, sel};
use objc2_app_kit::NSApplication;
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let action = match edit {
Edit::Copy => sel!(copy:),
Edit::Cut => sel!(cut:),
Edit::Paste => sel!(paste:),
Edit::SelectAll => sel!(selectAll:),
Edit::Undo => sel!(undo:),
Edit::Redo => sel!(redo:),
};
unsafe { NSApplication::sharedApplication(mtm).sendAction_to_from(action, None, None) };
}
pub(super) fn back(view: &wry::WebView) {
unsafe { view.webview().goBack() };
}
pub(super) fn forward(view: &wry::WebView) {
unsafe { view.webview().goForward() };
}
pub(super) fn capture(
view: &wry::WebView,
done: impl FnOnce(Option<Arc<RenderImage>>) + 'static,
) -> bool {
use block2::RcBlock;
use objc2_app_kit::NSImage;
use objc2_foundation::NSError;
let done = Cell::new(Some(done));
let block = RcBlock::new(move |picture: *mut NSImage, _: *mut NSError| {
let still = unsafe { picture.as_ref() }.and_then(pixels);
if let Some(done) = done.take() {
done(still);
}
});
unsafe {
view.webview()
.takeSnapshotWithConfiguration_completionHandler(None, &block);
}
true
}
fn pixels(picture: &objc2_app_kit::NSImage) -> Option<Arc<RenderImage>> {
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapContextCreate, CGColorSpace, CGContext, CGImage, CGImageAlphaInfo,
CGImageByteOrderInfo, kCGColorSpaceSRGB,
};
let picture =
unsafe { picture.CGImageForProposedRect_context_hints(std::ptr::null_mut(), None, None) }?;
let (width, height) = (
CGImage::width(Some(&picture)),
CGImage::height(Some(&picture)),
);
if width == 0 || height == 0 {
return None;
}
let mut bytes = vec![0u8; width * height * 4];
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceSRGB }))?;
let info = CGImageAlphaInfo::PremultipliedFirst.0 | CGImageByteOrderInfo::Order32Little.0;
let context = unsafe {
CGBitmapContextCreate(
bytes.as_mut_ptr().cast(),
width,
height,
8,
width * 4,
Some(&space),
info,
)
}?;
let rect = CGRect::new(CGPoint::ZERO, CGSize::new(width as f64, height as f64));
CGContext::draw_image(Some(&context), rect, Some(&picture));
drop(context);
let buffer =
image::RgbaImage::from_raw(width.try_into().ok()?, height.try_into().ok()?, bytes)?;
Some(Arc::new(RenderImage::new([image::Frame::new(buffer)])))
}
pub(super) fn closed(view: &wry::WebView) -> bool {
view.webview().window().is_none()
}
pub(super) fn default_user_agent() -> Option<String> {
use objc2_foundation::{NSBundle, NSString};
let version = NSBundle::bundleWithPath(&NSString::from_str("/Applications/Safari.app"))
.and_then(|safari| {
safari.objectForInfoDictionaryKey(&NSString::from_str("CFBundleShortVersionString"))
})
.and_then(|version| version.downcast::<NSString>().ok())
.map_or_else(|| "26.0".to_owned(), |version| version.to_string());
Some(format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 \
(KHTML, like Gecko) Version/{version} Safari/605.1.15"
))
}