use std::time::Instant;
use crate::outputs::{self, OutputId, OutputInfo};
use crate::platform::LockEvent;
use crate::platform::outputs::OutputRegistry;
use crate::reactive;
use crate::renderer::{GpuContext, RenderTarget, Renderer};
use crate::surface::{SurfaceConfig, SurfaceId};
use crate::surface_manager::{ManagedSurface, SurfaceManager};
use crate::tree::{Tree, WidgetId};
use crate::widgets::{Event, MouseButton, Rect, Widget};
use crate::{Frame, LoopContext, Platform, Surface, iterate};
#[derive(Default)]
struct RecordedSurface {
parent: Option<SurfaceId>,
grab: bool,
width: u32,
height: u32,
scale: f32,
configured: bool,
events: Vec<(Instant, Event)>,
pacing: crate::surface::FramePacing,
callbacks_held: bool,
exclusive_zones: Vec<i32>,
input_regions: Vec<Option<Vec<Rect>>>,
input_requests: Vec<crate::region::InputRegionRequest>,
blur_regions: Vec<Vec<Rect>>,
sizes_asked: Vec<(u32, u32)>,
viewport_destinations: Vec<(u32, u32)>,
frame_callbacks: u32,
}
#[derive(Default)]
struct RecordedLock {
asked: bool,
granted: bool,
requests: Vec<(SurfaceId, OutputId, bool)>,
events: Vec<LockEvent>,
}
#[derive(Default)]
struct Recorder {
surfaces: rustc_hash::FxHashMap<SurfaceId, RecordedSurface>,
created: Vec<SurfaceId>,
destroyed: Vec<SurfaceId>,
connectors: Vec<String>,
outputs: OutputRegistry<String>,
lock: RecordedLock,
cursors: Vec<crate::reactive::CursorIcon>,
clipboard: Option<String>,
primary: Option<String>,
selection_reads: Vec<crate::reactive::SelectionKind>,
targets_built: std::cell::RefCell<rustc_hash::FxHashMap<SurfaceId, Vec<(u32, u32)>>>,
}
impl Recorder {
fn offer(&mut self, kind: crate::reactive::SelectionKind, text: String) {
*self.selection(kind) = Some(text);
if kind == crate::reactive::SelectionKind::Clipboard {
crate::reactive::clipboard::set_clipboard_offered(true);
}
}
fn selection(&mut self, kind: crate::reactive::SelectionKind) -> &mut Option<String> {
match kind {
crate::reactive::SelectionKind::Clipboard => &mut self.clipboard,
crate::reactive::SelectionKind::Primary => &mut self.primary,
}
}
fn get(&self, id: SurfaceId) -> &RecordedSurface {
self.surfaces.get(&id).unwrap_or_else(|| missing(id))
}
fn get_mut(&mut self, id: SurfaceId) -> &mut RecordedSurface {
self.surfaces.get_mut(&id).unwrap_or_else(|| missing(id))
}
fn connect_output(&mut self, name: &str) -> OutputId {
self.connectors.push(name.to_string());
let id = self.outputs.add(name.to_string());
self.publish_outputs();
id
}
fn disconnect_output(&mut self, id: OutputId) {
let Some(at) = self
.connectors
.iter()
.position(|name| self.outputs.id_for(name) == Some(id))
else {
return;
};
self.outputs.remove(&self.connectors[at]);
outputs::output_removed(id);
self.publish_outputs();
self.connectors.remove(at);
}
fn publish_outputs(&self) {
outputs::sync_outputs(self.outputs.connected(
self.connectors.iter().map(|name| (name.clone(), name)),
|name, id| Some(OutputInfo::named(id, name)),
));
}
}
fn missing(id: SurfaceId) -> ! {
panic!("surface {id:?} was never declared, or has been closed")
}
impl Surface for &mut RecordedSurface {
fn open_frame(&mut self) -> Option<Frame> {
if !self.configured {
return None;
}
Some(Frame {
events: std::mem::take(&mut self.events),
scale_factor: self.scale,
width: self.width,
height: self.height,
pacing: self.pacing,
})
}
fn pacing(&self) -> crate::surface::FramePacing {
self.pacing
}
fn configured_size(&self) -> Option<(u32, u32)> {
self.configured.then_some((self.width, self.height))
}
fn scale_factor(&self) -> f32 {
self.scale
}
fn set_size(&mut self, width: u32, height: u32) {
self.sizes_asked.push((width, height));
}
fn set_exclusive_zone(&mut self, zone: i32) {
self.exclusive_zones.push(zone);
}
fn set_viewport_destination(&mut self, width: u32, height: u32) {
self.viewport_destinations.push((width, height));
}
fn set_input_region(&mut self, rects: Option<&[Rect]>) {
self.input_regions.push(rects.map(<[Rect]>::to_vec));
}
fn sync_input_region(&mut self, request: &crate::region::InputRegionRequest, _commit: bool) {
self.input_requests.push(request.clone());
}
fn has_published_blur(&self) -> bool {
self.blur_regions
.last()
.is_some_and(|rects| !rects.is_empty())
}
fn sync_blur_region(&mut self, rects: Vec<crate::region::RegionRect>, _commit: bool) {
self.blur_regions.push(
rects
.iter()
.map(|r| Rect::new(r.x as f32, r.y as f32, r.width as f32, r.height as f32))
.collect(),
);
}
fn request_frame_callback(&mut self) {
self.frame_callbacks += 1;
}
fn mark_frame_callback_pending(&mut self) {
self.pacing.committed();
}
}
impl Platform for Recorder {
type Surface<'a> = &'a mut RecordedSurface;
fn surface(&mut self, id: SurfaceId) -> Option<&mut RecordedSurface> {
self.surfaces.get_mut(&id)
}
fn supports_blur_region(&self) -> bool {
true
}
fn create_surface(&mut self, id: SurfaceId, config: &crate::surface::SurfaceConfig) {
let declared = crate::surface::initial_declaration(config);
self.created.push(id);
self.surfaces.insert(
id,
RecordedSurface {
scale: 1.0,
sizes_asked: vec![declared.asked],
exclusive_zones: vec![declared.exclusive_zone],
input_regions: Vec::from_iter(declared.input_region.map(Some)),
..Default::default()
},
);
}
fn create_popup(
&mut self,
id: SurfaceId,
parent: SurfaceId,
config: &crate::surface::PopupConfig,
size: (u32, u32),
) -> bool {
self.created.push(id);
self.surfaces.insert(
id,
RecordedSurface {
parent: Some(parent),
grab: config.grab,
width: size.0,
height: size.1,
scale: 1.0,
configured: true,
..Default::default()
},
);
true
}
fn destroy_surface(&mut self, id: SurfaceId) {
self.destroyed.push(id);
self.surfaces.remove(&id);
}
fn popup_descendants_bottom_up(&self, root: SurfaceId) -> Vec<SurfaceId> {
crate::descendants_bottom_up(
root,
self.surfaces
.iter()
.filter_map(|(id, surface)| surface.parent.map(|parent| (*id, parent))),
)
}
fn conflicting_grab_popups(&self, new_parent: SurfaceId) -> Vec<SurfaceId> {
crate::conflicting_grabs(
new_parent,
self.surfaces.iter().filter_map(|(id, surface)| {
surface.parent.map(|parent| (*id, parent, surface.grab))
}),
)
}
fn start_session_lock(&mut self) -> bool {
if self.lock.asked {
return false;
}
self.lock.asked = true;
true
}
fn create_lock_surface(&mut self, id: SurfaceId, output: OutputId) -> bool {
let accepted = self.lock.asked
&& self
.connectors
.iter()
.any(|name| self.outputs.id_for(name) == Some(output));
self.lock.requests.push((id, output, accepted));
if !accepted {
return false;
}
self.created.push(id);
self.surfaces.insert(
id,
RecordedSurface {
scale: 1.0,
..Default::default()
},
);
true
}
fn set_cursor(&mut self, cursor: crate::reactive::CursorIcon) {
self.cursors.push(cursor);
}
fn set_clipboard(&mut self, text: String) {
self.offer(crate::reactive::SelectionKind::Clipboard, text);
}
fn set_primary(&mut self, text: String) {
self.offer(crate::reactive::SelectionKind::Primary, text);
}
fn read_selection(&mut self, kind: crate::reactive::SelectionKind, token: u64) {
self.selection_reads.push(kind);
let offered = self.selection(kind).clone();
crate::reactive::clipboard::answer_paste(token, offered);
}
fn take_lock_events(&mut self) -> Vec<LockEvent> {
std::mem::take(&mut self.lock.events)
}
fn unlock_session(&mut self) {
self.lock.asked = false;
self.lock.granted = false;
}
fn create_render_target(
&self,
id: SurfaceId,
gpu: &crate::renderer::GpuContext,
size: (u32, u32),
) -> Option<crate::renderer::RenderTarget> {
self.targets_built
.borrow_mut()
.entry(id)
.or_default()
.push(size);
Some(crate::renderer::RenderTarget::offscreen(
gpu, size.0, size.1,
))
}
}
pub struct Headless {
gpu: crate::renderer::GpuSlot,
tree: Tree,
renderer: Option<Renderer>,
surfaces: SurfaceManager,
host: Recorder,
layout_roots: rustc_hash::FxHashMap<WidgetId, Vec<WidgetId>>,
quit_on_last_surface: bool,
}
fn shared_device() -> Option<&'static GpuContext> {
static GPU: std::sync::OnceLock<Option<GpuContext>> = std::sync::OnceLock::new();
GPU.get_or_init(GpuContext::try_new).as_ref()
}
impl Headless {
pub fn new() -> Option<Self> {
let gpu = crate::renderer::GpuSlot::Shared(shared_device()?);
reactive::create_root_owner();
Some(Self {
gpu,
tree: Tree::new(),
renderer: None,
surfaces: SurfaceManager::new(),
host: Recorder::default(),
layout_roots: rustc_hash::FxHashMap::default(),
quit_on_last_surface: true,
})
}
pub fn own_gpu(&mut self) {
self.gpu = crate::renderer::GpuSlot::lazy();
}
pub fn without_gpu(&mut self) {
self.gpu = crate::renderer::GpuSlot::Absent;
}
pub fn lock_asked(&self) -> bool {
self.host.lock.asked
}
pub fn holds_gpu(&self) -> bool {
self.gpu.held().is_some() && self.renderer.is_some()
}
pub fn quit_on_last_surface(&mut self, quit: bool) {
self.quit_on_last_surface = quit;
}
pub fn surface<W, F>(&mut self, config: SurfaceConfig, widget_fn: F) -> SurfaceId
where
W: Widget + 'static,
F: FnOnce() -> W,
{
let (widget, owner) = reactive::with_owner(|| Box::new(widget_fn()) as Box<dyn Widget>);
let id = SurfaceId::next();
self.host.create_surface(id, &config);
self.surfaces.add(ManagedSurface::new(
id,
config,
widget,
owner,
&mut self.tree,
));
id
}
pub fn connect_output(&mut self, name: &str) -> OutputId {
self.host.connect_output(name)
}
pub fn disconnect_output(&mut self, id: OutputId) {
self.host.disconnect_output(id)
}
pub fn enter_output(&mut self, surface: SurfaceId, output: OutputId) {
outputs::surface_entered_output(surface, output);
}
pub fn configure(&mut self, id: SurfaceId, width: u32, height: u32, scale: f32) {
self.configure_size(id, width, height);
self.send_scale(id, scale);
}
pub fn configure_size(&mut self, id: SurfaceId, width: u32, height: u32) {
let surface = self.host.get_mut(id);
surface.width = width;
surface.height = height;
surface.configured = true;
}
pub fn hold_frame_callbacks(&mut self, id: SurfaceId) {
self.host.get_mut(id).callbacks_held = true;
}
pub fn release_frame_callbacks(&mut self, id: SurfaceId) {
self.host.get_mut(id).callbacks_held = false;
}
pub fn next_wait(&mut self) -> Option<std::time::Duration> {
crate::loop_timeout(
&mut self.host,
&self.surfaces,
&self.gpu,
false,
Instant::now(),
)
}
pub fn send_scale(&mut self, id: SurfaceId, scale: f32) {
let surface = self.host.get_mut(id);
surface.scale = scale;
}
pub fn click(&mut self, id: SurfaceId, x: f32, y: f32) {
let now = Instant::now();
self.event_at(id, Event::mouse_down(x, y, MouseButton::Left), now);
self.event_at(id, Event::mouse_up(x, y, MouseButton::Left), now);
}
pub fn event_at(&mut self, id: SurfaceId, event: Event, at: Instant) {
self.host.get_mut(id).events.push((at, event));
}
pub fn step(&mut self) -> Option<crate::ExitReason> {
self.step_at(Instant::now())
}
pub fn step_at(&mut self, at: Instant) -> Option<crate::ExitReason> {
for surface in self.host.surfaces.values_mut() {
if !surface.callbacks_held && surface.pacing.callback_pending() {
surface.pacing.callback_done();
}
}
let ctx = LoopContext {
wayland_state: &mut self.host,
surface_manager: &mut self.surfaces,
gpu: &mut self.gpu,
renderer: &mut self.renderer,
quit_on_last_surface: self.quit_on_last_surface,
};
iterate(ctx, &mut self.tree, &mut self.layout_roots, Some(at))
}
fn root(&self, id: SurfaceId) -> WidgetId {
self.surfaces
.get(id)
.unwrap_or_else(|| missing(id))
.widget_id
}
fn target(&self, id: SurfaceId) -> &RenderTarget {
self.surfaces
.get(id)
.and_then(|s| s.wgpu_surface.as_ref())
.expect("no target; step once after configuring")
}
pub fn adapter_name(&self) -> &str {
self.gpu.held().map_or("", |gpu| &gpu.adapter_info.name)
}
pub fn root_size(&self, id: SurfaceId) -> (f32, f32) {
let bounds = self.tree.get_bounds(self.root(id)).unwrap_or_default();
(bounds.width, bounds.height)
}
pub fn physical_size(&self, id: SurfaceId) -> (u32, u32) {
let target = self.target(id);
(target.width(), target.height())
}
pub fn exclusive_zones_asked(&self, id: SurfaceId) -> &[i32] {
&self.host.get(id).exclusive_zones
}
pub fn input_regions_asked(&self, id: SurfaceId) -> &[Option<Vec<Rect>>] {
&self.host.get(id).input_regions
}
pub fn input_reaches(&self, id: SurfaceId, x: f32, y: f32) -> bool {
let (x, y) = (x.floor() as i32, y.floor() as i32);
let surface = self.host.get(id);
let Some(request) = surface.input_requests.last() else {
return match surface.input_regions.last() {
None | Some(None) => true,
Some(Some(rects)) => rects.iter().any(|r| r.contains(x as f32, y as f32)),
};
};
let mut takes = request.base.iter().any(|r| r.contains(x, y));
for op in &request.ops {
if op.rect.contains(x, y) {
takes = op.takes;
}
}
takes
}
pub fn input_regions_published(&self, id: SurfaceId) -> usize {
self.host.get(id).input_requests.len()
}
pub fn blur_regions_asked(&self, id: SurfaceId) -> &[Vec<Rect>] {
&self.host.get(id).blur_regions
}
pub fn sizes_asked(&self, id: SurfaceId) -> &[(u32, u32)] {
&self.host.get(id).sizes_asked
}
pub fn viewport_destinations(&self, id: SurfaceId) -> &[(u32, u32)] {
&self.host.get(id).viewport_destinations
}
pub fn targets_built(&self, id: SurfaceId) -> Vec<(u32, u32)> {
self.host
.targets_built
.borrow()
.get(&id)
.cloned()
.unwrap_or_default()
}
pub fn frames_presented(&self, id: SurfaceId) -> u32 {
self.host.get(id).frame_callbacks
}
pub fn surfaces_created(&self) -> &[SurfaceId] {
&self.host.created
}
pub fn surfaces_destroyed(&self) -> &[SurfaceId] {
&self.host.destroyed
}
pub fn surfaces_live(&self) -> Vec<SurfaceId> {
let mut ids: Vec<SurfaceId> = self.surfaces.ids().collect();
ids.sort_by_key(SurfaceId::raw);
ids
}
pub fn cursors_asked(&self) -> &[crate::reactive::CursorIcon] {
&self.host.cursors
}
pub fn offer_selection(&mut self, kind: crate::reactive::SelectionKind, text: &str) {
self.host.offer(kind, text.to_owned());
}
pub fn selection_reads(&self) -> &[crate::reactive::SelectionKind] {
&self.host.selection_reads
}
pub fn grant_lock(&mut self) {
self.host.lock.granted = true;
self.host.lock.events.push(LockEvent::Locked);
}
pub fn finish_lock(&mut self) {
crate::Platform::unlock_session(&mut self.host);
self.host.lock.events.push(LockEvent::Finished);
}
pub fn is_locked(&self) -> bool {
self.host.lock.granted
}
pub fn lock_surface_requests(&self) -> &[(SurfaceId, OutputId, bool)] {
&self.host.lock.requests
}
pub fn lock_surfaces_created(&self) -> Vec<(SurfaceId, OutputId)> {
self.host
.lock
.requests
.iter()
.filter(|(_, _, accepted)| *accepted)
.map(|(id, output, _)| (*id, *output))
.collect()
}
pub fn read_pixel(&self, id: SurfaceId, x: u32, y: u32) -> [u8; 4] {
match self.target(id) {
RenderTarget::Offscreen(offscreen) => offscreen.read_pixel(x, y),
RenderTarget::Swapchain(_) => panic!("a headless surface has no swapchain"),
}
}
pub fn read_frame(&self, id: SurfaceId) -> Vec<u8> {
match self.target(id) {
RenderTarget::Offscreen(offscreen) => offscreen.read_pixels(),
RenderTarget::Swapchain(_) => panic!("a headless surface has no swapchain"),
}
}
pub fn hold_image_decodes(&self) -> DecodeHold {
crate::image_decode::hold()
}
pub fn wait_for_image_decodes(&self) {
crate::image_decode::wait();
}
pub fn image_decodes_started(&self) -> u64 {
crate::image_decode::started()
}
pub fn image_bytes_held(&self) -> usize {
crate::image_decode::held_bytes()
}
pub fn image_textures(&self) -> usize {
self.renderer
.as_ref()
.map_or(0, |renderer| renderer.image_textures())
}
pub fn image_atlas_pages(&self) -> usize {
self.renderer
.as_ref()
.map_or(0, |renderer| renderer.image_atlas_pages())
}
pub fn forget_image_textures(&mut self) {
if let Some(renderer) = self.renderer.as_mut() {
renderer.forget_image_textures();
}
}
}
pub use crate::image_decode::DecodeHold;
pub fn finish_image_decodes() -> u64 {
crate::image_decode::wait();
crate::image_decode::started()
}
#[cfg(test)]
mod the_two_refusals_the_loop_cannot_reach {
use super::*;
#[test]
fn a_cover_is_refused_without_a_lock_and_for_a_monitor_that_is_not_there() {
let mut recorder = Recorder::default();
let screen = recorder.connect_output("eDP-1");
let phantom = OutputId::from_raw(9);
assert!(
!recorder.create_lock_surface(SurfaceId::next(), screen),
"no lock has been asked for yet"
);
assert!(recorder.start_session_lock());
assert!(
!recorder.create_lock_surface(SurfaceId::next(), phantom),
"an output the registry never gave that id to"
);
assert!(
recorder.create_lock_surface(SurfaceId::next(), screen),
"and the monitor that is really there is covered"
);
let accepted: Vec<bool> = recorder
.lock
.requests
.iter()
.map(|(_, _, accepted)| *accepted)
.collect();
assert_eq!(
accepted,
[false, false, true],
"every ask is kept, refused or not"
);
}
}
impl Drop for Headless {
fn drop(&mut self) {
drop(std::mem::take(&mut self.surfaces));
drop(self.renderer.take());
crate::reset_thread_state(&mut self.tree);
}
}
#[cfg(test)]
mod one_device_for_the_binary {
use super::*;
#[test]
fn two_applications_are_handed_the_same_device() {
let Some(first) = crate::or_skip(Headless::new()) else {
return;
};
let second = Headless::new().expect("the first one had an adapter");
assert!(
std::ptr::eq(first.gpu.held().unwrap(), second.gpu.held().unwrap()),
"a second application built a context of its own"
);
}
}
#[cfg(test)]
mod the_next_application_on_this_thread {
use super::*;
use crate::layout::Flex;
use crate::widgets::{Color, container};
fn rows() -> impl Widget + 'static {
container()
.layout(Flex::column())
.children((0..200).map(|_| {
container()
.width(40.0)
.height(10.0)
.background(Color::WHITE)
}))
}
#[test]
fn does_not_inherit_the_last_one_tearing_its_widgets_down() {
let Some(mut first) = crate::or_skip(Headless::new()) else {
return;
};
let white = drew_a_row(&mut first);
assert_eq!(white, [255, 255, 255, 255], "the first application's row");
drop(first);
let mut second = Headless::new().expect("the first one had an adapter");
assert_eq!(
drew_a_row(&mut second),
white,
"the second application's row is not the one the first one drew"
);
}
fn drew_a_row(app: &mut Headless) -> [u8; 4] {
let id = app.surface(
SurfaceConfig::new()
.width(100)
.height(200)
.background_color(Color::BLACK),
rows,
);
app.configure(id, 100, 200, 1.0);
app.step();
app.read_pixel(id, 20, 25)
}
}