guido 0.6.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
//! Surface lifecycle management for Guido applications.
//!
//! This module provides types for managing the lifecycle of surfaces,
//! including GPU initialization and widget layout.

use rustc_hash::FxHashMap;

use crate::layout::Constraints;
use crate::reactive::owner::{OwnerId, dispose_owner_now};
use crate::renderer::{
    CommandLayer, FlattenScratch, FlattenedCommand, GpuContext, RenderNode, RenderTarget,
};
use crate::surface::{SurfaceConfig, SurfaceId};
use crate::tree::{Tree, WidgetId};
use crate::widgets::{Rect, Widget};
use crate::{Platform, Surface};

/// A surface with unified GPU lifecycle management.
///
/// This combines the widget tree, GPU surface state, and configuration
/// into a single struct that manages the surface's entire lifecycle.
pub struct ManagedSurface {
    /// The unique identifier for this surface
    pub id: SurfaceId,
    /// Configuration for the surface
    pub config: SurfaceConfig,
    /// The root widget ID (widget is stored in the tree)
    pub widget_id: WidgetId,
    /// Owner for reactive primitives created in the widget factory
    owner_id: OwnerId,
    /// Where this surface's frames land (None until GPU init).
    pub wgpu_surface: Option<RenderTarget>,
    /// Previous scale factor for detecting changes
    pub previous_scale_factor: f32,
    /// Root render node (reused across frames to avoid allocation)
    pub root_node: RenderNode,
    /// Flattened commands buffer (reused across frames to avoid allocation)
    pub flattened_commands: Vec<FlattenedCommand>,
    /// Draw groups over `flattened_commands`, in draw order.
    pub command_layers: Vec<CommandLayer>,
    /// Where flatten builds those two before it fills them (reused across
    /// frames to avoid allocation)
    pub flatten_scratch: FlattenScratch,
    /// Where input reaches this surface before any container has spoken:
    /// what the config declared, and what a handle has said since.
    ///
    /// One home for it, so a runtime `set_input_region` and a declaration in
    /// the tree compose instead of erasing each other.
    pub input_base: Option<Vec<Rect>>,
    /// The region this surface last asked the compositor for, or `None` while
    /// no container has ever declared one.
    ///
    /// Kept here rather than in a platform so that both of them publish on the
    /// same rule: once, when it changes, and never for a surface whose tree has
    /// said nothing about input.
    pub input_region: Option<crate::region::InputRegionRequest>,
    /// The logical size this surface last declared its buffer stands for, or
    /// `None` while it has declared none.
    ///
    /// Here beside the reservation and the input region because it is the same
    /// kind of thing: a double-buffered request the compositor already holds,
    /// which a frame with nothing new to say must not send again.
    pub viewport_destination: Option<(u32, u32)>,
    /// The reservation this surface last asked the compositor for, starting
    /// with the one a layer surface is created with, so a number the compositor
    /// already has is not sent again. Popups and lock surfaces carry one too and
    /// never send it.
    pub exclusive_zone: Option<i32>,
}

impl ManagedSurface {
    /// Create a new managed surface (wgpu_surface is None until GPU init).
    /// The root widget and its children are registered in the tree.
    pub fn new(
        id: SurfaceId,
        config: SurfaceConfig,
        widget: Box<dyn Widget>,
        owner_id: OwnerId,
        tree: &mut Tree,
    ) -> Self {
        let config_input_region = config.input_region.clone();
        let exclusive_zone = Some(crate::surface::initial_declaration(&config).exclusive_zone);

        // Register root widget - tree assigns the ID
        let widget_id = tree.register(widget);

        // Register children recursively with the assigned widget ID
        tree.with_widget_mut(widget_id, |widget, id, tree| {
            widget.register_children(tree, id);
        });

        Self {
            id,
            config,
            widget_id,
            owner_id,
            wgpu_surface: None,
            previous_scale_factor: 1.0,
            root_node: RenderNode::new(widget_id.as_u64()),
            flattened_commands: Vec::new(),
            command_layers: Vec::new(),
            flatten_scratch: FlattenScratch::default(),
            input_base: config_input_region,
            input_region: None,
            viewport_destination: None,
            exclusive_zone,
        }
    }

