mod autoplay;
mod browsing;
mod clipboard;
mod content_filter;
mod context;
mod context_menu;
mod downloads;
mod favicon;
mod features;
mod file_chooser;
mod fullscreen;
mod hover;
mod input_method;
mod main_loop;
mod media_codecs;
mod modifiers;
mod navigation;
mod policy;
mod popups;
mod print;
mod privacy;
mod scripting;
mod session;
mod snapshot;
mod software;
mod termination;
pub use autoplay::set_autoplay;
pub use content_filter::{ remove_content_filters, set_content_filter };
pub use context::{ set_cache_model, set_memory_limit };
pub use downloads::{ cancel_download, poll_downloads, set_download_dir };
pub use features::set_feature;
pub use file_chooser::FileRequest;
pub use main_loop::wake_on_events;
pub use media_codecs::{ hardware_video_decoders, set_blocked_video_codecs };
pub use modifiers::set_modifiers;
pub use privacy::{ set_preferred_languages, set_third_party_cookies, set_tracking_prevention };
pub use session::{ prefetch_dns, set_data_directories };
use std::cell::RefCell;
use std::ffi::{ c_char, c_int, c_uint, c_void, CStr };
use std::ptr;
use std::sync::{ Arc, LazyLock, Mutex, Once };
use std::time::Instant;
use glib_sys::*;
use gobject_sys::*;
use wpe_platform_sys::*;
use wpe_platform_headless_sys::{ wpe_display_headless_new, wpe_view_headless_get_type };
use wpe_webkit_sys::*;
use glow::HasContext;
use ltk::{ CursorShape, External, ExternalSource };
use ltk::types::Rect;
use ltk::Element;
use crate::types::{ LoadEvent, PageTarget, Termination };
type GlEglImageTargetTexture2DOes = unsafe extern "system" fn( target: u32, image: *const c_void );
type EglCreateImageKHR = unsafe extern "system" fn(
dpy: *mut c_void,
ctx: *mut c_void,
target: u32,
buffer: *mut c_void,
attrib_list: *const c_int,
) -> *mut c_void;
type EglDestroyImageKHR = unsafe extern "system" fn( dpy: *mut c_void, image: *mut c_void ) -> u32;
type EglGetCurrentDisplay = unsafe extern "system" fn() -> *mut c_void;
const GL_TEXTURE_2D: u32 = 0x0DE1;
const EGL_NONE: c_int = 0x3038;
const EGL_WIDTH: c_int = 0x3057;
const EGL_HEIGHT: c_int = 0x3056;
const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
const EGL_LINUX_DRM_FOURCC_EXT: c_int = 0x3271;
const EGL_DMA_BUF_PLANE0_FD_EXT: c_int = 0x3272;
const EGL_DMA_BUF_PLANE0_OFFSET_EXT: c_int = 0x3273;
const EGL_DMA_BUF_PLANE0_PITCH_EXT: c_int = 0x3274;
const EGL_DMA_BUF_PLANE1_FD_EXT: c_int = 0x3275;
const EGL_DMA_BUF_PLANE1_OFFSET_EXT: c_int = 0x3276;
const EGL_DMA_BUF_PLANE1_PITCH_EXT: c_int = 0x3277;
const EGL_DMA_BUF_PLANE2_FD_EXT: c_int = 0x3278;
const EGL_DMA_BUF_PLANE2_OFFSET_EXT: c_int = 0x3279;
const EGL_DMA_BUF_PLANE2_PITCH_EXT: c_int = 0x327A;
const EGL_DMA_BUF_PLANE3_FD_EXT: c_int = 0x3440;
const EGL_DMA_BUF_PLANE3_OFFSET_EXT: c_int = 0x3441;
const EGL_DMA_BUF_PLANE3_PITCH_EXT: c_int = 0x3442;
const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: c_int = 0x3443;
const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: c_int = 0x3444;
const EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT: c_int = 0x3445;
const EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT: c_int = 0x3446;
const EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT: c_int = 0x3447;
const EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT: c_int = 0x3448;
const EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT: c_int = 0x3449;
const EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT: c_int = 0x344A;
const PLANE_FD: [ c_int; 4 ] = [ EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE3_FD_EXT ];
const PLANE_OFF: [ c_int; 4 ] = [ EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT ];
const PLANE_PITCH: [ c_int; 4 ] = [ EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT ];
const PLANE_MOD_LO: [ c_int; 4 ] = [ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT ];
const PLANE_MOD_HI: [ c_int; 4 ] = [ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT ];
struct Inner
{
display: *mut WPEDisplay,
webview: *mut WebKitWebView,
view: *mut WPEView,
width: f32,
height: f32,
last_rect: Rect,
scale: f64,
pending_buffer: Option<*mut WPEBuffer>,
bound_buffer: Option<*mut WPEBuffer>,
bound_image: Option<*mut c_void>,
gl_texture: Option<glow::Texture>,
copy_path: Option<bool>,
copy_size: ( i32, i32 ),
bind_image_fn: Option<GlEglImageTargetTexture2DOes>,
egl_create_fn: Option<EglCreateImageKHR>,
egl_destroy_fn: Option<EglDestroyImageKHR>,
egl_get_current_display_fn: Option<EglGetCurrentDisplay>,
cursor_shape: CursorShape,
needs_redraw: bool,
button1_held: bool,
new_window_requests: Vec<String>,
created_views: Vec<WebView>,
close_requested: bool,
file_request: Option<file_chooser::FileRequest>,
clipboard_seen: i64,
fullscreen_request: Option<bool>,
print_job: Option<print::Job>,
print_requested: bool,
termination: Option<Termination>,
page_target: Option<PageTarget>,
hovered_link: Option<String>,
favicon: Option<ltk::ImageData>,
im_context: *mut WebKitInputMethodContext,
editable_focused: bool,
navigation_handler: Option<navigation::NavigationHandler>,
host_load_pending: bool,
load_events: Vec<LoadEvent>,
#[ allow( dead_code ) ]
egl_instance: khronos_egl::DynamicInstance<khronos_egl::EGL1_4>,
}
unsafe impl Send for Inner {}
unsafe impl Sync for Inner {}
pub struct WebView
{
inner: Arc<Mutex<Inner>>,
}
impl WebView
{
pub fn new( width: f32, height: f32, url: &str ) -> Result<Self, String>
{
let this = Self::build( width, height, None )?;
if !url.is_empty()
{
let url_c = std::ffi::CString::new( url ).map_err( |e| format!( "url contains NUL: {}", e ) )?;
unsafe
{
let webview = this.inner.lock().unwrap().webview;
webkit_web_view_load_uri( webview, url_c.as_ptr() );
}
}
Ok( this )
}
pub( crate ) fn build( width: f32, height: f32, opener: Option<popups::Opener> ) -> Result<Self, String>
{
let inner = unsafe
{
let display = match &opener
{
Some( o ) => g_object_ref( o.display as *mut GObject ) as *mut WPEDisplay,
None => wpe_display_headless_new(),
};
if display.is_null()
{
return Err( "wpe_display_headless_new returned NULL".into() );
}
let mut err: *mut GError = ptr::null_mut();
if opener.is_none() && wpe_display_connect( display, &mut err ) == 0
{
let msg = error_message_or_default( err, "unknown" );
if !err.is_null() { g_error_free( err ); }
g_object_unref( display as *mut _ );
return Err( format!( "wpe_display_connect: {}", msg ) );
}
let display_prop: *const c_char = b"display\0".as_ptr() as _;
let related_prop: *const c_char = b"related-view\0".as_ptr() as _;
let context_prop: *const c_char = b"web-context\0".as_ptr() as _;
let session_prop: *const c_char = b"network-session\0".as_ptr() as _;
let content_prop: *const c_char = b"user-content-manager\0".as_ptr() as _;
let policy_prop: *const c_char = b"website-policies\0".as_ptr() as _;
let null_terminator: *const c_char = ptr::null();
let policies = autoplay::website_policies();
let webview = match &opener
{
Some( o ) => g_object_new(
webkit_web_view_get_type(),
display_prop,
display,
related_prop,
o.webview,
content_prop,
content_filter::user_content_manager(),
policy_prop,
policies,
null_terminator,
),
None => g_object_new(
webkit_web_view_get_type(),
display_prop,
display,
context_prop,
context::web_context(),
session_prop,
session::network_session(),
content_prop,
content_filter::user_content_manager(),
policy_prop,
policies,
null_terminator,
),
} as *mut WebKitWebView;
if !policies.is_null() { g_object_unref( policies as *mut _ ); }
if webview.is_null()
{
g_object_unref( display as *mut _ );
return Err( "g_object_new(WebKitWebView) returned NULL".into() );
}
if let Some( o ) = &opener { browsing::copy_user_agent( o.webview, webview ); }
scripting::console_to_stdout_if_asked( webview );
features::apply( webview );
let view = webkit_web_view_get_wpe_view( webview );
if view.is_null()
{
g_object_unref( webview as *mut _ );
g_object_unref( display as *mut _ );
return Err( "webkit_web_view_get_wpe_view returned NULL".into() );
}
let egl_instance = khronos_egl::DynamicInstance::<khronos_egl::EGL1_4>::load_required()
.map_err( |e| format!( "EGL load: {}", e ) )?;
Inner
{
display,
webview,
view,
width,
height,
last_rect: Rect { x: 0.0, y: 0.0, width, height },
scale: 1.0,
pending_buffer: None,
bound_buffer: None,
bound_image: None,
gl_texture: None,
copy_path: None,
copy_size: ( 0, 0 ),
bind_image_fn: None,
egl_create_fn: None,
egl_destroy_fn: None,
egl_get_current_display_fn: None,
cursor_shape: CursorShape::Default,
needs_redraw: true,
button1_held: false,
new_window_requests: Vec::new(),
created_views: Vec::new(),
close_requested: false,
file_request: None,
clipboard_seen: 0,
fullscreen_request: None,
print_job: None,
print_requested: false,
termination: None,
page_target: None,
hovered_link: None,
favicon: None,
im_context: ptr::null_mut(),
editable_focused: false,
navigation_handler: None,
host_load_pending: false,
load_events: Vec::new(),
egl_instance,
}
};
let inner = Arc::new( Mutex::new( inner ) );
unsafe
{
let view = inner.lock().unwrap().view;
wpe_view_map( view );
wpe_view_set_visible( view, 1 );
wpe_view_focus_in( view );
let top = wpe_view_get_toplevel( view );
if !top.is_null()
{
wpe_toplevel_resize( top, width as i32, height as i32 );
}
install_view_overrides();
let qdata_box = Box::into_raw( Box::new( Arc::downgrade( &inner ) ) ) as gpointer;
g_object_set_data_full(
view as *mut GObject,
CURSOR_INNER_QDATA_KEY.as_ptr() as *const c_char,
qdata_box,
Some( drop_weak_qdata ),
);
let webview = inner.lock().unwrap().webview;
policy::connect( webview, Arc::downgrade( &inner ) );
navigation::connect( webview, Arc::downgrade( &inner ) );
popups::connect( webview, Arc::downgrade( &inner ) );
file_chooser::connect( webview, Arc::downgrade( &inner ) );
print::connect( webview, Arc::downgrade( &inner ) );
termination::connect( webview, Arc::downgrade( &inner ) );
fullscreen::connect( webview, Arc::downgrade( &inner ) );
context_menu::connect( webview, Arc::downgrade( &inner ) );
hover::connect( webview, Arc::downgrade( &inner ) );
let im_context = input_method::install( webview, Arc::downgrade( &inner ) );
inner.lock().unwrap().im_context = im_context;
downloads::track( webview );
}
Ok( Self { inner } )
}
pub fn resize( &self, width: f32, height: f32 )
{
let ( view, ( lw, lh ) ) = match self.inner.lock()
{
Ok( mut g ) =>
{
g.width = width;
g.height = height;
if g.print_job.is_some() { return; }
( g.view, g.logical_size() )
}
Err( _ ) => return,
};
if view.is_null() { return; }
unsafe
{
let top = wpe_view_get_toplevel( view );
if top.is_null() { return; }
wpe_toplevel_resize( top, lw, lh );
}
}
pub fn tick( &self )
{
unsafe
{
while g_main_context_iteration( ptr::null_mut(), 0 ) != 0
{
}
}
main_loop::rearm();
}
pub fn pointer_press( &self, x: f64, y: f64 )
{
self.send_button( WPE_EVENT_POINTER_DOWN, WPE_BUTTON_PRIMARY, x, y );
}
pub fn pointer_release( &self, x: f64, y: f64 )
{
self.send_button( WPE_EVENT_POINTER_UP, WPE_BUTTON_PRIMARY, x, y );
}
pub fn click_at( &self, x: f64, y: f64 )
{
self.pointer_press( x, y );
self.pointer_release( x, y );
}
pub fn touch_down( &self, id: u32, x: f64, y: f64 )
{
self.send_touch( WPE_EVENT_TOUCH_DOWN, id, x, y );
}
pub fn touch_move( &self, id: u32, x: f64, y: f64 )
{
self.send_touch( WPE_EVENT_TOUCH_MOVE, id, x, y );
}
pub fn touch_up( &self, id: u32, x: f64, y: f64 )
{
self.send_touch( WPE_EVENT_TOUCH_UP, id, x, y );
}
pub fn touch_cancel( &self, id: u32, x: f64, y: f64 )
{
self.send_touch( WPE_EVENT_TOUCH_CANCEL, id, x, y );
}
fn send_touch( &self, ev_type: WPEEventType, id: u32, x: f64, y: f64 )
{
let ( view, x, y ) = match self.inner.lock()
{
Ok( g ) =>
{
let ( lx, ly ) = g.to_view( x, y );
( g.view, lx, ly )
}
Err( _ ) => return,
};
if view.is_null() { return; }
unsafe
{
let ev = wpe_event_touch_new(
ev_type,
view,
WPE_INPUT_SOURCE_TOUCHSCREEN,
elapsed_ms(),
0,
id,
x, y,
);
if !ev.is_null() { wpe_view_event( view, ev ); wpe_event_unref( ev ); }
}
}
pub( crate ) fn send_button( &self, ev_type: WPEEventType, button: c_int, x: f64, y: f64 )
{
let ( view, x, y ) = match self.inner.lock()
{
Ok( mut g ) =>
{
if button == WPE_BUTTON_PRIMARY
{
if ev_type == WPE_EVENT_POINTER_DOWN
{
g.button1_held = true;
} else if ev_type == WPE_EVENT_POINTER_UP {
g.button1_held = false;
}
}
let ( lx, ly ) = g.to_view( x, y );
( g.view, lx, ly )
}
Err( _ ) => return,
};
if view.is_null() { return; }
let time = elapsed_ms();
let press_count = if ev_type == WPE_EVENT_POINTER_DOWN
{
unsafe { wpe_view_compute_press_count( view, x, y, button as c_uint, time ) }
} else {
0
};
unsafe
{
let ev = wpe_event_pointer_button_new(
ev_type,
view,
WPE_INPUT_SOURCE_MOUSE,
time,
modifiers::held(),
button as c_uint,
x, y,
press_count,
);
if !ev.is_null() { wpe_view_event( view, ev ); wpe_event_unref( ev ); }
}
}
pub fn scroll( &self, x: f64, y: f64, dx: f64, dy: f64 )
{
let ( view, x, y ) = match self.inner.lock()
{
Ok( g ) =>
{
let ( lx, ly ) = g.to_view( x, y );
( g.view, lx, ly )
}
Err( _ ) => return,
};
if view.is_null() { return; }
unsafe
{
let ev = wpe_event_scroll_new(
view,
WPE_INPUT_SOURCE_MOUSE,
0, modifiers::held(),
dx, dy,
1, 0, x, y,
);
if !ev.is_null() { wpe_view_event( view, ev ); wpe_event_unref( ev ); }
}
}
pub fn pointer_move( &self, x: f64, y: f64 )
{
let ( view, x, y, mods ) = match self.inner.lock()
{
Ok( g ) =>
{
let ( lx, ly ) = g.to_view( x, y );
let mods = modifiers::held() | if g.button1_held { WPE_MODIFIER_POINTER_BUTTON1 } else { 0 };
( g.view, lx, ly, mods )
}
Err( _ ) => return,
};
if view.is_null() { return; }
unsafe
{
let ev = wpe_event_pointer_move_new(
WPE_EVENT_POINTER_MOVE,
view,
WPE_INPUT_SOURCE_MOUSE,
elapsed_ms(),
mods,
x, y,
0.0, 0.0,
);
if !ev.is_null() { wpe_view_event( view, ev ); wpe_event_unref( ev ); }
}
}
pub fn send_key( &self, keysym: u32, keycode: u32, pressed: bool, ctrl: bool, shift: bool )
{
let view = match self.inner.lock()
{
Ok( g ) => g.view,
Err( _ ) => return,
};
if view.is_null() { return; }
let mut mods: WPEModifiers = 0;
if ctrl { mods |= WPE_MODIFIER_KEYBOARD_CONTROL; }
if shift { mods |= WPE_MODIFIER_KEYBOARD_SHIFT; }
let ev_type = if pressed { WPE_EVENT_KEYBOARD_KEY_DOWN } else { WPE_EVENT_KEYBOARD_KEY_UP };
unsafe
{
let ev = wpe_event_keyboard_new(
ev_type,
view,
WPE_INPUT_SOURCE_KEYBOARD,
0, mods,
keycode,
keysym,
);
if !ev.is_null() { wpe_view_event( view, ev ); wpe_event_unref( ev ); }
}
}
pub fn take_redraw_request( &self ) -> bool
{
let was = self.inner.lock()
.map( | mut g |
{
let was = g.needs_redraw;
g.needs_redraw = false;
was
} )
.unwrap_or( false );
if was { dbg_log( "redraw request consumed" ); }
was
}
pub fn cursor_shape( &self ) -> CursorShape
{
self.inner.lock().map( |g| g.cursor_shape ).unwrap_or( CursorShape::Default )
}
pub fn element<Msg: Clone + 'static>( &self ) -> Element<Msg>
{
let ( width, height ) = match self.inner.lock()
{
Ok( g ) => ( g.width, g.height ),
Err( _ ) => ( 0.0, 0.0 ),
};
let inner = Arc::clone( &self.inner );
External::new(
width,
height,
ExternalSource::Frame( Arc::new( move | gl: &glow::Context, rect: Rect | -> Option<( glow::Texture, u32, u32 )>
{
dbg_log( &format!( "draw rect=({},{} {}x{})", rect.x, rect.y, rect.width, rect.height ) );
let mut guard = inner.lock().ok()?;
guard.last_rect = rect;
let new_w = rect.width;
let new_h = rect.height;
if ( new_w - guard.width ).abs() > 0.5 || ( new_h - guard.height ).abs() > 0.5
{
guard.width = new_w;
guard.height = new_h;
if !guard.view.is_null() && guard.print_job.is_none()
{
let view = guard.view;
let ( lw, lh ) = guard.logical_size();
drop( guard );
unsafe
{
let top = wpe_view_get_toplevel( view );
if !top.is_null()
{
wpe_toplevel_resize( top, lw, lh );
}
}
let mut guard = inner.lock().ok()?;
return guard.frame( gl );
}
}
guard.frame( gl )
} ) ),
).into()
}
}
impl Inner
{
fn to_view( &self, x: f64, y: f64 ) -> ( f64, f64 )
{
( ( x - self.last_rect.x as f64 ) / self.scale, ( y - self.last_rect.y as f64 ) / self.scale )
}
fn to_host( &self, x: f64, y: f64 ) -> ( f64, f64 )
{
( x * self.scale + self.last_rect.x as f64, y * self.scale + self.last_rect.y as f64 )
}
fn logical_size( &self ) -> ( i32, i32 )
{
( ( self.width as f64 / self.scale ) as i32, ( self.height as f64 / self.scale ) as i32 )
}
fn frame( &mut self, gl: &glow::Context ) -> Option<( glow::Texture, u32, u32 )>
{
let texture = self.refresh_texture( gl )?;
let ( width, height ) = if self.copy_path == Some( true )
{
( self.copy_size.0 as u32, self.copy_size.1 as u32 )
} else {
let buffer = self.bound_buffer?;
unsafe { ( wpe_buffer_get_width( buffer ) as u32, wpe_buffer_get_height( buffer ) as u32 ) }
};
let fit = |size: u32, extent: f32| if ( extent - size as f32 ).abs() <= self.scale as f32 + 1.0 { extent.round() as u32 } else { size };
Some( ( texture, fit( width, self.last_rect.width ), fit( height, self.last_rect.height ) ) )
}
fn refresh_texture( &mut self, gl: &glow::Context ) -> Option<glow::Texture>
{
if self.bind_image_fn.is_none()
{
self.bind_image_fn = self.load_proc::<GlEglImageTargetTexture2DOes>( "glEGLImageTargetTexture2DOES" );
self.egl_create_fn = self.load_proc::<EglCreateImageKHR>( "eglCreateImageKHR" );
self.egl_destroy_fn = self.load_proc::<EglDestroyImageKHR>( "eglDestroyImageKHR" );
self.egl_get_current_display_fn = self.load_proc::<EglGetCurrentDisplay>( "eglGetCurrentDisplay" );
}
let bind = self.bind_image_fn?;
let egl_create = self.egl_create_fn?;
let egl_destroy = self.egl_destroy_fn?;
let egl_dpy_fn = self.egl_get_current_display_fn?;
let ltk_egl_display = unsafe { egl_dpy_fn() };
if ltk_egl_display.is_null()
{
static mut WARNED: bool = false;
unsafe { if !WARNED { eprintln!( "[ltk-webkit] eglGetCurrentDisplay returned NULL" ); WARNED = true; } };
return None;
}
release_orphans( gl, ltk_egl_display, egl_destroy );
if *self.copy_path.get_or_insert_with( || software::wanted( gl ) )
{
return self.copy_pending( gl );
}
if let Some( pending ) = self.take_shown_buffer()
{
match unsafe { import_dmabuf_as_image( pending, ltk_egl_display, egl_create ) }
{
Ok( image ) =>
{
if let Some( old_image ) = self.bound_image.take()
{
unsafe { egl_destroy( ltk_egl_display, old_image ); }
}
if let Some( old_buf ) = self.bound_buffer.take()
{
unsafe
{
if !self.view.is_null()
{
wpe_view_buffer_released( self.view, old_buf );
}
g_object_unref( old_buf as *mut _ );
}
}
self.bound_buffer = Some( pending );
self.bound_image = Some( image );
dbg_log( "buffer imported" );
}
Err( msg ) =>
{
eprintln!( "[ltk-webkit] DMA-BUF import failed: {}", msg );
unsafe
{
if !self.view.is_null()
{
wpe_view_buffer_released( self.view, pending );
}
g_object_unref( pending as *mut _ );
}
}
}
}
let image = self.bound_image?;
if self.gl_texture.is_none()
{
let tex = unsafe { gl.create_texture() }.ok()?;
unsafe
{
gl.bind_texture( GL_TEXTURE_2D, Some( tex ) );
gl.tex_parameter_i32( GL_TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32 );
gl.tex_parameter_i32( GL_TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32 );
gl.tex_parameter_i32( GL_TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
gl.tex_parameter_i32( GL_TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
}
self.gl_texture = Some( tex );
}
let tex = self.gl_texture?;
unsafe
{
gl.bind_texture( GL_TEXTURE_2D, Some( tex ) );
bind( GL_TEXTURE_2D, image );
let err = gl.get_error();
if err != 0
{
static mut WARNED_BIND: bool = false;
if !WARNED_BIND
{
eprintln!( "[ltk-webkit] glEGLImageTargetTexture2DOES error: 0x{:x}", err );
WARNED_BIND = true;
}
}
}
static mut FIRST: bool = true;
unsafe { if FIRST { eprintln!( "[ltk-webkit] first texture composited" ); FIRST = false; } };
Some( tex )
}
fn take_shown_buffer( &mut self ) -> Option<*mut WPEBuffer>
{
if self.print_job.is_some() { None } else { self.pending_buffer.take() }
}
fn copy_pending( &mut self, gl: &glow::Context ) -> Option<glow::Texture>
{
if let Some( pending ) = self.take_shown_buffer()
{
match software::upload( gl, &mut self.gl_texture, &mut self.copy_size, pending )
{
Ok( () ) => dbg_log( "buffer copied" ),
Err( msg ) => eprintln!( "[ltk-webkit] buffer copy failed: {}", msg ),
}
unsafe
{
if !self.view.is_null()
{
wpe_view_buffer_released( self.view, pending );
}
g_object_unref( pending as *mut _ );
}
}
if self.copy_size == ( 0, 0 ) { None } else { self.gl_texture }
}
fn load_proc<F>( &self, name: &str ) -> Option<F>
{
let raw = self.egl_instance.get_proc_address( name )?;
Some( unsafe { std::mem::transmute_copy::<extern "system" fn(), F>( &raw ) } )
}
}
unsafe fn import_dmabuf_as_image(
buffer: *mut WPEBuffer,
egl_display: *mut c_void,
egl_create: EglCreateImageKHR,
) -> Result<*mut c_void, String>
{
let is_dmabuf = unsafe
{
g_type_check_instance_is_a(
buffer as *mut GTypeInstance,
wpe_buffer_dma_buf_get_type(),
)
} != 0;
if !is_dmabuf
{
return Err( "buffer is not WPEBufferDMABuf (SHM not supported here)".into() );
}
let dmabuf = buffer as *mut WPEBufferDMABuf;
let ( w, h, fourcc, n_planes, modifier ) = unsafe
{
(
wpe_buffer_get_width( buffer ),
wpe_buffer_get_height( buffer ),
wpe_buffer_dma_buf_get_format( dmabuf ),
wpe_buffer_dma_buf_get_n_planes( dmabuf ),
wpe_buffer_dma_buf_get_modifier( dmabuf ),
)
};
if !( 1..=4 ).contains( &n_planes )
{
return Err( format!( "unsupported plane count {}", n_planes ) );
}
dbg_log( &format!(
"dmabuf {}x{} fourcc=0x{:08x} modifier={:#x} planes={} pitch={}",
w, h, fourcc, modifier, n_planes, unsafe { wpe_buffer_dma_buf_get_stride( dmabuf, 0 ) },
) );
let mut attribs: Vec<c_int> = Vec::with_capacity( 7 + ( n_planes as usize ) * 10 + 1 );
attribs.push( EGL_WIDTH ); attribs.push( w );
attribs.push( EGL_HEIGHT ); attribs.push( h );
attribs.push( EGL_LINUX_DRM_FOURCC_EXT ); attribs.push( fourcc as c_int );
let mod_lo = ( modifier & 0xFFFF_FFFF ) as c_int;
let mod_hi = ( ( modifier >> 32 ) & 0xFFFF_FFFF ) as c_int;
for i in 0..n_planes as usize
{
let fd = unsafe { wpe_buffer_dma_buf_get_fd ( dmabuf, i as u32 ) };
let offset = unsafe { wpe_buffer_dma_buf_get_offset( dmabuf, i as u32 ) };
let pitch = unsafe { wpe_buffer_dma_buf_get_stride( dmabuf, i as u32 ) };
attribs.push( PLANE_FD[ i ] ); attribs.push( fd );
attribs.push( PLANE_OFF[ i ] ); attribs.push( offset as c_int );
attribs.push( PLANE_PITCH[ i ] ); attribs.push( pitch as c_int );
if modifier != 0 && modifier != u64::MAX
{
attribs.push( PLANE_MOD_LO[ i ] ); attribs.push( mod_lo );
attribs.push( PLANE_MOD_HI[ i ] ); attribs.push( mod_hi );
}
}
attribs.push( EGL_NONE );
let image = unsafe
{
egl_create(
egl_display,
ptr::null_mut(),
EGL_LINUX_DMA_BUF_EXT,
ptr::null_mut(),
attribs.as_ptr(),
)
};
if image.is_null()
{
return Err( format!( "eglCreateImageKHR returned NULL ({}x{}, fourcc=0x{:08x}, mod={:#x}, planes={})", w, h, fourcc, modifier, n_planes ) );
}
Ok( image )
}
impl Drop for Inner
{
fn drop( &mut self )
{
unsafe
{
if let Some( buf ) = self.pending_buffer.take() { g_object_unref( buf as *mut _ ); }
if let Some( buf ) = self.bound_buffer.take() { g_object_unref( buf as *mut _ ); }
if !self.webview.is_null() { g_object_unref( self.webview as *mut _ ); }
if !self.display.is_null() { g_object_unref( self.display as *mut _ ); }
}
let _ = ORPHANS.try_with( |orphans|
{
let mut orphans = orphans.borrow_mut();
orphans.images.extend( self.bound_image.take() );
orphans.textures.extend( self.gl_texture.take() );
} );
}
}
#[ derive( Default ) ]
struct Orphans
{
images: Vec<*mut c_void>,
textures: Vec<glow::Texture>,
}
thread_local!
{
static ORPHANS: RefCell<Orphans> = RefCell::new( Orphans::default() );
}
fn release_orphans( gl: &glow::Context, egl_display: *mut c_void, egl_destroy: EglDestroyImageKHR )
{
let orphans = ORPHANS.with( |orphans| std::mem::take( &mut *orphans.borrow_mut() ) );
for texture in orphans.textures
{
unsafe { gl.delete_texture( texture ); }
}
for image in orphans.images
{
unsafe { egl_destroy( egl_display, image ); }
}
}
unsafe extern "C" fn render_buffer_override(
view: *mut WPEView,
buffer: *mut WPEBuffer,
_damage_rects: *const WPERectangle,
_n_damage: c_uint,
_error: *mut *mut GError,
) -> gboolean
{
if view.is_null() || buffer.is_null() { return 1; }
let raw = unsafe
{
g_object_get_data( view as *mut GObject, CURSOR_INNER_QDATA_KEY.as_ptr() as *const c_char )
};
let inner = if raw.is_null()
{
None
} else {
unsafe { &*( raw as *const std::sync::Weak<Mutex<Inner>> ) }.upgrade()
};
let Some( inner ) = inner else
{
unsafe { wpe_view_buffer_rendered( view, buffer ); }
return 1;
};
let replaced = match inner.lock()
{
Ok( mut guard ) =>
{
let old = guard.pending_buffer.take();
unsafe { g_object_ref( buffer as *mut _ ); }
guard.pending_buffer = Some( buffer );
guard.needs_redraw = true;
old
}
Err( _ ) => None,
};
if let Some( old ) = replaced
{
unsafe
{
wpe_view_buffer_released( view, old );
g_object_unref( old as *mut _ );
}
}
unsafe { wpe_view_buffer_rendered( view, buffer ); }
dbg_log( "buffer arrived" );
1
}
fn dbg_log( msg: &str )
{
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *ON.get_or_init( || std::env::var_os( "LTK_WEBKIT_DEBUG" ).is_some() )
{
eprintln!( "[ltk-webkit dbg] {}", msg );
}
}
const CURSOR_INNER_QDATA_KEY: &[ u8 ] = b"ltk-webkit-inner\0";
fn install_view_overrides()
{
static ONCE: Once = Once::new();
ONCE.call_once( ||
{
unsafe
{
let class = g_type_class_ref( wpe_view_headless_get_type() ) as *mut WPEViewClass;
if !class.is_null()
{
( *class ).set_cursor_from_name = Some( cursor_from_name_override );
( *class ).render_buffer = Some( render_buffer_override );
}
}
} );
}
unsafe extern "C" fn cursor_from_name_override( view: *mut WPEView, name: *const c_char )
{
if view.is_null() { return; }
let raw = unsafe
{
g_object_get_data( view as *mut GObject, CURSOR_INNER_QDATA_KEY.as_ptr() as *const c_char )
};
if raw.is_null() { return; }
let weak = unsafe { &*( raw as *const std::sync::Weak<Mutex<Inner>> ) };
let Some( inner ) = weak.upgrade() else { return; };
let mut guard = match inner.lock()
{
Ok( g ) => g,
Err( _ ) => return,
};
let shape = if name.is_null()
{
CursorShape::Default
} else {
let s = unsafe { CStr::from_ptr( name ).to_string_lossy() };
cursor_name_to_shape( &s )
};
guard.cursor_shape = shape;
}
fn cursor_name_to_shape( name: &str ) -> CursorShape
{
use CursorShape::*;
match name
{
"default" => Default,
"context-menu" => ContextMenu,
"help" => Help,
"pointer" => Pointer,
"progress" => Progress,
"wait" => Wait,
"cell" => Cell,
"crosshair" => Crosshair,
"text" => Text,
"vertical-text" => VerticalText,
"alias" => Alias,
"copy" => Copy,
"move" => Move,
"no-drop" => NoDrop,
"not-allowed" => NotAllowed,
"grab" => Grab,
"grabbing" => Grabbing,
"e-resize" => EResize,
"n-resize" => NResize,
"ne-resize" => NeResize,
"nw-resize" => NwResize,
"s-resize" => SResize,
"se-resize" => SeResize,
"sw-resize" => SwResize,
"w-resize" => WResize,
"ew-resize" => EwResize,
"ns-resize" => NsResize,
"nesw-resize" => NeswResize,
"nwse-resize" => NwseResize,
"col-resize" => ColResize,
"row-resize" => RowResize,
"all-scroll" => AllScroll,
"zoom-in" => ZoomIn,
"zoom-out" => ZoomOut,
_ => Default,
}
}
static APP_START: LazyLock<Instant> = LazyLock::new( Instant::now );
fn elapsed_ms() -> u32
{
APP_START.elapsed().as_millis() as u32
}
unsafe extern "C" fn drop_weak_qdata( data: gpointer )
{
if data.is_null() { return; }
drop( unsafe { Box::from_raw( data as *mut std::sync::Weak<Mutex<Inner>> ) } );
}
unsafe fn error_message_or_default( err: *mut GError, default: &str ) -> String
{
if err.is_null() { return default.to_string(); }
unsafe { CStr::from_ptr( ( *err ).message ).to_string_lossy().to_string() }
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn cursor_default_is_default()
{
assert_eq!( cursor_name_to_shape( "default" ), CursorShape::Default );
}
#[ test ]
fn cursor_pointer_is_pointer()
{
assert_eq!( cursor_name_to_shape( "pointer" ), CursorShape::Pointer );
}
#[ test ]
fn cursor_text_is_text()
{
assert_eq!( cursor_name_to_shape( "text" ), CursorShape::Text );
}
#[ test ]
fn cursor_grab_grabbing()
{
assert_eq!( cursor_name_to_shape( "grab" ), CursorShape::Grab );
assert_eq!( cursor_name_to_shape( "grabbing" ), CursorShape::Grabbing );
}
#[ test ]
fn cursor_resize_edges()
{
assert_eq!( cursor_name_to_shape( "n-resize" ), CursorShape::NResize );
assert_eq!( cursor_name_to_shape( "s-resize" ), CursorShape::SResize );
assert_eq!( cursor_name_to_shape( "e-resize" ), CursorShape::EResize );
assert_eq!( cursor_name_to_shape( "ne-resize" ), CursorShape::NeResize );
assert_eq!( cursor_name_to_shape( "se-resize" ), CursorShape::SeResize );
assert_eq!( cursor_name_to_shape( "sw-resize" ), CursorShape::SwResize );
assert_eq!( cursor_name_to_shape( "w-resize" ), CursorShape::WResize );
}
#[ test ]
fn cursor_resize_axes()
{
assert_eq!( cursor_name_to_shape( "ew-resize" ), CursorShape::EwResize );
assert_eq!( cursor_name_to_shape( "ns-resize" ), CursorShape::NsResize );
assert_eq!( cursor_name_to_shape( "nesw-resize" ), CursorShape::NeswResize );
assert_eq!( cursor_name_to_shape( "nwse-resize" ), CursorShape::NwseResize );
assert_eq!( cursor_name_to_shape( "col-resize" ), CursorShape::ColResize );
assert_eq!( cursor_name_to_shape( "row-resize" ), CursorShape::RowResize );
}
#[ test ]
fn cursor_scroll_and_zoom()
{
assert_eq!( cursor_name_to_shape( "all-scroll" ), CursorShape::AllScroll );
assert_eq!( cursor_name_to_shape( "zoom-in" ), CursorShape::ZoomIn );
assert_eq!( cursor_name_to_shape( "zoom-out" ), CursorShape::ZoomOut );
}
#[ test ]
fn cursor_unknown_falls_back_to_default()
{
assert_eq!( cursor_name_to_shape( "none" ), CursorShape::Default );
assert_eq!( cursor_name_to_shape( "bogus" ), CursorShape::Default );
assert_eq!( cursor_name_to_shape( "" ), CursorShape::Default );
}
#[ test ]
fn elapsed_ms_is_monotonic()
{
let a = elapsed_ms();
std::thread::sleep( std::time::Duration::from_millis( 2 ) );
let b = elapsed_ms();
assert!( b >= a );
}
}