pub mod animation;
mod app_state;
pub mod backdrop;
mod blur;
pub mod clock;
pub mod compositor;
mod deferred;
pub(crate) mod finite;
pub mod heap;
mod image_decode;
pub mod image_metadata;
mod ingress;
mod jobs;
pub mod keyboard;
pub mod layout;
pub mod outputs;
pub mod pivot;
pub mod reactive;
mod region;
pub mod render_stats;
mod secret;
pub mod session_lock;
pub mod shape;
pub mod surface;
mod surface_manager;
#[cfg(feature = "testing")]
pub mod testing;
pub mod transform;
pub mod tree;
pub mod widget_ref;
pub mod widgets;
pub mod platform;
pub mod renderer;
pub use guido_macros::{SignalFields, component};
pub use secret::Secret;
use std::sync::Arc;
use layout::Constraints;
use platform::create_wayland_app;
use reactive::owner::with_owner;
use reactive::{OwnerId, take_clipboard_change, take_cursor_change};
use renderer::{GpuSlot, Renderer, flatten_root_into};
use surface::{SurfaceCommand, SurfaceConfig, SurfaceId, drain_surface_commands};
use surface_manager::{ManagedSurface, SurfaceManager};
use widgets::Widget;
use widgets::font::FontFamily;
use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::reexports::calloop::channel as calloop_channel;
use smithay_client_toolkit::reexports::calloop::ping::make_ping;
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
pub fn set_default_font_family(family: FontFamily) {
with_app_state(|app| app.default_font_family.set(family));
}
pub fn default_font_family() -> FontFamily {
with_app_state(|app| app.default_font_family.get())
}
pub fn load_font(data: Vec<u8>) {
if with_app_state(|app| app.fonts_consumed.get()) {
log::warn!(
"load_font() called after FontSystem initialization — \
this font will not be available. Call load_font() before App::run()."
);
}
let mut hasher = std::hash::BuildHasher::build_hasher(&rustc_hash::FxBuildHasher);
std::hash::Hasher::write(&mut hasher, &data);
let hash = std::hash::Hasher::finish(&hasher);
with_app_state(|app| {
if app.custom_font_hashes.borrow_mut().insert(hash) {
app.custom_fonts.borrow_mut().push(Arc::new(data));
}
});
}
pub const DEFAULT_IMAGE_CACHE_BUDGET: usize = 100 << 20;
pub fn set_image_cache_budget(bytes: usize) {
with_app_state(|app| app.image_cache_budget.set(Some(bytes)));
}
pub(crate) fn image_cache_budget() -> usize {
with_app_state(|app| app.image_cache_budget.get()).unwrap_or(DEFAULT_IMAGE_CACHE_BUDGET)
}
pub(crate) fn get_registered_fonts() -> Vec<Arc<Vec<u8>>> {
with_app_state(|app| {
app.fonts_consumed.set(true);
app.custom_fonts.borrow().clone()
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExitReason {
Quit,
Restart,
Error(platform::PlatformError),
}
pub fn restart_app() {
jobs::set_exit_request(jobs::ExitRequest::Restart);
}
pub fn quit_app() {
jobs::set_exit_request(jobs::ExitRequest::Quit);
}
pub mod prelude {
pub use crate::animation::{
Animate, Animated, IntoAnimated, Keyframes, Repeat, SpringConfig, TimingFunction,
Transition, TransitionConfig,
};
pub use crate::backdrop::{BackdropBlur, BackdropSources};
pub use crate::compositor::{CompositorEffects, compositor_effects};
pub use crate::keyboard::keyboard_modifiers;
pub use crate::layout::{
Axis, CrossAlignment, Flex, Length, MainAlignment, Size, ZStack, at_least, at_most, fill,
fraction,
};
pub use crate::outputs::{OutputId, OutputInfo, outputs, surface_output};
pub use crate::pivot::{HorizontalAnchor, Pivot, VerticalAnchor};
pub use crate::platform::{Anchor, KeyboardInteractivity, Layer};
pub use crate::reactive::{
Callback, CursorIcon, IntoSignal, IntoVal, Memo, Password, RwSignal, Service, Signal,
TimerHandle, Trigger, WriteSignal, create_derived, create_effect, create_memo,
create_password, create_service, create_signal, create_stored, create_task, create_trigger,
current_owner, expect_context, has_context, on_cleanup, provide_context,
provide_signal_context, set_interval, set_timeout, use_context, with_context,
};
pub use crate::renderer::{Shadow, measure_text};
pub use crate::session_lock::{
LockState, lock_session, lock_state, session_locked, unlock_session,
};
pub use crate::surface::{
ExclusiveZone, Margin, PopupAnchor, PopupConfig, PopupGravity, PopupHandle, SurfaceConfig,
SurfaceExtent, SurfaceHandle, SurfaceId, content, spawn_popup, spawn_surface,
surface_handle,
};
pub use crate::transform::{Scale, Translate};
pub use crate::widget_ref::{WidgetRef, create_widget_ref};
pub use crate::widgets::{
AnyWidget, Border, Color, Container, ContentFit, Control, CornerRadii, Corners, Event,
EventResponse, FontFamily, FontWeight, GradientDirection, Image, ImageSource, IntoChildren,
IntoClickHandler, Key, LineHeight, LinearGradient, Modifiers, MouseButton, Overflow,
Padding, PasswordInput, Point, PointerKind, Rect, RippleConfig, Scroll, ScrollSource,
ScrollbarVisibility, Selection, StateStyle, Stateful, Text, TextAlign, TextInput,
TextOverflow, TextShadow, TextStroke, TextStyle, Widget, container, image, keyed,
password_input, text, text_input,
};
pub use crate::{
App, ExitReason, Secret, SignalFields, component, default_font_family, load_font, quit_app,
restart_app, set_default_font_family, set_image_cache_budget,
};
}
pub mod widget_prelude {
pub use crate::clock::{EventInstant, FrameInstant};
pub use crate::layout::{Constraints, IntoF32, Layout};
pub use crate::reactive::{JobType, with_signal_tracking};
pub use crate::renderer::{PaintContext, RenderNode};
pub use crate::transform::Transform;
pub use crate::tree::{LayoutCtx, LayoutPass, Tree, WidgetId};
pub use crate::widgets::paint_children::{ChildPaintOptions, paint_children};
pub use crate::widgets::{LayoutHints, Widget};
}
use crate::app_state::with_app_state;
use crate::{
jobs::{get_exit_request, init_wakeup, process_jobs, take_wake_request},
tree::{DamageRegion, Tree, WidgetId},
};
#[allow(clippy::type_complexity)]
struct SurfaceDefinition {
id: SurfaceId,
config: SurfaceConfig,
widget_fn: Box<dyn FnOnce() -> Box<dyn Widget>>,
}
fn close_surface_now<P: Platform>(
id: SurfaceId,
surface_manager: &mut SurfaceManager,
wayland_state: &mut P,
tree: &mut Tree,
) {
surface::mark_popup_dismissed(id);
if let Some(managed) = surface_manager.remove(id) {
surface_manager::teardown_widget_subtree(tree, managed.widget_id);
}
wayland_state.destroy_surface(id);
}
fn publish_exclusive_zone<S: Surface>(
surface: &mut S,
config: &SurfaceConfig,
measured: Option<(u32, u32)>,
published: &mut Option<i32>,
) {
if config.exclusive_zone == surface::ExclusiveZone::Auto
&& measured.is_none()
&& surface::needs_content_measure(config)
{
return;
}
let known = measured.or_else(|| surface.configured_size());
let width = surface::requested_extent(config.width, known.map(|(w, _)| w));
let height = surface::requested_extent(config.height, known.map(|(_, h)| h));
let zone = config
.exclusive_zone
.resolve(config.anchor, config.margin, width, height);
if *published != Some(zone) {
surface.set_exclusive_zone(zone);
*published = Some(zone);
}
}
fn process_surface_commands<P: Platform>(
surface_manager: &mut SurfaceManager,
wayland_state: &mut P,
tree: &mut Tree,
quit_on_last_surface: bool,
) -> bool {
let mut closed_surface = false;
for cmd in drain_surface_commands() {
match cmd {
SurfaceCommand::Create {
id,
config,
widget_fn,
} => {
log::info!("Creating dynamic surface {:?}", id);
wayland_state.create_surface(id, &config);
let (widget, owner_id) = with_owner(widget_fn);
let managed = ManagedSurface::new(id, config, widget, owner_id, tree);
surface_manager.add(managed);
}
SurfaceCommand::Close(id) => {
log::info!("Closing dynamic surface {:?}", id);
for child in wayland_state.popup_descendants_bottom_up(id) {
close_surface_now(child, surface_manager, wayland_state, tree);
}
close_surface_now(id, surface_manager, wayland_state, tree);
closed_surface = true;
}
SurfaceCommand::SetLayer { id, layer } => {
with_surface(wayland_state, id, |s| s.set_layer(layer));
}
SurfaceCommand::SetKeyboardInteractivity { id, mode } => {
with_surface(wayland_state, id, |s| s.set_keyboard_interactivity(mode));
}
SurfaceCommand::SetAnchor { id, anchor } => {
let asked = reconfigure(id, surface_manager, wayland_state, |managed, surface| {
managed.config.anchor = anchor;
surface.set_anchor(anchor);
send_size(managed, surface)
});
if let Some((true, root)) = asked {
jobs::request_job(root, jobs::JobRequest::Layout);
}
}
SurfaceCommand::SetSize { id, width, height } => {
let asked = reconfigure(id, surface_manager, wayland_state, |managed, surface| {
managed.config.width = width;
managed.config.height = height;
surface::warn_size_request_on_stretched_axis(id, &managed.config);
send_size(managed, surface)
});
if let Some((true, root)) = asked {
jobs::request_job(root, jobs::JobRequest::Layout);
}
}
SurfaceCommand::SetExclusiveZone { id, zone } => {
let asked = reconfigure(id, surface_manager, wayland_state, |managed, _| {
managed.config.exclusive_zone = zone;
(
surface::needs_content_measure(&managed.config),
managed.widget_id,
)
});
if let Some((true, root)) = asked {
jobs::request_job(root, jobs::JobRequest::Layout);
}
}
SurfaceCommand::SetMargin { id, margin } => {
let asked = reconfigure(id, surface_manager, wayland_state, |managed, surface| {
managed.config.margin = margin;
surface.set_margin(margin);
(
surface::needs_content_measure(&managed.config),
managed.widget_id,
)
});
if let Some((true, root)) = asked {
jobs::request_job(root, jobs::JobRequest::Layout);
}
}
SurfaceCommand::SetInputRegion { id, rects } => {
let mut declaring = false;
if let Some(managed) = surface_manager.get_mut(id) {
managed.input_base = rects.clone();
declaring = managed.input_region.is_some();
}
if declaring {
if let Some(managed) = surface_manager.get(id) {
jobs::request_job(managed.widget_id, jobs::JobRequest::Paint);
}
} else {
with_surface(wayland_state, id, |s| s.set_input_region(rects.as_deref()));
}
}
SurfaceCommand::CreatePopup {
id,
parent,
config,
widget_fn,
} => {
log::info!("Creating popup {:?} anchored to {:?}", id, parent);
let (widget, owner_id) = with_owner(widget_fn);
let surface_config = SurfaceConfig::new()
.width(config.width)
.height(config.height.unwrap_or(1))
.background_color(config.background_color);
let managed = ManagedSurface::new(id, surface_config, widget, owner_id, tree);
let height = config.height.unwrap_or_else(|| {
measure_popup_height(tree, managed.widget_id, config.width, parent)
});
if config.grab {
for stale in wayland_state.conflicting_grab_popups(parent) {
log::info!("Closing popup {:?} before new grab popup {:?}", stale, id);
close_surface_now(stale, surface_manager, wayland_state, tree);
}
}
if wayland_state.create_popup(id, parent, &config, (config.width, height)) {
surface_manager.add(managed);
} else {
drop(managed);
surface::mark_popup_dismissed(id);
}
}
}
}
!(closed_surface && quit_on_last_surface && surface_manager.is_empty())
}
fn measure_popup_height(tree: &mut Tree, root: WidgetId, width: u32, parent: SurfaceId) -> u32 {
measure_natural_size(tree, root, Some(width), None, parent).1
}
fn measure_natural_size(
tree: &mut Tree,
root: WidgetId,
fixed_w: Option<u32>,
fixed_h: Option<u32>,
surface: SurfaceId,
) -> (u32, u32) {
let output_size = outputs::surface_output(surface)
.and_then(|oid| {
outputs::current_outputs()
.into_iter()
.find(|o| o.id == oid)
.and_then(|o| o.logical_size)
})
.or_else(|| {
outputs::current_outputs()
.into_iter()
.find_map(|o| o.logical_size)
});
let cap = |v: i32| (v as f32 - 64.0).max(100.0);
let (cap_w, cap_h) = match output_size {
Some((w, h)) => (cap(w), cap(h)),
None => (800.0, 800.0),
};
let (min_w, max_w) = match fixed_w {
Some(w) => (w as f32, w as f32),
None => (0.0, cap_w),
};
let (min_h, max_h) = match fixed_h {
Some(h) => (h as f32, h as f32),
None => (0.0, cap_h),
};
let constraints = Constraints::new(min_w, min_h, max_w, max_h);
let measured = tree
.measure_widget(root, constraints)
.unwrap_or(layout::Size::new(100.0, 100.0));
let w = fixed_w.unwrap_or_else(|| (measured.width.ceil() as u32).max(1));
let h = fixed_h.unwrap_or_else(|| (measured.height.ceil() as u32).max(1));
(w, h)
}
pub fn cache_paint_results(tree: &mut Tree, node: &std::rc::Rc<renderer::RenderNode>) -> bool {
if !node.repainted.get() {
return false;
}
let mut subtree_partial = node.partial;
for child in &node.children {
subtree_partial |= cache_paint_results(tree, child);
}
let widget_id = WidgetId::from_u64(node.id);
if tree.contains(widget_id) {
if subtree_partial {
tree.clear_cached_paint(widget_id);
} else {
tree.cache_paint(widget_id, std::rc::Rc::clone(node));
}
tree.clear_needs_paint(widget_id);
}
node.repainted.set(false);
subtree_partial
}
fn dispatch_events(
events: &[(std::time::Instant, widgets::Event)],
root: WidgetId,
tree: &mut Tree,
active_roots: &rustc_hash::FxHashSet<WidgetId>,
closing: impl Fn() -> bool,
) {
for (at, event) in events {
if closing() {
break;
}
if matches!(event, widgets::Event::MouseEnter { .. }) {
reactive::cursor::resend_cursor();
}
tree.set_event_instant(Some(*at));
reactive::diagnostics::snapshot_zone(|| {
let response = tree.dispatch(root, event);
if event.coords().is_some() && tree.asked_under_the_point() {
reactive::cursor::point_at(tree.cursor_under_the_point());
}
if matches!(event, widgets::Event::MouseDown { .. })
&& response != Some(widgets::EventResponse::Handled)
&& !tree.focus_claimed_the_press()
{
reactive::focus::release_focus_under(root);
}
});
}
tree.set_event_instant(None);
jobs::distribute_jobs(tree, active_roots);
}
fn sync_platform_state<P: Platform>(wayland_state: &mut P) {
if let Some(text) = take_clipboard_change() {
wayland_state.set_clipboard(text);
}
if let Some(text) = reactive::take_primary_change() {
wayland_state.set_primary(text);
}
if let Some(cursor) = take_cursor_change() {
wayland_state.set_cursor(cursor);
}
for (kind, token) in reactive::clipboard::take_paste_requests() {
wayland_state.read_selection(kind, token);
}
}
fn run_jobs(
root: WidgetId,
tree: &mut Tree,
layout_roots: &mut Vec<WidgetId>,
active_roots: &rustc_hash::FxHashSet<WidgetId>,
) {
let jobs = jobs::drain_surface_jobs(root);
process_jobs(&jobs, tree, layout_roots);
jobs::recycle_job_buffer(jobs);
jobs::distribute_jobs(tree, active_roots);
let followup = jobs::drain_surface_non_animation_jobs(root);
if !followup.is_empty() {
process_jobs(&followup, tree, layout_roots);
}
jobs::recycle_job_buffer(followup);
}
struct Frame {
events: Vec<(std::time::Instant, widgets::Event)>,
scale_factor: f32,
width: u32,
height: u32,
pacing: surface::FramePacing,
}
fn paced_out(frame: &Frame, geometry: &Geometry) -> bool {
frame.pacing.callback_pending() && !geometry.needs_resize && !geometry.scale_changed
}
pub(crate) trait Surface {
fn open_frame(&mut self) -> Option<Frame>;
fn pacing(&self) -> surface::FramePacing;
fn configured_size(&self) -> Option<(u32, u32)> {
None
}
fn scale_factor(&self) -> f32 {
1.0
}
fn popup_auto_width(&self) -> Option<u32> {
None
}
fn reposition_popup_if_changed(&mut self, new_height: u32) {
let _ = new_height;
}
fn set_size(&mut self, width: u32, height: u32) {
let _ = (width, height);
}
fn set_exclusive_zone(&mut self, zone: i32) {
let _ = zone;
}
fn set_viewport_destination(&mut self, width: u32, height: u32) {
let _ = (width, height);
}
fn batch_layer_requests<F: FnOnce(&mut Self)>(&mut self, f: F) {
f(self);
}
fn has_published_blur(&self) -> bool {
false
}
fn take_blur_resync(&mut self) -> bool {
false
}
fn sync_blur_region(&mut self, rects: Vec<region::RegionRect>, commit: bool) {
let _ = (rects, commit);
}
fn sync_input_region(&mut self, request: ®ion::InputRegionRequest, commit: bool) {
let _ = (request, commit);
}
fn request_frame_callback(&mut self) {}
fn damage(&mut self, x: i32, y: i32, width: i32, height: i32) {
let _ = (x, y, width, height);
}
fn mark_frame_callback_pending(&mut self);
fn set_layer(&mut self, layer: platform::Layer) {
let _ = layer;
}
fn set_anchor(&mut self, anchor: platform::Anchor) {
let _ = anchor;
}
fn set_margin(&mut self, margin: surface::Margin) {
let _ = margin;
}
fn set_input_region(&mut self, rects: Option<&[widgets::Rect]>) {
let _ = rects;
}
fn set_keyboard_interactivity(&mut self, mode: platform::KeyboardInteractivity) {
let _ = mode;
}
}
pub(crate) fn descendants_bottom_up(
root: SurfaceId,
edges: impl Iterator<Item = (SurfaceId, SurfaceId)>,
) -> Vec<SurfaceId> {
let edges: Vec<(SurfaceId, SurfaceId)> = edges.collect();
let mut out: Vec<SurfaceId> = Vec::new();
let mut frontier = vec![root];
while let Some(current) = frontier.pop() {
for (popup, parent) in &edges {
if *parent == current {
out.push(*popup);
frontier.push(*popup);
}
}
}
out.sort_unstable_by_key(|id| std::cmp::Reverse(id.raw()));
out
}
pub(crate) fn conflicting_grabs(
new_parent: SurfaceId,
popups: impl Iterator<Item = (SurfaceId, SurfaceId, bool)>,
) -> Vec<SurfaceId> {
let popups: Vec<(SurfaceId, SurfaceId, bool)> = popups.collect();
let mut ancestors = vec![new_parent];
let mut current = new_parent;
while let Some((_, parent, _)) = popups.iter().find(|(popup, _, _)| *popup == current) {
ancestors.push(*parent);
current = *parent;
}
let mut out: Vec<SurfaceId> = Vec::new();
for (popup, _, holds_grab) in &popups {
if *holds_grab && !ancestors.contains(popup) {
out.push(*popup);
out.extend(descendants_bottom_up(
*popup,
popups.iter().map(|(popup, parent, _)| (*popup, *parent)),
));
}
}
out.sort_unstable_by_key(|id| std::cmp::Reverse(id.raw()));
out.dedup();
out
}
#[cfg(test)]
mod teardown_order {
use super::{conflicting_grabs, descendants_bottom_up};
use crate::surface::SurfaceId;
fn ids(n: usize) -> Vec<SurfaceId> {
(0..n).map(|_| SurfaceId::next()).collect()
}
#[test]
fn a_chain_comes_back_deepest_first() {
let s = ids(4);
let edges = vec![(s[1], s[0]), (s[2], s[1]), (s[3], s[2])];
assert_eq!(
descendants_bottom_up(s[0], edges.into_iter()),
vec![s[3], s[2], s[1]]
);
}
#[test]
fn siblings_come_back_newest_first() {
let s = ids(4);
let edges = vec![(s[1], s[0]), (s[2], s[1]), (s[3], s[0])];
assert_eq!(
descendants_bottom_up(s[0], edges.into_iter()),
vec![s[3], s[2], s[1]]
);
}
#[test]
fn the_order_the_edges_arrive_in_does_not_matter() {
let s = ids(4);
let edges = [(s[1], s[0]), (s[2], s[1]), (s[3], s[0])];
let want = vec![s[3], s[2], s[1]];
let mut shuffled = edges;
shuffled.reverse();
assert_eq!(descendants_bottom_up(s[0], shuffled.into_iter()), want);
assert_eq!(
descendants_bottom_up(s[0], [edges[1], edges[2], edges[0]].into_iter()),
want
);
}
#[test]
fn only_what_hangs_from_the_root_comes_back() {
let s = ids(4);
let edges = vec![(s[1], s[0]), (s[3], s[2])];
assert_eq!(descendants_bottom_up(s[0], edges.into_iter()), vec![s[1]]);
}
#[test]
fn a_new_grab_takes_the_chain_it_cannot_nest_under() {
let s = ids(4);
let popups = vec![(s[1], s[0], true), (s[2], s[1], true)];
assert_eq!(
conflicting_grabs(s[0], popups.clone().into_iter()),
vec![s[2], s[1]],
"a grab on the bar itself cannot nest under the menu"
);
assert_eq!(
conflicting_grabs(s[2], popups.into_iter()),
Vec::new(),
"a grab under the current holder nests, and takes nothing down"
);
}
#[test]
fn two_grab_chains_come_down_newest_first() {
let s = ids(5);
let popups = vec![(s[1], s[0], true), (s[2], s[1], true), (s[4], s[3], true)];
let mut reversed = popups.clone();
reversed.reverse();
let want = vec![s[4], s[2], s[1]];
assert_eq!(conflicting_grabs(s[3], popups.into_iter()), want);
assert_eq!(conflicting_grabs(s[3], reversed.into_iter()), want);
}
#[test]
fn a_popup_holding_no_grab_is_left_alone() {
let s = ids(3);
let popups = vec![(s[1], s[0], false), (s[2], s[1], false)];
assert_eq!(conflicting_grabs(s[0], popups.into_iter()), Vec::new());
}
#[test]
fn a_surface_with_no_popups_has_nothing_to_tear_down() {
let s = ids(2);
let edges = vec![(s[1], s[0])];
assert!(descendants_bottom_up(s[1], edges.into_iter()).is_empty());
}
}
pub(crate) trait Platform {
type Surface<'a>: Surface
where
Self: 'a;
fn surface(&mut self, id: SurfaceId) -> Option<Self::Surface<'_>>;
fn supports_blur_region(&self) -> bool {
false
}
fn create_surface(&mut self, id: SurfaceId, config: &SurfaceConfig) {
let _ = (id, config);
}
fn create_popup(
&mut self,
id: SurfaceId,
parent: SurfaceId,
config: &surface::PopupConfig,
size: (u32, u32),
) -> bool {
let _ = (id, parent, config, size);
false
}
fn destroy_surface(&mut self, id: SurfaceId) {
let _ = id;
}
fn conflicting_grab_popups(&self, parent: SurfaceId) -> Vec<SurfaceId> {
let _ = parent;
Vec::new()
}
fn popup_descendants_bottom_up(&self, root: SurfaceId) -> Vec<SurfaceId> {
let _ = root;
Vec::new()
}
fn start_session_lock(&mut self) -> bool {
false
}
fn create_lock_surface(&mut self, id: SurfaceId, output: outputs::OutputId) -> bool {
let _ = (id, output);
false
}
fn take_lock_events(&mut self) -> Vec<platform::LockEvent> {
Vec::new()
}
fn unlock_session(&mut self) {}
fn set_clipboard(&mut self, text: String) {
let _ = text;
}
fn set_primary(&mut self, text: String) {
let _ = text;
}
fn set_cursor(&mut self, cursor: reactive::CursorIcon) {
let _ = cursor;
}
fn read_selection(&mut self, kind: reactive::SelectionKind, token: u64);
fn flush(&self) -> bool {
true
}
fn create_render_target(
&self,
id: SurfaceId,
gpu: &renderer::GpuContext,
size: (u32, u32),
) -> Option<renderer::RenderTarget> {
let _ = (id, gpu, size);
None
}
}
pub(crate) struct WaylandSurface<'a> {
state: &'a mut platform::WaylandState,
id: SurfaceId,
}
impl Surface for WaylandSurface<'_> {
fn open_frame(&mut self) -> Option<Frame> {
let surface = self.state.get_surface_mut(self.id)?;
if !surface.configured {
return None;
}
let events = surface.take_events();
Some(Frame {
events,
scale_factor: surface.scale_factor,
width: surface.width,
height: surface.height,
pacing: surface.pacing,
})
}
fn pacing(&self) -> surface::FramePacing {
self.state
.get_surface(self.id)
.map(|s| s.pacing)
.unwrap_or_default()
}
fn configured_size(&self) -> Option<(u32, u32)> {
self.state
.get_surface(self.id)
.filter(|s| s.configured)
.map(|s| (s.width, s.height))
}
fn scale_factor(&self) -> f32 {
self.state
.get_surface(self.id)
.map_or(1.0, |surface| surface.scale_factor)
}
fn popup_auto_width(&self) -> Option<u32> {
self.state.popup_auto_width(self.id)
}
fn reposition_popup_if_changed(&mut self, new_height: u32) {
self.state.reposition_popup_if_changed(self.id, new_height)
}
fn set_size(&mut self, width: u32, height: u32) {
self.state.set_surface_size(self.id, width, height)
}
fn set_exclusive_zone(&mut self, zone: i32) {
self.state.set_surface_exclusive_zone(self.id, zone)
}
fn set_viewport_destination(&mut self, width: u32, height: u32) {
self.state.set_viewport_destination(self.id, width, height)
}
fn batch_layer_requests<F: FnOnce(&mut Self)>(&mut self, f: F) {
let id = self.id;
let batch = self.state.open_layer_batch(id);
f(self);
self.state.close_layer_batch(batch);
}
fn has_published_blur(&self) -> bool {
self.state.has_published_blur(self.id)
}
fn take_blur_resync(&mut self) -> bool {
self.state.take_blur_resync(self.id)
}
fn sync_blur_region(&mut self, rects: Vec<region::RegionRect>, commit: bool) {
self.state.sync_blur_region(self.id, rects, commit)
}
fn sync_input_region(&mut self, request: ®ion::InputRegionRequest, commit: bool) {
self.state.sync_input_region(self.id, request, commit)
}
fn request_frame_callback(&mut self) {
self.state.request_frame_callback(self.id)
}
fn damage(&mut self, x: i32, y: i32, width: i32, height: i32) {
self.state.damage_surface(self.id, x, y, width, height)
}
fn mark_frame_callback_pending(&mut self) {
self.state.mark_frame_callback_pending(self.id)
}
fn set_layer(&mut self, layer: platform::Layer) {
self.state.set_surface_layer(self.id, layer)
}
fn set_anchor(&mut self, anchor: platform::Anchor) {
self.state.set_surface_anchor(self.id, anchor)
}
fn set_margin(&mut self, margin: surface::Margin) {
self.state.set_surface_margin(self.id, margin)
}
fn set_input_region(&mut self, rects: Option<&[widgets::Rect]>) {
self.state.set_surface_input_region(self.id, rects)
}
fn set_keyboard_interactivity(&mut self, mode: platform::KeyboardInteractivity) {
self.state.set_surface_keyboard_interactivity(self.id, mode)
}
}
impl Platform for platform::WaylandState {
type Surface<'a> = WaylandSurface<'a>;
fn surface(&mut self, id: SurfaceId) -> Option<WaylandSurface<'_>> {
self.get_surface(id)?;
Some(WaylandSurface { state: self, id })
}
fn supports_blur_region(&self) -> bool {
self.supports_blur_region()
}
fn create_surface(&mut self, id: SurfaceId, config: &SurfaceConfig) {
self.create_surface_with_id(id, config)
}
fn create_popup(
&mut self,
id: SurfaceId,
parent: SurfaceId,
config: &surface::PopupConfig,
size: (u32, u32),
) -> bool {
self.create_popup_surface_with_id(id, parent, config, size)
}
fn destroy_surface(&mut self, id: SurfaceId) {
self.destroy_surface(id)
}
fn conflicting_grab_popups(&self, parent: SurfaceId) -> Vec<SurfaceId> {
self.conflicting_grab_popups(parent)
}
fn popup_descendants_bottom_up(&self, root: SurfaceId) -> Vec<SurfaceId> {
self.popup_descendants_bottom_up(root)
}
fn start_session_lock(&mut self) -> bool {
self.start_session_lock()
}
fn create_lock_surface(&mut self, id: SurfaceId, output: outputs::OutputId) -> bool {
self.create_lock_surface_with_id(id, output)
}
fn take_lock_events(&mut self) -> Vec<platform::LockEvent> {
self.take_lock_events()
}
fn unlock_session(&mut self) {
self.unlock_session()
}
fn set_clipboard(&mut self, text: String) {
self.set_clipboard(text)
}
fn set_primary(&mut self, text: String) {
self.set_primary(text)
}
fn set_cursor(&mut self, cursor: reactive::CursorIcon) {
self.set_cursor(cursor)
}
fn read_selection(&mut self, kind: reactive::SelectionKind, token: u64) {
self.read_selection(kind, token)
}
fn flush(&self) -> bool {
if let Err(e) = self.connection.flush() {
log::error!("Wayland connection flush failed: {e}");
return false;
}
true
}
fn create_render_target(
&self,
id: SurfaceId,
gpu: &renderer::GpuContext,
size: (u32, u32),
) -> Option<renderer::RenderTarget> {
let wl_surface = &self.get_surface(id)?.wl_surface;
let window = platform::WaylandWindowWrapper::new(&self.connection, wl_surface);
Some(renderer::RenderTarget::Swapchain(
gpu.create_surface(window, size.0, size.1),
))
}
}
struct Geometry {
physical_width: u32,
physical_height: u32,
needs_resize: bool,
scale_changed: bool,
declare_destination: Option<(u32, u32)>,
}
fn open_frame<P: Platform>(ctx: &mut FrameContext<P>) -> Option<Frame> {
let frame = ctx.wayland_state.surface(ctx.id)?.open_frame()?;
ctx.surface.is_gpu_ready().then_some(frame)
}
fn resolve_geometry<P: Platform>(ctx: &mut FrameContext<P>, frame: &Frame) -> Geometry {
let id = ctx.id;
let surface = &mut *ctx.surface;
let (physical_width, physical_height) =
surface::buffer_size((frame.width, frame.height), frame.scale_factor);
let wgpu_surface = surface.wgpu_surface.as_mut().unwrap();
let needs_resize =
wgpu_surface.width() != physical_width || wgpu_surface.height() != physical_height;
let scale_changed = frame.scale_factor != surface.previous_scale_factor;
if needs_resize {
log::info!(
"Resizing surface {:?} to {}x{} (physical), scale {}",
id,
physical_width,
physical_height,
frame.scale_factor
);
wgpu_surface.resize(physical_width, physical_height);
}
if scale_changed {
log::info!(
"Surface {:?} scale factor changed: {} -> {}",
id,
surface.previous_scale_factor,
frame.scale_factor
);
surface.previous_scale_factor = frame.scale_factor;
}
let destination = (frame.width, frame.height);
let declare_destination =
(surface.viewport_destination != Some(destination)).then_some(destination);
Geometry {
physical_width,
physical_height,
needs_resize,
scale_changed,
declare_destination,
}
}
fn layout_pass<P: Platform>(
ctx: &mut FrameContext<P>,
frame: &Frame,
geometry: &Geometry,
has_pending_layouts: bool,
layout_roots: &mut Vec<WidgetId>,
) {
let id = ctx.id;
let surface = &mut *ctx.surface;
let wayland_state = &mut *ctx.wayland_state;
let renderer = &mut *ctx.renderer;
let tree = &mut *ctx.tree;
renderer.set_screen_size(
geometry.physical_width as f32,
geometry.physical_height as f32,
);
renderer.set_scale_factor(frame.scale_factor);
let constraints = Constraints::new(0.0, 0.0, frame.width as f32, frame.height as f32);
if !layout_roots.is_empty() {
let mut roots = Vec::new();
std::mem::swap(&mut roots, layout_roots);
for root_id in &roots {
let cached = tree
.last_layout_constraints(*root_id)
.unwrap_or(constraints);
tree.layout_widget(*root_id, cached);
}
} else if geometry.needs_resize {
tree.layout_widget(surface.widget_id, constraints);
tree.mark_subtree_needs_paint(surface.widget_id);
}
if has_pending_layouts
&& let Some(popup_width) = wayland_state.surface(id).and_then(|s| s.popup_auto_width())
{
let natural = measure_popup_height(tree, surface.widget_id, popup_width, id);
tree.layout_widget(surface.widget_id, constraints);
with_surface(wayland_state, id, |s| {
s.reposition_popup_if_changed(natural)
});
} else if (has_pending_layouts || frame.pacing.forced())
&& surface::needs_content_measure(&surface.config)
{
let (fixed_w, fixed_h) =
surface::measure_pins(&surface.config, (frame.width, frame.height));
let (nw, nh) = measure_natural_size(tree, surface.widget_id, fixed_w, fixed_h, id);
tree.layout_widget(surface.widget_id, constraints);
let asking = surface::honour_owned_axes(surface.config.anchor, nw, nh);
let resizing =
asking != surface::honour_owned_axes(surface.config.anchor, frame.width, frame.height);
{
let config = &surface.config;
let published = &mut surface.exclusive_zone;
if let Some(mut handle) = wayland_state.surface(id) {
handle.batch_layer_requests(|surface| {
if resizing {
let (ask_w, ask_h) = asking;
surface.set_size(ask_w, ask_h);
}
publish_exclusive_zone(surface, config, Some((nw, nh)), published);
});
}
}
}
widget_ref::update_widget_refs(tree);
reactive::focus::apply_pending_focus(tree);
}
fn send_size<S: Surface>(managed: &ManagedSurface, surface: &mut S) -> (bool, WidgetId) {
let live = surface.configured_size();
let (ask_w, ask_h, needs_measure) = surface::resize_request(&managed.config, live);
surface.set_size(ask_w, ask_h);
(needs_measure, managed.widget_id)
}
fn reconfigure<P: Platform, R>(
id: SurfaceId,
surface_manager: &mut SurfaceManager,
platform: &mut P,
change: impl FnOnce(&mut ManagedSurface, &mut P::Surface<'_>) -> R,
) -> Option<R> {
let managed = surface_manager.get_mut(id)?;
let mut handle = platform.surface(id)?;
let mut result = None;
handle.batch_layer_requests(|surface| {
result = Some(change(managed, surface));
publish_exclusive_zone(surface, &managed.config, None, &mut managed.exclusive_zone);
});
result
}
fn paint_and_present<P: Platform>(ctx: &mut FrameContext<P>, frame: &Frame, geometry: &Geometry) {
let id = ctx.id;
let surface = &mut *ctx.surface;
let wayland_state = &mut *ctx.wayland_state;
let renderer = &mut *ctx.renderer;
let tree = &mut *ctx.tree;
if frame.pacing.forced() || geometry.needs_resize || geometry.scale_changed {
tree.mark_subtree_needs_paint(surface.widget_id);
}
if !tree.needs_paint(surface.widget_id) {
if let Some(mut handle) = wayland_state.surface(id)
&& handle.take_blur_resync()
{
let blur_rects = blur::regions_from_commands(
&surface.flattened_commands,
surface.flatten_scratch.clip_chains(),
);
handle.sync_blur_region(blur_rects, true);
}
render_stats::record_frame_skipped();
render_stats::end_frame(&DamageRegion::None);
return;
}
surface.root_node.clear();
surface.root_node.bounds =
widgets::Rect::new(0.0, 0.0, frame.width as f32, frame.height as f32);
time_phase!(render_stats::Phase::Paint, {
tree.paint_widget(
surface.widget_id,
&mut surface.root_node,
frame.scale_factor,
);
});
let carried;
time_phase!(render_stats::Phase::Flatten, {
carried = flatten_root_into(
&surface.root_node,
&mut surface.flattened_commands,
&mut surface.command_layers,
&mut surface.flatten_scratch,
);
});
if wayland_state.supports_blur_region()
&& let Some(mut handle) = wayland_state.surface(id)
&& (carried.compositor_blur || handle.has_published_blur())
{
let blur_rects = blur::regions_from_commands(
&surface.flattened_commands,
surface.flatten_scratch.clip_chains(),
);
handle.sync_blur_region(blur_rects, false);
}
if carried.input_region || surface.input_region.is_some() {
let request = region::InputRegionRequest {
base: region::base_rects(
surface.input_base.as_deref(),
frame.width as f32,
frame.height as f32,
),
ops: region::input_ops_from_commands(
&surface.flattened_commands,
surface.flatten_scratch.clip_chains(),
),
};
if surface.input_region.as_ref() != Some(&request)
&& let Some(mut handle) = wayland_state.surface(id)
{
handle.sync_input_region(&request, false);
surface.input_region = carried.input_region.then_some(request);
}
}
if let Some((width, height)) = geometry.declare_destination {
surface.viewport_destination = Some((width, height));
with_surface(wayland_state, id, |s| {
s.set_viewport_destination(width, height)
});
}
with_surface(wayland_state, id, |s| s.request_frame_callback());
let damage = tree.take_damage(surface.widget_id);
match damage {
DamageRegion::None => {
with_surface(wayland_state, id, |s| {
s.damage(
0,
0,
geometry.physical_width as i32,
geometry.physical_height as i32,
)
});
}
DamageRegion::Partial(rect) => {
let scale = frame.scale_factor;
with_surface(wayland_state, id, |s| {
s.damage(
(rect.x * scale) as i32,
(rect.y * scale) as i32,
(rect.width * scale).ceil() as i32,
(rect.height * scale).ceil() as i32,
)
});
}
DamageRegion::Full => {
with_surface(wayland_state, id, |s| {
s.damage(
0,
0,
geometry.physical_width as i32,
geometry.physical_height as i32,
)
});
}
}
let presented;
time_phase!(render_stats::Phase::GpuRender, {
presented = renderer.render(
surface.wgpu_surface.as_mut().unwrap(),
&renderer::FlatFrame {
commands: &surface.flattened_commands,
layers: &surface.command_layers,
clips: surface.flatten_scratch.clip_chains(),
},
surface.config.background_color,
);
});
if !presented {
tree.set_full_damage(surface.widget_id);
jobs::wake_loop();
return;
}
time_phase!(render_stats::Phase::CachePaintResults, {
for child in &surface.root_node.children {
cache_paint_results(tree, child);
}
tree.clear_needs_paint(surface.widget_id);
});
render_stats::record_frame_painted();
render_stats::end_frame(&damage);
with_surface(wayland_state, id, |s| s.mark_frame_callback_pending());
}
struct FrameContext<'a, P: Platform> {
id: SurfaceId,
surface: &'a mut surface_manager::ManagedSurface,
wayland_state: &'a mut P,
renderer: &'a mut Renderer,
tree: &'a mut Tree,
}
struct Pending {
force_render: bool,
jobs: bool,
wake_requested: bool,
deadline: Option<std::time::Instant>,
}
fn wake_deadline(gpu: &GpuSlot) -> Option<std::time::Instant> {
[
jobs::next_deadline(),
reactive::timer::next_deadline(),
gpu.release_at(),
]
.into_iter()
.flatten()
.min()
}
fn wait_for(pending: &Pending, now: std::time::Instant) -> Option<std::time::Duration> {
if pending.jobs || pending.force_render || pending.wake_requested {
return Some(std::time::Duration::from_millis(16));
}
pending
.deadline
.map(|deadline| deadline.saturating_duration_since(now))
}
fn loop_timeout<P: Platform>(
platform: &mut P,
surfaces: &SurfaceManager,
gpu: &GpuSlot,
wake_requested: bool,
now: std::time::Instant,
) -> Option<std::time::Duration> {
let mut force_render = false;
let mut waiting = smallvec::SmallVec::<[WidgetId; 4]>::new();
for id in surfaces.ids() {
let Some(pacing) = platform.surface(id).map(|s| s.pacing()) else {
continue;
};
force_render |= pacing.forced();
if pacing.callback_pending()
&& let Some(surface) = surfaces.get(id)
{
waiting.push(surface.widget_id);
}
}
let pending = Pending {
force_render,
jobs: jobs::has_runnable_jobs(|root| waiting.contains(&root)),
wake_requested,
deadline: wake_deadline(gpu),
};
wait_for(&pending, now)
}
fn close_requested<P: Platform>(platform: &P, id: SurfaceId) -> bool {
let closing = surface::closes_requested();
closing.contains(&id)
|| closing
.into_iter()
.any(|root| platform.popup_descendants_bottom_up(root).contains(&id))
}
fn with_surface<P: Platform>(platform: &mut P, id: SurfaceId, f: impl FnOnce(&mut P::Surface<'_>)) {
if let Some(mut surface) = platform.surface(id) {
f(&mut surface);
}
}
fn render_surface<P: Platform>(
ctx: &mut FrameContext<P>,
layout_roots: &mut Vec<WidgetId>,
woken: bool,
active_roots: &rustc_hash::FxHashSet<WidgetId>,
at: std::time::Instant,
) {
let Some(frame) = open_frame(ctx) else {
return;
};
let root = ctx.surface.widget_id;
let (id, platform) = (ctx.id, &*ctx.wayland_state);
dispatch_events(&frame.events, root, ctx.tree, active_roots, move || {
close_requested(platform, id)
});
let geometry = resolve_geometry(ctx, &frame);
if paced_out(&frame, &geometry) {
render_stats::record_frame_skipped();
return;
}
ctx.tree.set_frame_instant(Some(at));
run_jobs(root, ctx.tree, layout_roots, active_roots);
let has_pending_layouts = !layout_roots.is_empty();
if !(frame.pacing.forced()
|| woken
|| has_pending_layouts
|| geometry.needs_resize
|| geometry.scale_changed
|| ctx.tree.needs_paint(root))
{
ctx.tree.set_frame_instant(None);
return;
}
layout_pass(ctx, &frame, &geometry, has_pending_layouts, layout_roots);
paint_and_present(ctx, &frame, &geometry);
ctx.tree.set_frame_instant(None);
}
pub struct App {
surface_definitions: Vec<SurfaceDefinition>,
tree: Tree,
layout_roots: rustc_hash::FxHashMap<WidgetId, Vec<WidgetId>>,
root_owner_id: Option<OwnerId>,
quit_on_last_surface: bool,
}
impl App {
pub fn new() -> Self {
Self {
surface_definitions: Vec::new(),
tree: Tree::new(),
layout_roots: rustc_hash::FxHashMap::default(),
root_owner_id: None,
quit_on_last_surface: true,
}
}
pub fn default_font_family(self, family: FontFamily) -> Self {
set_default_font_family(family);
self
}
pub fn image_cache_budget(self, bytes: usize) -> Self {
set_image_cache_budget(bytes);
self
}
pub fn quit_on_last_surface(mut self, quit: bool) -> Self {
self.quit_on_last_surface = quit;
self
}
pub fn add_surface<W, F>(&mut self, config: SurfaceConfig, widget_fn: F) -> SurfaceId
where
W: Widget + 'static,
F: FnOnce() -> W + 'static,
{
let id = SurfaceId::next();
self.surface_definitions.push(SurfaceDefinition {
id,
config,
widget_fn: Box::new(move || Box::new(widget_fn())),
});
id
}
pub fn run(mut self, setup: impl FnOnce(&mut Self)) -> ExitReason {
let mut event_loop: EventLoop<platform::WaylandState> =
EventLoop::try_new().expect("Failed to create event loop");
let loop_handle = event_loop.handle();
let (ping, ping_source) = make_ping().expect("Failed to create ping");
init_wakeup(ping);
loop_handle
.insert_source(ping_source, |_, _, _| {
})
.expect("Failed to insert ping source");
let (ingress_tx, ingress_channel) = calloop_channel::channel();
ingress::install_ingress(ingress_tx);
loop_handle
.insert_source(ingress_channel, |event, _, _| {
if let calloop_channel::Event::Msg(message) = event {
match message {
ingress::IngressMessage::BgWritesQueued => {}
ingress::IngressMessage::SelectionRead { token, content } => {
reactive::clipboard::answer_paste(token, content)
}
}
}
})
.expect("Failed to insert ingress channel");
self.root_owner_id = Some(reactive::create_root_owner());
setup(&mut self);
if self.surface_definitions.is_empty() {
log::info!(
"No static surfaces defined; waiting for dynamic surfaces \
(spawn_surface, e.g. from an outputs() effect)"
);
}
let (connection, mut event_queue, mut wayland_state) =
match create_wayland_app(loop_handle.clone()) {
Ok(app) => app,
Err(e) => {
log::error!("Cannot start: {e}");
return ExitReason::Error(e);
}
};
for def in &self.surface_definitions {
wayland_state.create_surface_with_id(def.id, &def.config);
}
while !wayland_state.all_surfaces_configured() {
if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
log::error!("Wayland dispatch failed during configure: {e}");
return ExitReason::Error(platform::PlatformError::ConnectionLost);
}
}
let mut gpu = GpuSlot::lazy();
let mut surface_manager = SurfaceManager::new();
let mut renderer: Option<Renderer> = None;
for def in self.surface_definitions.drain(..) {
let (widget, owner_id) = with_owner(|| (def.widget_fn)());
surface_manager.add(ManagedSurface::new(
def.id,
def.config,
widget,
owner_id,
&mut self.tree,
));
}
WaylandSource::new(connection.clone(), event_queue)
.insert(loop_handle.clone())
.expect("Failed to insert Wayland source");
loop {
jobs::promote_due_jobs();
let timeout = loop_timeout(
&mut wayland_state,
&surface_manager,
&gpu,
jobs::wake_request_pending(),
std::time::Instant::now(),
);
if let Err(e) = event_loop.dispatch(timeout, &mut wayland_state) {
log::error!("Event loop dispatch failed: {e}");
return ExitReason::Error(platform::PlatformError::ConnectionLost);
}
let ctx = LoopContext {
wayland_state: &mut wayland_state,
surface_manager: &mut surface_manager,
gpu: &mut gpu,
renderer: &mut renderer,
quit_on_last_surface: self.quit_on_last_surface,
};
if let Some(reason) = iterate(ctx, &mut self.tree, &mut self.layout_roots, None) {
return reason;
}
render_stats::report_if_due();
}
}
}
struct LoopContext<'a, P: Platform> {
wayland_state: &'a mut P,
surface_manager: &'a mut SurfaceManager,
gpu: &'a mut GpuSlot,
renderer: &'a mut Option<Renderer>,
quit_on_last_surface: bool,
}
fn iterate<P: Platform>(
ctx: LoopContext<'_, P>,
tree: &mut Tree,
layout_roots: &mut rustc_hash::FxHashMap<WidgetId, Vec<WidgetId>>,
frame_at: Option<std::time::Instant>,
) -> Option<ExitReason> {
let LoopContext {
gpu,
wayland_state,
surface_manager,
renderer,
quit_on_last_surface,
} = ctx;
jobs::mark_loop_awake();
match get_exit_request() {
jobs::ExitRequest::Quit => return Some(ExitReason::Quit),
jobs::ExitRequest::Restart => return Some(ExitReason::Restart),
jobs::ExitRequest::Running => {}
}
session_lock::process_session_lock(surface_manager, wayland_state, tree, gpu);
reactive::owner::flush_pending_disposals();
image_decode::settle_image_events();
if !process_surface_commands(surface_manager, wayland_state, tree, quit_on_last_surface) {
return Some(ExitReason::Quit);
}
let frame_at = frame_at.unwrap_or_else(std::time::Instant::now);
if surface_manager.is_empty() && gpu.release_if_idle(frame_at) {
*renderer = None;
}
if !surface_manager.is_empty() {
let Some(gpu_context) = gpu.get() else {
return Some(ExitReason::Error(platform::PlatformError::NoGpu));
};
surface_manager.init_pending_gpu(gpu_context, wayland_state, tree, frame_at);
}
if renderer.is_none()
&& let Some(wgpu_surface) = surface_manager.first_gpu_surface()
{
*renderer = Some(Renderer::new(
wgpu_surface.device().clone(),
wgpu_surface.queue().clone(),
wgpu_surface.format(),
));
}
reactive::timer::run_due_timers(frame_at);
reactive::clipboard::deliver_pastes(tree);
reactive::flush_bg_writes();
let woken = take_wake_request();
if let Some(renderer) = renderer.as_mut() {
let surface_ids: Vec<SurfaceId> = surface_manager.ids().collect();
let active_roots: rustc_hash::FxHashSet<WidgetId> = surface_ids
.iter()
.filter_map(|sid| surface_manager.get(*sid).map(|s| s.widget_id))
.collect();
jobs::distribute_jobs(tree, &active_roots);
let orphans = jobs::drain_orphan_jobs();
if !orphans.is_empty() {
let mut scratch_layout_roots = Vec::new();
process_jobs(&orphans, tree, &mut scratch_layout_roots);
}
jobs::recycle_job_buffer(orphans);
layout_roots.retain(|root, _| active_roots.contains(root));
for id in surface_ids {
let Some(surface) = surface_manager.get_mut(id) else {
continue;
};
let layout_roots = layout_roots.entry(surface.widget_id).or_default();
let mut ctx = FrameContext {
id,
surface,
wayland_state,
renderer,
tree,
};
render_surface(&mut ctx, layout_roots, woken, &active_roots, frame_at);
}
}
sync_platform_state(wayland_state);
if !wayland_state.flush() {
return Some(ExitReason::Error(platform::PlatformError::ConnectionLost));
}
None
}
pub(crate) fn reset_thread_state(tree: &mut Tree) {
tree.clear();
reactive::reset_reactive();
app_state::reset();
}
impl Drop for App {
fn drop(&mut self) {
if let Some(root_id) = self.root_owner_id {
reactive::dispose_owner_now(root_id);
}
reset_thread_state(&mut self.tree);
jobs::reset_wakeup();
ingress::reset_ingress();
reactive::reset_bg_writes();
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod restart_tests {
use super::*;
use crate::jobs::{JobRequest, has_pending_jobs, request_job};
use crate::reactive::clipboard::take_paste_requests;
use crate::reactive::owner::create_root_owner;
use crate::reactive::{
clipboard_copy, clipboard_paste, create_signal, flush_bg_writes, take_clipboard_change,
};
use crate::surface::drain_surface_commands;
use crate::widget_ref::create_widget_ref;
#[test]
fn widget_drop_can_read_global_focus_and_the_next_app_starts_clean() {
use std::cell::Cell;
use std::rc::Rc;
struct ReadsFocusOnDrop {
inner: Box<dyn Widget>,
drops: Rc<Cell<usize>>,
}
impl Widget for ReadsFocusOnDrop {
fn layout(
&mut self,
ctx: &mut crate::tree::LayoutCtx,
constraints: crate::layout::Constraints,
) -> crate::layout::Size {
self.inner.layout(ctx, constraints)
}
fn paint(&self, ctx: &mut crate::renderer::PaintContext) {
self.inner.paint(ctx);
}
}
impl Drop for ReadsFocusOnDrop {
fn drop(&mut self) {
assert_eq!(crate::reactive::focus::focused_widget(), None);
assert!(crate::reactive::focus::focus_path().is_empty());
self.drops.set(self.drops.get() + 1);
}
}
let _wakeup = crate::jobs::wakeup_test_lock();
let drops = Rc::new(Cell::new(0));
for expected_drops in 1..=2 {
let mut app = App::new();
app.root_owner_id = Some(create_root_owner());
assert_eq!(crate::reactive::focus::focused_widget(), None);
let widget = app.tree.register(Box::new(ReadsFocusOnDrop {
inner: Box::new(crate::widgets::container()),
drops: drops.clone(),
}));
crate::reactive::focus::request_focus(&app.tree, widget);
assert_eq!(crate::reactive::focus::focused_widget(), Some(widget));
drop(app);
assert_eq!(drops.get(), expected_drops);
assert!(!has_pending_jobs());
}
}
#[test]
fn a_second_app_does_not_inherit_the_font_the_first_one_declared() {
let first = App::new().default_font_family(FontFamily::name("Inter"));
assert_eq!(
default_font_family(),
FontFamily::name("Inter"),
"the declaration reaches the widgets that will read it"
);
drop(first);
let _next = App::new();
assert_eq!(
default_font_family(),
FontFamily::SansSerif,
"an App that declares no font gets the default, not the last App's"
);
}
#[test]
fn a_second_app_does_not_inherit_the_fonts_the_first_one_loaded() {
let first = App::new();
load_font(vec![11u8; 64]);
assert_eq!(
get_registered_fonts().len(),
1,
"the App under test has a font to forget"
);
drop(first);
let _next = App::new();
assert!(
get_registered_fonts().is_empty(),
"the next App would draw in a font it was never given"
);
}
#[test]
fn writers_from_the_first_app_cannot_change_the_second_apps_signals() {
let first = App::new();
let stale_set = create_signal(1u32).writer();
let stale_update = create_signal(2u32).writer();
let stale_set_always = create_signal(3u32).writer();
let stale_update_always = create_signal(4u32).writer();
drop(first);
let _next = App::new();
let set_target = create_signal(10u32);
let update_target = create_signal(20u32);
let set_always_target = create_signal(30u32);
let update_always_target = create_signal(40u32);
stale_set.set(11);
stale_update.update(|value| *value = 21);
stale_set_always.set_always(31);
stale_update_always.update_always(|value| *value = 41);
assert_eq!(set_target.get_untracked(), 10);
assert_eq!(update_target.get_untracked(), 20);
assert_eq!(set_always_target.get_untracked(), 30);
assert_eq!(update_always_target.get_untracked(), 40);
update_always_target
.writer()
.update_always(|value| *value = 41);
assert_eq!(update_always_target.get_untracked(), 41);
}
#[test]
fn background_writers_from_the_first_app_cannot_change_the_second_apps_signals() {
let first = App::new();
let stale_set = create_signal(1u32).writer();
let stale_update = create_signal(2u32).writer();
let stale_set_always = create_signal(3u32).writer();
let stale_update_always = create_signal(4u32).writer();
std::thread::spawn(move || {
stale_set.set(11);
stale_update.update(|value| *value = 21);
stale_set_always.set_always(31);
stale_update_always.update_always(|value| *value = 41);
})
.join()
.unwrap();
drop(first);
let _next = App::new();
let set_target = create_signal(10u32);
let update_target = create_signal(20u32);
let set_always_target = create_signal(30u32);
let update_always_target = create_signal(40u32);
flush_bg_writes();
assert_eq!(set_target.get_untracked(), 10);
assert_eq!(update_target.get_untracked(), 20);
assert_eq!(set_always_target.get_untracked(), 30);
assert_eq!(update_always_target.get_untracked(), 40);
}
#[test]
fn late_background_writers_cannot_change_the_second_apps_signals() {
let first = App::new();
let stale_set = create_signal(1u32).writer();
let stale_update = create_signal(2u32).writer();
let stale_set_always = create_signal(3u32).writer();
let stale_update_always = create_signal(4u32).writer();
drop(first);
let _next = App::new();
let set_target = create_signal(10u32);
let update_target = create_signal(20u32);
let set_always_target = create_signal(30u32);
let update_always_target = create_signal(40u32);
std::thread::spawn(move || {
stale_set.set(11);
stale_update.update(|value| *value = 21);
stale_set_always.set_always(31);
stale_update_always.update_always(|value| *value = 41);
})
.join()
.unwrap();
flush_bg_writes();
assert_eq!(set_target.get_untracked(), 10);
assert_eq!(update_target.get_untracked(), 20);
assert_eq!(set_always_target.get_untracked(), 30);
assert_eq!(update_always_target.get_untracked(), 40);
}
#[test]
fn signals_from_the_first_app_cannot_reach_the_second_apps_signals() {
let first = App::new();
let stale = create_signal(1u32);
drop(first);
let _next = App::new();
let target = create_signal(10u32);
stale.set(11);
assert_eq!(target.get_untracked(), 10);
assert!(stale.try_get_untracked().is_none());
}
#[test]
fn a_job_queued_while_the_app_is_being_forgotten_is_forgotten_too() {
let _wakeup = crate::jobs::wakeup_test_lock();
struct QueuesOnDrop(WidgetId);
impl Drop for QueuesOnDrop {
fn drop(&mut self) {
request_job(self.0, JobRequest::Unregister);
}
}
create_root_owner();
let mut app = App::new();
let widget = app
.tree
.register(Box::new(crate::widgets::container()) as Box<dyn Widget>);
let late = QueuesOnDrop(widget);
crate::surface::spawn_surface(crate::surface::SurfaceConfig::new(), move || {
let _captured = ⪭
crate::widgets::container()
});
drop(app);
assert!(
!has_pending_jobs(),
"a command dropped by the reset queued a job the reset had already emptied"
);
}
#[test]
fn nothing_the_last_app_queued_survives_its_drop() {
let _wakeup = crate::jobs::wakeup_test_lock();
create_root_owner();
let mut app = App::new();
let widget = app
.tree
.register(Box::new(crate::widgets::container()) as Box<dyn Widget>);
request_job(widget, JobRequest::Paint);
crate::surface::spawn_surface(crate::surface::SurfaceConfig::new(), || {
crate::widgets::container()
});
clipboard_copy("a secret");
clipboard_paste(|_| {});
create_widget_ref().focus();
crate::session_lock::unlock_session();
assert!(has_pending_jobs(), "the App under test has work to forget");
drop(app);
assert!(!has_pending_jobs(), "a job is still aimed at the old tree");
assert!(
drain_surface_commands().is_empty(),
"the loop would open a surface the old App asked for"
);
assert_eq!(
take_clipboard_change(),
None,
"the next App would offer the last one's copy"
);
assert!(
take_paste_requests().is_empty(),
"the next App would read for a paste the last one asked for"
);
with_app_state(|app| {
assert!(
app.pending_focus.borrow().is_none(),
"a focus request outlived the widget it named"
);
assert!(
app.lock_request.is_empty(),
"the next App would answer a lock question it was never asked"
);
});
}
}
#[cfg(test)]
mod exclusive_zone_resync_tests {
use super::*;
use crate::platform::Anchor;
use crate::surface::{ExclusiveZone, Margin, SurfaceExtent};
#[test]
fn a_fixed_axis_resolves_against_the_requested_size() {
let live_height = Some(32);
let height = surface::requested_extent(SurfaceExtent::Fixed(48), live_height);
assert_eq!(height, 48, "the request wins over the stale configure");
let zone = ExclusiveZone::Auto.resolve(Anchor::TOP, Margin::default(), 800, height);
assert_eq!(zone, 48);
}
#[test]
fn a_content_axis_holds_the_live_size_until_it_is_measured() {
assert_eq!(
surface::requested_extent(SurfaceExtent::Content, Some(240)),
240
);
assert_eq!(
surface::requested_extent(SurfaceExtent::Content, None),
SurfaceExtent::Content.initial()
);
assert_eq!(
surface::requested_extent(SurfaceExtent::Content, Some(1)),
1,
"and once configured at 1px, 1px is the honest answer"
);
}
#[test]
fn a_reservation_follows_the_anchor_it_was_given() {
let margin = Margin::default();
let bar = ExclusiveZone::Auto.resolve(Anchor::TOP, margin, 800, 32);
assert_eq!(bar, 32);
let dock = ExclusiveZone::Auto.resolve(Anchor::LEFT, margin, 48, 600);
assert_eq!(dock, 48);
assert_ne!(dock, bar, "the axis genuinely changes with the anchor");
}
#[test]
fn handing_an_axis_to_content_keeps_the_size_and_asks_for_a_measure() {
let live = Some((300u32, 40u32));
let anchored = |w: SurfaceExtent, h: SurfaceExtent| SurfaceConfig {
anchor: Anchor::TOP,
width: w,
height: h,
..SurfaceConfig::new()
};
let (w, h, measure) =
surface::resize_request(&anchored(SurfaceExtent::Content, 40.into()), live);
assert_eq!((w, h), (300, 40), "no 1px collapse");
assert!(measure, "the measure pass has to be woken");
let (w, h, measure) = surface::resize_request(&anchored(200.into(), 60.into()), live);
assert_eq!((w, h), (200, 60));
assert!(!measure, "two fixed axes need no measure");
}
#[test]
fn a_stretched_content_axis_asks_for_no_measure() {
let live = Some((1920u32, 32u32));
let bar = |anchor: Anchor| SurfaceConfig {
anchor,
width: SurfaceExtent::Content,
height: 32.into(),
..SurfaceConfig::new()
};
let stretched = bar(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT);
let (_, _, measure) = surface::resize_request(&stretched, live);
assert!(!measure, "the width is the compositor's either way");
assert!(
!surface::needs_content_measure(&stretched),
"and the frame loop's measure pass asks the same question, or it \
re-measures a discarded axis on every frame that lays anything out"
);
let ours = bar(Anchor::TOP | Anchor::LEFT);
let (_, _, measure) = surface::resize_request(&ours, live);
assert!(
measure,
"anchored to one edge it is ours, and has to be measured"
);
assert!(surface::needs_content_measure(&ours));
}
#[test]
fn a_reservation_waits_for_the_measure_rather_than_guessing() {
let dock = SurfaceConfig {
anchor: Anchor::LEFT | Anchor::TOP | Anchor::BOTTOM,
width: SurfaceExtent::Content,
height: 32.into(),
exclusive_zone: surface::ExclusiveZone::Auto,
..SurfaceConfig::new()
};
let stale = 1920;
let measured = 240;
assert!(
surface::needs_content_measure(&dock),
"the width is ours now, so it is waiting on a measure"
);
assert_eq!(
dock.exclusive_zone
.resolve(dock.anchor, dock.margin, measured, 32),
240,
"the measure is what it reserves for"
);
assert_eq!(
dock.exclusive_zone
.resolve(dock.anchor, dock.margin, stale, 32),
1920,
"and the confirmed size is the whole screen, which is why publishing \
from it has to wait"
);
}
#[test]
fn only_a_reservation_that_reads_a_size_waits_for_one() {
let toast = |zone| SurfaceConfig {
anchor: Anchor::LEFT | Anchor::TOP | Anchor::BOTTOM,
width: SurfaceExtent::Content,
height: SurfaceExtent::Content,
exclusive_zone: zone,
..SurfaceConfig::new()
};
assert!(
surface::needs_content_measure(&toast(surface::ExclusiveZone::Auto)),
"the premise: this is the surface whose measure is pending"
);
for zone in [
surface::ExclusiveZone::Fixed(40),
surface::ExclusiveZone::None,
surface::ExclusiveZone::Ignore,
] {
let config = toast(zone);
let unmeasured = zone.resolve(config.anchor, config.margin, 1, 1);
let measured = zone.resolve(config.anchor, config.margin, 240, 60);
assert_eq!(
unmeasured, measured,
"{zone:?} does not depend on the size, so deferring it only loses it"
);
}
}
#[test]
fn the_size_asked_for_follows_the_anchor() {
let bar = |anchor: Anchor| SurfaceConfig {
anchor,
width: 800.into(),
height: 32.into(),
..SurfaceConfig::new()
};
let live = Some((1920u32, 32u32));
let stretched =
surface::resize_request(&bar(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT), live);
assert_eq!(
(stretched.0, stretched.1),
(0, 32),
"anchored to both horizontal edges, the width is the compositor's"
);
let pinned = surface::resize_request(&bar(Anchor::TOP | Anchor::LEFT), live);
assert_eq!(
(pinned.0, pinned.1),
(800, 32),
"anchored to one, it is ours again and must be sent"
);
let dock = surface::resize_request(&bar(Anchor::LEFT | Anchor::TOP | Anchor::BOTTOM), live);
assert_eq!(
(dock.0, dock.1),
(800, 0),
"and the other axis, the other way"
);
}
}
#[cfg(test)]
mod font_registry_tests {
use super::*;
#[test]
fn loading_the_same_font_twice_registers_it_once() {
let font = vec![7u8; 64];
super::load_font(font.clone());
let after_first = with_app_state(|app| app.custom_fonts.borrow().len());
super::load_font(font);
let after_second = with_app_state(|app| app.custom_fonts.borrow().len());
assert_eq!(after_first, after_second);
super::load_font(vec![9u8; 64]);
assert_eq!(
with_app_state(|app| app.custom_fonts.borrow().len()),
after_second + 1,
"a different font must still register"
);
}
}
#[cfg(test)]
mod dispatch_declares_the_moment {
use super::*;
use crate::layout::Size;
use crate::tree::LayoutCtx;
use crate::widgets::widget::{Event, EventResponse};
struct Spy(std::rc::Rc<std::cell::Cell<Option<crate::clock::EventInstant>>>);
impl widgets::Widget for Spy {
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
constraints: crate::layout::Constraints,
) -> Size {
Size::new(constraints.max_width, constraints.max_height)
}
fn paint(&self, _ctx: &mut crate::renderer::PaintContext) {}
fn event(&mut self, tree: &mut Tree, _id: WidgetId, _event: &Event) -> EventResponse {
self.0.set(Some(tree.event_instant()));
EventResponse::Handled
}
}
#[test]
fn a_widget_is_told_when_the_event_it_is_handed_happened() {
let seen = std::rc::Rc::new(std::cell::Cell::new(None));
let mut tree = Tree::new();
let root = tree.register(Box::new(Spy(seen.clone())));
tree.set_origin(root, 0.0, 0.0);
let happened = std::time::Instant::now() - std::time::Duration::from_secs(3600);
let happened_on_the_event_clock = crate::clock::EventInstant::from(happened);
let events = [(happened, Event::mouse_move(10.0, 10.0))];
dispatch_events(&events, root, &mut tree, &Default::default(), || false);
assert_eq!(
seen.get(),
Some(happened_on_the_event_clock),
"the widget has to be told the moment the queue carried, not the \
moment the dispatch ran"
);
let after = tree.event_instant();
assert!(
after > happened_on_the_event_clock,
"the moment has to be cleared when the dispatch ends, got {after:?} \
for an event from an hour ago"
);
}
}
#[cfg(test)]
mod a_press_nothing_claimed_takes_the_focus_with_it {
use super::*;
use crate::layout::{Constraints, Flex};
use crate::reactive::create_signal;
use crate::reactive::owner::create_root_owner;
use crate::widget_ref::{WidgetRef, create_widget_ref, update_widget_refs};
use crate::widgets::state_layer::Stateful;
use crate::widgets::widget::{Event, MouseButton};
use crate::widgets::{Widget, container, text_input};
struct Screen {
tree: Tree,
root: WidgetId,
}
impl Screen {
fn new(widget: impl Widget + 'static) -> Self {
let mut tree = Tree::new();
let root = Self::lay_out(&mut tree, widget);
Self { tree, root }
}
fn add(&mut self, widget: impl Widget + 'static) -> WidgetId {
Self::lay_out(&mut self.tree, widget)
}
fn lay_out(tree: &mut Tree, widget: impl Widget + 'static) -> WidgetId {
let root = tree.register(Box::new(widget));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
tree.set_origin(root, 0.0, 0.0);
tree.layout_widget(root, Constraints::new(0.0, 0.0, 200.0, 100.0));
update_widget_refs(tree);
root
}
fn press(&mut self, x: f32, y: f32) {
let root = self.root;
self.press_button(root, x, y, MouseButton::Left);
}
fn click(&mut self, x: f32, y: f32) {
self.press(x, y);
let root = self.root;
let events = [(
std::time::Instant::now(),
Event::mouse_up(x, y, MouseButton::Left),
)];
dispatch_events(&events, root, &mut self.tree, &Default::default(), || false);
}
fn press_button(&mut self, root: WidgetId, x: f32, y: f32, button: MouseButton) {
let events = [(std::time::Instant::now(), Event::mouse_down(x, y, button))];
dispatch_events(&events, root, &mut self.tree, &Default::default(), || false);
}
}
fn a_field() -> WidgetRef {
create_root_owner();
create_widget_ref()
}
fn a_focused_field() -> (Screen, WidgetRef) {
let field = a_field();
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.child(text_input(create_signal(String::new())).widget_ref(field)),
);
screen.press(100.0, 8.0);
assert!(field.is_focused(), "a press inside the field focuses it");
(screen, field)
}
#[test]
fn a_press_on_the_background_takes_the_keyboard_off_the_field() {
let (mut screen, field) = a_focused_field();
screen.press(100.0, 80.0);
assert!(
!field.is_focused(),
"a press on the background is a press on nothing, and takes the \
keyboard with it"
);
}
#[test]
fn a_press_inside_the_field_keeps_it() {
let (mut screen, field) = a_focused_field();
screen.press(40.0, 8.0);
assert!(
field.is_focused(),
"a second press inside the field moves the caret, it does not \
surrender the keyboard"
);
}
#[test]
fn a_middle_press_on_the_background_takes_the_keyboard_too() {
let (mut screen, field) = a_focused_field();
let root = screen.root;
screen.press_button(root, 100.0, 80.0, MouseButton::Middle);
assert!(
!field.is_focused(),
"the button that can take the focus can give it back"
);
}
#[test]
fn a_right_press_inside_the_field_keeps_it() {
let (mut screen, field) = a_focused_field();
let root = screen.root;
screen.press_button(root, 100.0, 8.0, MouseButton::Right);
assert!(
field.is_focused(),
"a press inside the field is the field's, whatever the button"
);
}
#[test]
fn a_when_focused_box_holding_no_focus_claims_nothing() {
let field = a_field();
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.layout(Flex::column())
.child(
container()
.padding(8.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE))
.child(text_input(create_signal(String::new())).widget_ref(field)),
)
.child(
container()
.width(200.0)
.height(40.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE)),
),
);
screen.press(100.0, 12.0);
assert!(field.is_focused(), "the press reached the field");
screen.press(100.0, 50.0);
assert!(
!field.is_focused(),
"a box that declares when_focused while the focus is elsewhere has claimed nothing, and the press is still a press on nothing"
);
}
#[test]
fn a_press_in_one_surface_does_not_blur_a_field_focused_in_another() {
let (mut screen, field) = a_focused_field();
let other = screen.add(container().width(200.0).height(100.0));
screen.press_button(other, 100.0, 80.0, MouseButton::Left);
assert!(
field.is_focused(),
"a press on another surface says nothing about where this one's \
keyboard is"
);
}
#[test]
fn a_press_on_the_padding_of_a_when_focused_box_keeps_it() {
let field = a_field();
let mut screen = Screen::new(
container().width(200.0).height(100.0).child(
container()
.padding(8.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE))
.child(text_input(create_signal(String::new())).widget_ref(field)),
),
);
screen.press(100.0, 12.0);
assert!(field.is_focused(), "the press reached the field");
screen.press(4.0, 4.0);
assert!(
field.is_focused(),
"the padding of the box that lights up for this focus belongs to it"
);
}
#[test]
fn a_box_that_keeps_the_focus_does_not_swallow_the_press() {
let field = a_field();
let clicks = std::rc::Rc::new(std::cell::Cell::new(0u32));
let counted = std::rc::Rc::clone(&clicks);
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.on_click(move || counted.set(counted.get() + 1))
.child(
container()
.padding(8.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE))
.child(text_input(create_signal(String::new())).widget_ref(field)),
),
);
screen.click(100.0, 12.0);
assert!(field.is_focused(), "the press reached the field");
assert_eq!(
clicks.get(),
0,
"which the field claimed, so the row did not"
);
screen.click(4.0, 4.0);
assert!(
field.is_focused(),
"the padding of the box that lights up for this focus belongs to it"
);
assert_eq!(
clicks.get(),
1,
"and the row around it is still a row that can be clicked"
);
}
#[test]
fn a_claim_does_not_outlive_the_press_that_made_it() {
let field = a_field();
let mut screen = Screen::new(
container().width(200.0).height(100.0).child(
container()
.padding(8.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE))
.child(text_input(create_signal(String::new())).widget_ref(field)),
),
);
screen.press(100.0, 12.0);
assert!(field.is_focused(), "the press reached the field");
screen.press(4.0, 4.0);
assert!(field.is_focused(), "the box's padding claims this one");
screen.press(100.0, 80.0);
assert!(
!field.is_focused(),
"the claim belonged to the press before it, not to this one"
);
}
#[test]
fn a_box_offset_from_the_surface_is_asked_in_its_own_coordinates() {
let field = a_field();
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.layout(Flex::column())
.child(container().width(200.0).height(50.0))
.child(
container().layout(Flex::column()).child(
container()
.width(200.0)
.height(40.0)
.padding(8.0)
.when_focused(|s| s.border(2.0, crate::widgets::Color::WHITE))
.child(text_input(create_signal(String::new())).widget_ref(field)),
),
),
);
screen.press(100.0, 65.0);
assert!(field.is_focused(), "the press reached the field");
screen.press(100.0, 85.0);
assert!(
field.is_focused(),
"the padding of the box belongs to it wherever the box happens to be"
);
}
#[test]
fn a_field_keeping_its_own_focus_does_not_swallow_the_press() {
let field = a_field();
let menus = std::rc::Rc::new(std::cell::Cell::new(0u32));
let counted = std::rc::Rc::clone(&menus);
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.on_right_click(move || counted.set(counted.get() + 1))
.child(text_input(create_signal(String::new())).widget_ref(field)),
);
screen.press(100.0, 8.0);
assert!(field.is_focused(), "the press reached the field");
let root = screen.root;
screen.press_button(root, 100.0, 8.0, MouseButton::Right);
assert!(
field.is_focused(),
"a press inside the field is the field's, whatever the button"
);
assert_eq!(
menus.get(),
1,
"and the row around it still opens its context menu"
);
}
#[test]
fn a_press_a_button_claimed_keeps_it() {
let field = a_field();
let mut screen = Screen::new(
container()
.width(200.0)
.height(100.0)
.layout(Flex::column())
.child(text_input(create_signal(String::new())).widget_ref(field))
.child(container().width(200.0).height(40.0).on_click(|| {})),
);
screen.press(100.0, 8.0);
assert!(field.is_focused(), "the press reached the field");
screen.press(100.0, 40.0);
assert!(
field.is_focused(),
"a widget that claimed the press said the press was its own; the \
focus is not up for grabs"
);
}
}
#[cfg(test)]
mod a_frame_is_a_description_not_a_connection {
use super::*;
fn pacing(callback_pending: bool) -> surface::FramePacing {
let mut pacing = surface::FramePacing::default();
pacing.committed();
if !callback_pending {
pacing.callback_done();
}
pacing
}
fn frame(callback_pending: bool) -> Frame {
Frame {
events: Vec::new(),
scale_factor: 1.0,
width: 200,
height: 50,
pacing: pacing(callback_pending),
}
}
fn geometry(needs_resize: bool, scale_changed: bool) -> Geometry {
Geometry {
physical_width: 200,
physical_height: 50,
needs_resize,
scale_changed,
declare_destination: None,
}
}
#[test]
fn a_frame_can_be_described_without_a_display() {
let f = frame(false);
assert_eq!((f.width, f.height), (200, 50));
}
#[test]
fn a_callback_still_in_flight_paces_a_frame_out() {
assert!(paced_out(&frame(true), &geometry(false, false)));
}
#[test]
fn a_resize_and_a_scale_change_each_bypass_the_gate() {
assert!(!paced_out(&frame(true), &geometry(true, false)));
assert!(!paced_out(&frame(true), &geometry(false, true)));
}
#[test]
fn no_callback_in_flight_paces_nothing_out() {
assert!(!paced_out(&frame(false), &geometry(false, false)));
}
}
#[cfg(test)]
fn or_skip<T>(made: Option<T>) -> Option<T> {
match made {
Some(made) => Some(made),
None if std::env::var_os("GUIDO_GPU_REQUIRED").is_some() => {
panic!("GUIDO_GPU_REQUIRED is set and no GPU adapter was found")
}
None => {
eprintln!("no GPU adapter; skipping");
None
}
}
}
#[cfg(test)]
fn managed_surface(config: SurfaceConfig, tree: &mut Tree) -> ManagedSurface {
let (widget, owner) = with_owner(|| Box::new(widgets::container()) as Box<dyn Widget>);
ManagedSurface::new(SurfaceId::next(), config, widget, owner, tree)
}
#[cfg(test)]
mod a_second_host_can_answer_for_a_compositor {
use super::*;
use crate::platform::Anchor;
use crate::surface::{ExclusiveZone, Margin, SurfaceConfig};
#[derive(Default)]
struct Recorder {
configured: Option<(u32, u32)>,
exclusive_zones: Vec<i32>,
sizes_asked: Vec<(u32, u32)>,
}
impl Surface for Recorder {
fn open_frame(&mut self) -> Option<Frame> {
None
}
fn pacing(&self) -> crate::surface::FramePacing {
crate::surface::FramePacing::default()
}
fn mark_frame_callback_pending(&mut self) {}
fn configured_size(&self) -> Option<(u32, u32)> {
self.configured
}
fn set_exclusive_zone(&mut self, zone: i32) {
self.exclusive_zones.push(zone);
}
fn set_size(&mut self, width: u32, height: u32) {
self.sizes_asked.push((width, height));
}
}
fn confirmed(width: u32, height: u32) -> Recorder {
Recorder {
configured: Some((width, height)),
..Default::default()
}
}
fn bar() -> SurfaceConfig {
SurfaceConfig::new()
.height(32)
.anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
.exclusive_zone(ExclusiveZone::Auto)
}
#[test]
fn a_bar_reserving_automatically_asks_for_the_height_it_declared() {
let mut surface = confirmed(800, 32);
publish_exclusive_zone(&mut surface, &bar(), None, &mut None);
assert_eq!(surface.exclusive_zones, vec![32]);
}
#[test]
fn only_the_margin_facing_away_from_the_anchor_is_asked_for() {
let mut surface = confirmed(800, 32);
publish_exclusive_zone(
&mut surface,
&bar().margin(Margin::from([8, 0, 0, 0])),
None,
&mut None,
);
assert_eq!(
surface.exclusive_zones,
vec![32],
"the top margin is not ours to add"
);
let mut surface = confirmed(800, 32);
publish_exclusive_zone(&mut surface, &bar().margin(Margin::all(8)), None, &mut None);
assert_eq!(surface.exclusive_zones, vec![40], "the bottom margin is");
}
fn side_dock() -> SurfaceConfig {
SurfaceConfig::new()
.width(surface::content())
.anchor(Anchor::LEFT | Anchor::TOP | Anchor::BOTTOM)
.exclusive_zone(ExclusiveZone::Auto)
}
#[test]
fn a_dock_whose_width_is_unmeasured_reserves_nothing_rather_than_the_whole_output() {
let mut surface = confirmed(1920, 1080);
publish_exclusive_zone(&mut surface, &side_dock(), None, &mut None);
assert!(
surface.exclusive_zones.is_empty(),
"reserved {:?} against a width the compositor imposed",
surface.exclusive_zones
);
}
#[test]
fn the_dock_reserves_the_measured_width_not_the_confirmed_one() {
let mut surface = confirmed(1920, 1080);
publish_exclusive_zone(&mut surface, &side_dock(), Some((240, 1080)), &mut None);
assert_eq!(surface.exclusive_zones, vec![240]);
}
#[test]
fn a_bar_asks_for_its_own_height_and_leaves_the_stretched_width_to_the_compositor() {
let mut tree = Tree::new();
let managed = managed_surface(bar(), &mut tree);
let mut surface = confirmed(1920, 32);
let (needs_measure, root) = send_size(&managed, &mut surface);
assert_eq!(surface.sizes_asked, vec![(0, 32)]);
assert!(!needs_measure, "two fixed axes have nothing to measure");
assert_eq!(
root, managed.widget_id,
"and the layout job names this root"
);
}
#[test]
fn a_dock_asks_for_the_width_it_was_confirmed_at_rather_than_the_placeholder() {
let mut tree = Tree::new();
let managed = managed_surface(side_dock(), &mut tree);
let mut surface = confirmed(240, 1080);
let (needs_measure, _) = send_size(&managed, &mut surface);
assert_eq!(surface.sizes_asked, vec![(240, 0)]);
assert!(
needs_measure,
"and the width is still the surface's own, so it is still waiting on a measure"
);
}
#[test]
fn a_constant_reservation_on_an_unmeasured_dock_is_not_deferred() {
let mut surface = Recorder::default();
publish_exclusive_zone(
&mut surface,
&side_dock().exclusive_zone(48u32),
None,
&mut None,
);
assert_eq!(surface.exclusive_zones, vec![48]);
}
}
#[cfg(test)]
mod what_the_loop_waits_for {
use super::*;
use std::time::{Duration, Instant};
const FRAME: Duration = Duration::from_millis(16);
#[test]
fn a_device_waiting_to_go_is_a_deadline() {
let Some(gpu) = crate::or_skip(renderer::GpuContext::try_new()) else {
return;
};
let closed = Instant::now();
let slot = GpuSlot::Lazy {
gpu: Some(gpu),
idle_since: Some(closed),
};
assert_eq!(wake_deadline(&slot), Some(closed + renderer::GPU_GRACE));
assert_eq!(
wake_deadline(&GpuSlot::lazy()),
None,
"nothing held, nothing to wake for"
);
}
fn idle() -> Pending {
Pending {
force_render: false,
jobs: false,
wake_requested: false,
deadline: None,
}
}
#[test]
fn an_idle_loop_blocks() {
assert_eq!(wait_for(&idle(), Instant::now()), None);
}
#[test]
fn any_pending_work_polls_at_frame_rate() {
for pending in [
Pending {
jobs: true,
..idle()
},
Pending {
force_render: true,
..idle()
},
Pending {
wake_requested: true,
..idle()
},
] {
assert_eq!(wait_for(&pending, Instant::now()), Some(FRAME));
}
}
#[test]
fn a_deadline_is_slept_to_exactly() {
let now = Instant::now();
let pending = Pending {
deadline: Some(now + Duration::from_millis(500)),
..idle()
};
assert_eq!(wait_for(&pending, now), Some(Duration::from_millis(500)));
}
#[test]
fn a_deadline_in_the_past_does_not_wait() {
let now = Instant::now();
let pending = Pending {
deadline: Some(now - Duration::from_secs(1)),
..idle()
};
assert_eq!(wait_for(&pending, now), Some(Duration::ZERO));
}
#[test]
fn pending_work_outranks_a_later_deadline() {
let now = Instant::now();
let pending = Pending {
jobs: true,
deadline: Some(now + Duration::from_secs(10)),
..idle()
};
assert_eq!(wait_for(&pending, now), Some(FRAME));
}
}
#[cfg(test)]
mod a_frame_lands_where_the_surface_points {
use super::*;
use crate::renderer::{GpuContext, RenderTarget};
use crate::surface::SurfaceConfig;
const W: u32 = 100;
const H: u32 = 32;
struct OneSurface {
width: u32,
height: u32,
}
impl Surface for OneSurface {
fn open_frame(&mut self) -> Option<Frame> {
Some(Frame {
events: Vec::new(),
scale_factor: 1.0,
width: self.width,
height: self.height,
pacing: crate::surface::FramePacing::default(),
})
}
fn pacing(&self) -> crate::surface::FramePacing {
crate::surface::FramePacing::default()
}
fn mark_frame_callback_pending(&mut self) {}
}
#[test]
fn a_surface_nobody_has_told_a_scale_is_at_one() {
let surface = OneSurface {
width: W,
height: H,
};
assert_eq!(surface.scale_factor(), 1.0);
}
struct OnePlatform(OneSurface);
struct OneHandle<'a>(&'a mut OneSurface);
impl Surface for OneHandle<'_> {
fn open_frame(&mut self) -> Option<Frame> {
self.0.open_frame()
}
fn pacing(&self) -> crate::surface::FramePacing {
self.0.pacing()
}
fn mark_frame_callback_pending(&mut self) {
self.0.mark_frame_callback_pending()
}
}
impl Platform for OnePlatform {
type Surface<'a> = OneHandle<'a>;
fn surface(&mut self, _id: SurfaceId) -> Option<OneHandle<'_>> {
Some(OneHandle(&mut self.0))
}
fn read_selection(&mut self, _kind: reactive::SelectionKind, token: u64) {
reactive::clipboard::answer_paste(token, None);
}
}
#[test]
fn a_target_the_wrong_size_is_resized_once_and_not_again() {
let Some(gpu) = or_skip(GpuContext::try_new()) else {
return;
};
let mut tree = Tree::new();
let mut surface = managed_surface(SurfaceConfig::new(), &mut tree);
surface.wgpu_surface = Some(RenderTarget::offscreen(&gpu, 8, 8));
let mut renderer = Renderer::new(
gpu.device.clone(),
gpu.queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
);
let mut platform = OnePlatform(OneSurface {
width: W,
height: H,
});
let id = surface.id;
let mut ctx = FrameContext {
id,
surface: &mut surface,
wayland_state: &mut platform,
renderer: &mut renderer,
tree: &mut tree,
};
let frame = ctx.wayland_state.surface(id).unwrap().open_frame().unwrap();
let first = resolve_geometry(&mut ctx, &frame);
assert!(first.needs_resize, "an 8x8 target for a 100x32 frame");
assert_eq!(
(
ctx.surface.wgpu_surface.as_ref().unwrap().width(),
ctx.surface.wgpu_surface.as_ref().unwrap().height()
),
(W, H)
);
let again = resolve_geometry(&mut ctx, &frame);
assert!(!again.needs_resize, "the target already fits");
}
}
#[cfg(test)]
mod a_layout_is_attributed_to_the_widget_that_ran_it {
use super::*;
use crate::layout::{Constraints, Size};
use crate::reactive::create_signal;
use crate::tree::LayoutCtx;
struct Plain(crate::reactive::Signal<f32>);
impl widgets::Widget for Plain {
fn layout(&mut self, _ctx: &mut LayoutCtx, _constraints: Constraints) -> Size {
let extent = self.0.get();
Size::new(extent, extent)
}
fn paint(&self, _ctx: &mut crate::renderer::PaintContext) {}
}
#[test]
fn a_leaf_that_opens_no_scope_is_still_the_one_marked() {
let extent = create_signal(20.0f32);
let mut tree = Tree::new();
let root = tree.register(Box::new(
widgets::container()
.width(200.0)
.child(Plain(extent.into())),
));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
tree.layout_widget(root, Constraints::new(0.0, 0.0, 400.0, 400.0));
let leaf = tree.get_children(root)[0];
extent.set(40.0);
let roots = [root].into_iter().collect();
jobs::distribute_jobs(&tree, &roots);
let drained = jobs::drain_surface_jobs(root);
let mut layout_roots = Vec::new();
jobs::process_jobs(&drained, &mut tree, &mut layout_roots);
jobs::recycle_job_buffer(drained);
assert!(
tree.needs_layout(leaf),
"the widget that read the signal is the one that has to run again"
);
}
}