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
5 changes: 5 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ pub struct ConnectArgs {
#[arg(long)]
pub undecorated: bool,

/// Do not show the floating toolbar (or its top-edge hover hotspot) in
/// fullscreen. Useful when the guest has panels at the screen edges.
#[arg(long)]
pub no_fullscreen_bar: bool,

/// Use QEMU-provided DMABUF damage rectangles instead of full-surface
/// refreshes. This can be faster, but some guest/driver combinations may
/// flicker.
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async fn run_connect_command(args: ConnectArgs) -> Result<()> {
args.hotkeys.as_deref(),
args.fullscreen,
args.undecorated,
args.no_fullscreen_bar,
args.dmabuf_partial_updates,
)
}
Expand Down
11 changes: 5 additions & 6 deletions src/viewer/chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,11 @@ pub(super) fn sync_fullscreen_chrome(
update_fullscreen_button(button, is_fullscreen);
}

// GTK4 forbids set_titlebar() on a realized window (warning + segfault).
// Keep the titlebar set once at construction and only toggle its visibility;
// GTK hides titlebars in fullscreen anyway.
if is_fullscreen {
window.set_titlebar(None::<&gtk::Widget>);
header_bar.set_visible(false);
fullscreen_hotspot.set_visible(true);
reveal_fullscreen_bar(fullscreen_revealer, fullscreen_state);
schedule_hide_fullscreen_bar(window, fullscreen_revealer, fullscreen_state);
Expand All @@ -351,10 +354,6 @@ pub(super) fn sync_fullscreen_chrome(
fullscreen_revealer.set_reveal_child(false);
fullscreen_revealer.set_visible(false);
fullscreen_hotspot.set_visible(false);
if decorated_window {
window.set_titlebar(Some(header_bar));
} else {
window.set_titlebar(None::<&gtk::Widget>);
}
header_bar.set_visible(decorated_window);
}
}
12 changes: 12 additions & 0 deletions src/viewer/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,13 +703,22 @@ fn premultiply(channel: u8, alpha: u8) -> u8 {
pub(super) struct FrameStreamHandler {
event_tx: EventSender,
framebuffer: Option<Framebuffer>,
disable_notice_pending: bool,
}

impl FrameStreamHandler {
pub(super) fn new(event_tx: EventSender) -> Self {
Self {
event_tx,
framebuffer: None,
disable_notice_pending: false,
}
}

fn clear_disable_notice(&mut self) {
if self.disable_notice_pending {
self.disable_notice_pending = false;
self.send_status("");
}
}

Expand Down Expand Up @@ -794,16 +803,19 @@ impl FrameStreamHandler {
#[cfg(unix)]
pub(super) fn emit_dmabuf_scanout(&mut self, scanout: DmabufFrame) {
self.framebuffer = None;
self.clear_disable_notice();
let _ = self.event_tx.send(ViewerEvent::Dmabuf(scanout));
}

#[cfg(unix)]
pub(super) fn update_dmabuf(&mut self, update: UpdateDMABUF) {
self.clear_disable_notice();
let _ = self.event_tx.send(ViewerEvent::DmabufUpdate(update));
}

pub(super) fn disable(&mut self) {
self.framebuffer = None;
self.disable_notice_pending = true;
self.send_status("The guest display was disabled.");
}

Expand Down
13 changes: 10 additions & 3 deletions src/viewer/listener/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ impl RemoteConsole {
.context("failed to query the mouse mode")
}

pub(super) async fn set_ui_info(&self, width: u32, height: u32) -> Result<()> {
self.proxy
.set_ui_info(0, 0, 0, 0, width, height)
.await
.with_context(|| format!("failed to request a guest resize to {width}x{height}"))
}

pub(super) async fn check_alive(&self) -> Result<()> {
self.proxy
.label()
Expand All @@ -91,9 +98,9 @@ impl RemoteConsole {
.release(keycode)
.await
.with_context(|| format!("failed to send key release for qnum {keycode}")),
InputEvent::ClipboardViewerFocused(_) | InputEvent::ClipboardHostChanged(_, _) => {
Ok(())
}
InputEvent::ClipboardViewerFocused(_)
| InputEvent::ClipboardHostChanged(_, _)
| InputEvent::UiInfo { .. } => Ok(()),
InputEvent::MousePress(button) => self
.mouse
.press(button)
Expand Down
9 changes: 9 additions & 0 deletions src/viewer/listener/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ pub(super) async fn listener_session(
continue;
}

if let InputEvent::UiInfo { width, height } = &input {
if let Err(error) = console.set_ui_info(*width, *height).await {
let _ = event_tx.send(ViewerEvent::Status(format!(
"Guest resize request failed: {error:#}"
)));
}
continue;
}

let needs_mouse_mode = mouse::input_needs_mouse_mode(&input);
if let Err(error) = console.handle_input(input).await {
let recovered = if needs_mouse_mode {
Expand Down
115 changes: 107 additions & 8 deletions src/viewer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub fn connect(
hotkeys_spec: Option<&str>,
start_fullscreen: bool,
undecorated: bool,
no_fullscreen_bar: bool,
dmabuf_partial_updates: bool,
) -> Result<()> {
let hotkeys = hotkeys::ViewerHotkeys::parse(hotkeys_spec)
Expand Down Expand Up @@ -156,6 +157,7 @@ pub fn connect(
hotkeys,
start_fullscreen,
undecorated,
no_fullscreen_bar,
dmabuf_partial_updates,
);

Expand All @@ -176,6 +178,7 @@ fn run_window(
hotkeys: hotkeys::ViewerHotkeys,
start_fullscreen: bool,
undecorated: bool,
no_fullscreen_bar: bool,
dmabuf_partial_updates: bool,
) -> Result<()> {
gtk::init().context("failed to initialize GTK4")?;
Expand Down Expand Up @@ -290,9 +293,13 @@ fn run_window(
.build();
fullscreen_revealer.set_child(Some(&floating_controls.container));
fullscreen_revealer.set_visible(false);
overlay.add_overlay(&fullscreen_revealer);
overlay.set_measure_overlay(&fullscreen_revealer, false);
overlay.set_clip_overlay(&fullscreen_revealer, false);
// --no-fullscreen-bar: leave the floating bar and its hotspot unparented so
// fullscreen has no screen-edge chrome at all.
if !no_fullscreen_bar {
overlay.add_overlay(&fullscreen_revealer);
overlay.set_measure_overlay(&fullscreen_revealer, false);
overlay.set_clip_overlay(&fullscreen_revealer, false);
}

let fullscreen_hotspot = gtk::Box::builder()
.halign(gtk::Align::Center)
Expand All @@ -302,9 +309,11 @@ fn run_window(
.build();
fullscreen_hotspot.set_opacity(0.0);
fullscreen_hotspot.set_visible(false);
overlay.add_overlay(&fullscreen_hotspot);
overlay.set_measure_overlay(&fullscreen_hotspot, false);
overlay.set_clip_overlay(&fullscreen_hotspot, false);
if !no_fullscreen_bar {
overlay.add_overlay(&fullscreen_hotspot);
overlay.set_measure_overlay(&fullscreen_hotspot, false);
overlay.set_clip_overlay(&fullscreen_hotspot, false);
}

let fullscreen_state = Rc::new(RefCell::new(chrome::FullscreenChromeState::default()));
let titlebar_widget = header_bar.clone().upcast::<gtk::Widget>();
Expand Down Expand Up @@ -341,6 +350,91 @@ fn run_window(
}
});

// Auto-resize: forward viewer size changes to the guest via SetUIInfo,
// the same mechanism the SPICE vdagent uses. Debounced so an interactive
// resize sends one final size instead of a flood.
{
let input_tx = input_tx.clone();
let picture_for_send = picture.clone();
let last_sent: Rc<RefCell<(i32, i32)>> = Rc::new(RefCell::new((0, 0)));
let pending: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
let send_ui_info: Rc<dyn Fn()> = Rc::new(move || {
let scale = picture_for_send.scale_factor();
let width = picture_for_send.width() * scale;
let height = picture_for_send.height() * scale;
if width > 0 && height > 0 && *last_sent.borrow() != (width, height) {
*last_sent.borrow_mut() = (width, height);
let _ = input_tx.send(InputEvent::UiInfo {
width: width as u32,
height: height as u32,
});
}
});
let schedule: Rc<dyn Fn()> = Rc::new({
let pending = pending.clone();
move || {
if let Some(source) = pending.borrow_mut().take() {
source.remove();
}
let send_ui_info = send_ui_info.clone();
let pending_inner = pending.clone();
let source = glib::timeout_add_local_once(
std::time::Duration::from_millis(350),
move || {
*pending_inner.borrow_mut() = None;
send_ui_info();
},
);
*pending.borrow_mut() = Some(source);
}
});
window.connect_default_width_notify({
let schedule = schedule.clone();
move |_| schedule()
});
window.connect_default_height_notify({
let schedule = schedule.clone();
move |_| schedule()
});
window.connect_fullscreened_notify({
let schedule = schedule.clone();
move |_| schedule()
});
window.connect_maximized_notify({
let schedule = schedule.clone();
move |_| schedule()
});
// First map: the window may already be fullscreen (--fullscreen) before
// the picture gets an allocation, so no notify above will fire — request
// once the widgets are actually laid out.
window.connect_map({
let schedule = schedule.clone();
move |_| schedule()
});
// The compositor can resize the surface without any of the window
// properties above changing — e.g. the monitor's resolution changes
// while the viewer is fullscreen. The surface `layout` signal is the
// ground truth for actual size changes, so hook it on every realize
// (each realize creates a fresh surface).
window.connect_realize({
let schedule = schedule.clone();
move |window| {
if let Some(surface) = window.surface() {
surface.connect_layout({
let schedule = schedule.clone();
move |_, _, _| schedule()
});
}
}
});
// Moving to a monitor with a different scale changes the physical
// pixel count without a logical resize.
window.connect_scale_factor_notify({
let schedule = schedule.clone();
move |_| schedule()
});
}

let hotspot_motion = gtk::EventControllerMotion::new();
hotspot_motion.connect_enter({
let window = window.clone();
Expand Down Expand Up @@ -842,8 +936,12 @@ fn run_window(
}

if let Some(message) = latest_status {
status_label.set_label(&message);
status_label.set_visible(true);
if message.is_empty() {
status_label.set_visible(false);
} else {
status_label.set_label(&message);
status_label.set_visible(true);
}
}

glib::ControlFlow::Continue
Expand Down Expand Up @@ -999,6 +1097,7 @@ enum InputEvent {
MouseAbs { x: u32, y: u32 },
MouseRel { dx: i32, dy: i32 },
MouseWheel(MouseButton),
UiInfo { width: u32, height: u32 },
}

#[cfg(test)]
Expand Down