    /// Initialize GPU surface. Returns true if successful.
    ///
    /// `at` is the moment the first layout runs at — see
    /// [`Self::layout_widget`] for why it is the caller's to name.
    pub fn init_gpu<P: Platform>(
        &mut self,
        gpu_context: &GpuContext,
        platform: &P,
        (width, height): (u32, u32),
        scale_factor: f32,
        tree: &mut Tree,
        at: std::time::Instant,
    ) -> bool {
        if self.wgpu_surface.is_some() {
            return true; // Already initialized
        }

        let (physical_width, physical_height) =
            crate::surface::buffer_size((width, height), scale_factor);

        log::info!(
            "Creating render target for {:?}: logical {}x{}, physical {}x{}, scale {}",
            self.id,
            width,
            height,
            physical_width,
            physical_height,
            scale_factor
        );

        let Some(target) =
            platform.create_render_target(self.id, gpu_context, (physical_width, physical_height))
        else {
            return false;
        };
        self.wgpu_surface = Some(target);
        self.previous_scale_factor = scale_factor;

        // Perform initial layout
        self.layout_widget(tree, width as f32, height as f32, at);

        true
    }

    /// Check if GPU is initialized.
    pub fn is_gpu_ready(&self) -> bool {
        self.wgpu_surface.is_some()
    }

    /// Perform widget layout with the given dimensions, at the moment `at`.
    ///
    /// This is a surface's *first* layout, and a pass like any other: an enter
    /// animation is seeded here and advanced by the frame that follows, so the
    /// two have to be reading one clock. Undeclared, this one took the wall
    /// clock while the frame took the loop's `frame_at`, which on a bar is
    /// microseconds of nothing and to a driver that names its own instant is
    /// an enter already over before its first frame.
    pub fn layout_widget(&self, tree: &mut Tree, width: f32, height: f32, at: std::time::Instant) {
        let constraints = Constraints::new(0.0, 0.0, width, height);

        tree.set_frame_instant(Some(at));
        tree.layout_widget(self.widget_id, constraints);
        // Set root widget origin after layout
        tree.set_origin(self.widget_id, 0.0, 0.0);
        // The pass is over, and so is its instant — the rule `render_surface`
        // keeps for the same reason: a stale clock fails silently.
        tree.set_frame_instant(None);
    }
}

impl Drop for ManagedSurface {
    fn drop(&mut self) {
        dispose_owner_now(self.owner_id);
    }
}

/// Tear down a closed surface's widget tree synchronously.
///
/// Removing a `ManagedSurface` only disposes its reactive owner; without
/// this, the widget subtree stays registered in the [`Tree`] forever (a
/// leak that grows with every closed popup) and its signal subscribers
/// stay live — the next write to a subscribed signal reconciles a dead
/// dynamic child whose closure reads owner-disposed state and panics.
///
/// Widgets are unregistered children-first, so each Drop's deferred
/// Unregister requests target already-removed ids and no-op; any stale
/// queued job no-ops too because the ids fail the tree's generation check.
///
/// Call this BEFORE dropping the `ManagedSurface`: subscribers must be
/// gone before the owner (and the signals it holds) is disposed.
pub(crate) fn teardown_widget_subtree(tree: &mut Tree, root: WidgetId) {
    crate::jobs::teardown_widget_subtree(tree, root);
}

/// Manages all surfaces in the application.
pub struct SurfaceManager {
    surfaces: FxHashMap<SurfaceId, ManagedSurface>,
}

impl SurfaceManager {
    /// Create a new empty surface manager.
    pub fn new() -> Self {
        Self {
            surfaces: FxHashMap::default(),
        }
    }

    /// Add a surface.
    pub fn add(&mut self, surface: ManagedSurface) {
        self.surfaces.insert(surface.id, surface);
    }

    /// Remove a surface by ID.
    pub fn remove(&mut self, id: SurfaceId) -> Option<ManagedSurface> {
        self.surfaces.remove(&id)
    }

    /// Get a surface by ID.
    pub fn get(&self, id: SurfaceId) -> Option<&ManagedSurface> {
        self.surfaces.get(&id)
    }

