Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/app/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ impl ApplicationImpl for Application {
video,
#[weak]
webview,
move |refresh_rate, scale_factor| {
move |refresh_rate, scale_factor: f64| {
if let Some(ref browser) = *browser.borrow() {
browser.set_monitor_info(refresh_rate, scale_factor);
video.set_property("scale-factor", scale_factor);
webview.set_property("scale-factor", scale_factor);
video.set_property("scale-factor", scale_factor.ceil() as i32);
}
// Trigger size_allocate to recompute device pixel dimensions
webview.queue_allocate();
}
));

Expand Down Expand Up @@ -230,9 +231,9 @@ impl ApplicationImpl for Application {
});

let browser = self.browser.clone();
webview.connect_resized(move |width, height| {
webview.connect_resized(move |width, height, scale| {
if let Some(ref browser) = *browser.borrow() {
browser.resize(width, height);
browser.resize(width, height, scale);
}
});

Expand Down
190 changes: 85 additions & 105 deletions src/app/webview/imp.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,41 @@
use std::{cell::Cell, rc::Rc};
use std::{cell::RefCell, rc::Rc};

use adw::subclass::prelude::*;
use crossbeam_queue::SegQueue;
use epoxy::types::{GLint, GLuint};
use gtk::{
DropTarget,
gdk::{DragAction, FileList, GLContext},
glib::{self, ControlFlow, Propagation, Properties},
gdk::{DragAction, FileList, MemoryFormat, MemoryTexture},
glib::{self, Bytes, ControlFlow, Properties},
graphene,
prelude::*,
};

use crate::{
app::webview::gl,
shared::{
Frame,
states::{KeyboardState, PointerState},
},
use crate::shared::{
Frame,
states::{KeyboardState, PointerState},
};

pub const FRAGMENT_SRC: &str = include_str!("shader.frag");
pub const VERTEX_SRC: &str = include_str!("shader.vert");
pub const UPDATES_PER_RENDER: i32 = 8;
const BYTES_PER_PIXEL: usize = 4;

#[derive(Default, Properties)]
#[properties(wrapper_type = super::WebView)]
pub struct WebView {
#[property(get, set)]
scale_factor: Cell<i32>,
program: Cell<GLuint>,
vao: Cell<GLuint>,
vbo: Cell<GLuint>,
pbo: Cell<GLuint>,
texture: Cell<GLuint>,
texture_uniform: Cell<GLint>,
texture_height: Cell<i32>,
texture_width: Cell<i32>,
dummy: std::cell::Cell<i32>,
frame_buffer: RefCell<Vec<u8>>,
frame_width: std::cell::Cell<i32>,
frame_height: std::cell::Cell<i32>,
pub pointer_state: Rc<PointerState>,
pub keyboard_state: Rc<KeyboardState>,
pub frames: Box<SegQueue<Frame>>,
pub resize_callback: RefCell<Option<Box<dyn Fn(i32, i32, f64)>>>,
}

#[glib::object_subclass]
impl ObjectSubclass for WebView {
const NAME: &'static str = "WebView";
type Type = super::WebView;
type ParentType = gtk::GLArea;
type ParentType = gtk::Widget;
}

#[glib::derived_properties]
Expand All @@ -54,108 +45,97 @@ impl ObjectImpl for WebView {

let drop_target = DropTarget::new(FileList::static_type(), DragAction::COPY);
self.obj().add_controller(drop_target);
}
}

impl WidgetImpl for WebView {
fn realize(&self) {
self.parent_realize();

let gl_area = self.obj();
gl_area.make_current();

if gl_area.error().is_some() {
return;
}

let vertex_shader = gl::compile_vertex_shader(VERTEX_SRC);
let fragment_shader = gl::compile_fragment_shader(FRAGMENT_SRC);
let program = gl::create_program(vertex_shader, fragment_shader);
let (vao, vbo) = gl::create_geometry(program);
let pbo = gl::create_pbo();
let (texture, texture_uniform) = gl::create_texture(program, "text_uniform");

self.program.set(program);
self.vao.set(vao);
self.vbo.set(vbo);
self.pbo.set(pbo);
self.texture.set(texture);
self.texture_uniform.set(texture_uniform);

self.obj().add_tick_callback(|webview, _| {
if !webview.imp().frames.is_empty() {
webview.queue_render();
webview.queue_draw();
}

ControlFlow::Continue
});
}
}

fn unrealize(&self) {
unsafe {
epoxy::DeleteProgram(self.program.get());
epoxy::DeleteTextures(1, &self.texture.get());
epoxy::DeleteBuffers(1, &self.vbo.get());
epoxy::DeleteVertexArrays(1, &self.vao.get());
epoxy::DeleteBuffers(1, &self.pbo.get());
}

self.program.take();
self.vao.take();
self.vbo.take();
self.texture.take();
self.texture_uniform.take();
impl WidgetImpl for WebView {
fn snapshot(&self, snapshot: &gtk::Snapshot) {
// Apply pending frames to the CPU buffer
let mut buffer = self.frame_buffer.borrow_mut();

while let Some(frame) = self.frames.pop() {
let width = self.frame_width.get();
let height = self.frame_height.get();

// If full dimensions changed, resize buffer
if frame.full_width != width || frame.full_height != height {
let new_size =
(frame.full_width as usize) * (frame.full_height as usize) * BYTES_PER_PIXEL;
buffer.resize(new_size, 0);
self.frame_width.set(frame.full_width);
self.frame_height.set(frame.full_height);
}

self.parent_unrealize();
}
}
let buf_width = self.frame_width.get() as usize;

impl GLAreaImpl for WebView {
fn render(&self, _: &GLContext) -> Propagation {
let scale_factor = self.scale_factor.get();
let mut redraw = false;
// Blit dirty rect into full buffer
let src_stride = frame.width as usize * BYTES_PER_PIXEL;
let dst_stride = buf_width * BYTES_PER_PIXEL;

for _ in 0..UPDATES_PER_RENDER {
if let Some(frame) = self.frames.pop() {
let width = self.texture_width.get();
let height = self.texture_height.get();
for row in 0..frame.height as usize {
let src_offset = row * src_stride;
let dst_offset =
(frame.y as usize + row) * dst_stride + frame.x as usize * BYTES_PER_PIXEL;

if frame.full_width != width || frame.full_height != height {
gl::resize_texture(self.texture.get(), frame.full_width, frame.full_height);
self.texture_width.set(frame.full_width);
self.texture_height.set(frame.full_height);
if src_offset + src_stride <= frame.buffer.len()
&& dst_offset + src_stride <= buffer.len()
{
buffer[dst_offset..dst_offset + src_stride]
.copy_from_slice(&frame.buffer[src_offset..src_offset + src_stride]);
}

gl::update_texture(
self.pbo.get(),
self.texture.get(),
frame.x,
frame.y,
frame.width,
frame.height,
&frame.buffer,
);

redraw = true;
} else {
break;
}
}

if redraw {
let width = self.texture_width.get();
let height = self.texture_height.get();
let width = self.frame_width.get();
let height = self.frame_height.get();
let expected_size = (width as usize) * (height as usize) * BYTES_PER_PIXEL;

gl::resize_viewport(width * scale_factor, height * scale_factor);
if buffer.len() != expected_size || expected_size == 0 {
return;
}

gl::draw_texture(
self.program.get(),
self.texture.get(),
self.texture_uniform.get(),
self.vao.get(),
);
let stride = width as usize * BYTES_PER_PIXEL;

// CEF renders top-down in BGRA. GdkMemoryTexture expects top-down too.
// MemoryFormat::B8G8R8A8_PREMULTIPLIED matches CEF's BGRA output.
let bytes = Bytes::from(&*buffer);
let texture = MemoryTexture::new(
width,
height,
MemoryFormat::B8g8r8a8Premultiplied,
&bytes,
stride,
);

// Map texture to widget's CSS dimensions — GTK scales to device pixels
let css_width = self.obj().width() as f32;
let css_height = self.obj().height() as f32;

if css_width > 0.0 && css_height > 0.0 {
let rect = graphene::Rect::new(0.0, 0.0, css_width, css_height);
snapshot.append_texture(&texture, &rect);
}
}

Propagation::Proceed
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
self.parent_size_allocate(width, height, baseline);

// Compute device pixel dimensions using fractional scale
let surface = self.obj().native().and_then(|n| n.surface());
let scale = surface.map_or(1.0, |s| s.scale());
let dev_w = (width as f64 * scale).round() as i32;
let dev_h = (height as f64 * scale).round() as i32;

if let Some(callback) = self.resize_callback.borrow().as_ref() {
callback(dev_w, dev_h, scale);
}
}
}
9 changes: 3 additions & 6 deletions src/app/webview/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
mod gl;
mod imp;

use std::{path::PathBuf, rc::Rc};
Expand All @@ -24,7 +23,7 @@ use crate::shared::{

glib::wrapper! {
pub struct WebView(ObjectSubclass<imp::WebView>)
@extends gtk::GLArea, gtk::Widget,
@extends gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

Expand All @@ -44,10 +43,8 @@ impl WebView {
self.imp().frames.push(frame);
}

pub fn connect_resized<T: Fn(i32, i32) + 'static>(&self, callback: T) {
self.connect_resize(move |_, width, height| {
callback(width, height);
});
pub fn connect_resized<T: Fn(i32, i32, f64) + 'static>(&self, callback: T) {
self.imp().resize_callback.replace(Some(Box::new(callback)));
}

pub fn connect_motion<T: Fn(Rc<PointerState>) + 'static>(&self, callback: T) {
Expand Down
27 changes: 21 additions & 6 deletions src/app/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ mod imp;
use adw::subclass::prelude::*;
use gtk::{
Widget,
gdk::prelude::{DisplayExt, MonitorExt},
gdk::prelude::{DisplayExt, MonitorExt, SurfaceExt},
gio,
glib::{self, object::IsA},
prelude::{GtkWindowExt, NativeExt, WidgetExt},
prelude::{GtkWindowExt, NativeExt, ObjectExt, WidgetExt},
};
use url::Url;

Expand Down Expand Up @@ -42,7 +42,9 @@ impl Window {
self.set_fullscreened(fullscreen);
}

pub fn connect_monitor_info<F: Fn(f64, i32) + 'static>(&self, callback: F) {
pub fn connect_monitor_info<F: Fn(f64, f64) + Clone + 'static>(&self, callback: F) {
let realize_callback = callback.clone();
let scale_callback = callback.clone();
self.connect_realize(move |window| {
let display = window.display();
let surface = window.surface();
Expand All @@ -51,9 +53,22 @@ impl Window {
&& let Some(monitor) = display.monitor_at_surface(&surface)
{
let refresh_rate = monitor.refresh_rate() as f64 / 1000.0;
let scale_factor = monitor.scale_factor();

callback(refresh_rate, scale_factor);
let scale_factor = surface.scale();

realize_callback(refresh_rate, scale_factor);

// Listen for fractional scale changes on the surface
// (fires when moving between monitors with different scales)
let cb = scale_callback.clone();
let display = display.clone();
surface.connect_notify_local(Some("scale"), move |surface, _| {
if let Some(monitor) = display.monitor_at_surface(surface) {
let refresh_rate = monitor.refresh_rate() as f64 / 1000.0;
let scale_factor = surface.scale();

cb(refresh_rate, scale_factor);
}
});
}
});
}
Expand Down
17 changes: 9 additions & 8 deletions src/chromium/app/client/render_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ wrap_render_handler! {

impl RenderHandler {
fn screen_info(&self, _browser: Option<&mut Browser>, screen_info: Option<&mut ScreenInfo>) -> i32 {
if let Ok(viewport) = self.viewport.read()
&& let Some(screen_info) = screen_info {
screen_info.device_scale_factor = viewport.scale_factor as f32;

return true.into();
}
if let Some(screen_info) = screen_info {
// CEF OSR doesn't apply device_scale_factor to the paint buffer,
// so we set it to 1.0 and give device pixel dimensions directly
// as view_rect. DPI scaling is handled via zoom level instead.
screen_info.device_scale_factor = 1.0;
return true.into();
}

false.into()
}
Expand All @@ -45,8 +46,8 @@ wrap_render_handler! {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect
&& let Ok(viewport) = self.viewport.read() {
rect.width = viewport.width / viewport.scale_factor;
rect.height = viewport.height / viewport.scale_factor;
rect.width = viewport.width;
rect.height = viewport.height;
}
}

Expand Down
Loading