    /// Get a mutable surface by ID.
    pub fn get_mut(&mut self, id: SurfaceId) -> Option<&mut ManagedSurface> {
        self.surfaces.get_mut(&id)
    }

    /// Iterate over all surface IDs.
    pub fn ids(&self) -> impl Iterator<Item = SurfaceId> + '_ {
        self.surfaces.keys().copied()
    }

    /// Check if empty.
    pub fn is_empty(&self) -> bool {
        self.surfaces.is_empty()
    }

    /// First initialized wgpu surface, if any.
    ///
    /// Used to create the shared `Renderer` lazily: apps may start with zero
    /// surfaces and spawn them dynamically (e.g. one bar per output).
    pub fn first_gpu_surface(&self) -> Option<&RenderTarget> {
        self.surfaces.values().find_map(|s| s.wgpu_surface.as_ref())
    }

    /// Initialize GPU for surfaces that need it, laying each out at `at`.
    ///
    /// This iterates over all surfaces and initializes GPU for any
    /// that are configured in Wayland but don't yet have a wgpu surface.
    pub fn init_pending_gpu<P: Platform>(
        &mut self,
        gpu_context: &GpuContext,
        wayland_state: &mut P,
        tree: &mut Tree,
        at: std::time::Instant,
    ) {
        for (id, surface) in self.surfaces.iter_mut() {
            if surface.is_gpu_ready() {
                continue;
            }

            // A surface with no confirmed size has nothing to build a target
            // at, and one with no confirmed scale would build it at the wrong
            // one.
            let facts = wayland_state
                .surface(*id)
                .and_then(|s| Some((s.configured_size()?, s.scale_factor()?)));
            let Some(((width, height), scale)) = facts else {
                continue;
            };
            surface.init_gpu(gpu_context, wayland_state, (width, height), scale, tree, at);
        }
    }
}

impl Default for SurfaceManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use std::cell::Cell;
    use std::rc::Rc;

    use super::*;
    use crate::layout::{Constraints, Size};
    use crate::reactive::owner::{dispose_owner_now as dispose, with_owner};
    use crate::reactive::{create_memo, create_signal};
    use crate::tree::LayoutCtx;
    use crate::widgets::Widget;
    use crate::widgets::children::ChildrenSource;
    use crate::widgets::into_child::{DynamicChild, IntoChild};

    struct TestWidget;
    impl Widget for TestWidget {
        fn layout(&mut self, _: &mut LayoutCtx, _: Constraints) -> Size {
            Size::zero()
        }
        fn paint(&self, _: &mut crate::renderer::PaintContext) {}
    }

    /// The surface-close crash scenario: a dynamic child reads a memo owned
    /// by the surface factory's owner scope; the surface closes (owner
    /// disposed); a later write to the tracked signal must NOT re-run the
    /// dead closure (which would read the disposed memo and panic) — and the
    /// subtree must actually leave the tree (the popup leak).
    #[test]
    fn teardown_clears_subscribers_and_empties_tree() {
        let mut tree = Tree::new();
        let parent = tree.register(Box::new(TestWidget));
        let mut source = ChildrenSource::default();
        source.set_container_id(parent);

        let sig = create_signal(0u64);
        let builds = Rc::new(Cell::new(0usize));

        // Mimic the surface factory: reactive state owned by a scope that
        // dies with the surface
        let (memo, owner_id) = with_owner(|| create_memo(move || sig.get() + 1));

        let counter = builds.clone();
        let closure = move || {
            let _ = memo.get();
            counter.set(counter.get() + 1);
            TestWidget
        };
        IntoChild::<DynamicChild>::add_to_container(closure, &mut source);
        source.reconcile_and_get(&mut tree);
        assert_eq!(builds.get(), 1);
        assert!(tree.widget_count() > 1);

        // Surface close: subtree teardown, THEN owner disposal
        teardown_widget_subtree(&mut tree, parent);
        dispose(owner_id);

        // The write that used to reconcile the dead child into a panic
        sig.set(1);
        source.reconcile_with_tracking(&mut tree);

        assert_eq!(builds.get(), 1, "dead closure must not re-run");
        assert_eq!(tree.widget_count(), 0, "subtree must leave the tree");
    }
}