iced-runtime-kora 0.15.0

A renderer-agnostic runtime for iced (Playtron's Kora fork of iced_runtime)
Documentation
//! Popup actions and types for Wayland xdg_popup surfaces.
//!
//! This module provides types for creating and managing popup surfaces
//! that can appear outside the parent window bounds.

use iced_core::Rectangle;
use iced_core::layout::Limits;
use iced_core::window::Id;
use std::collections::BTreeSet;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::{Mutex, PoisonError};

/// Anchor position on the parent surface for popup positioning.
///
/// Based on `xdg_positioner::anchor` from the xdg-shell protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u32)]
pub enum Anchor {
    /// No anchor (use anchor_rect center)
    #[default]
    None = 0,
    /// Top edge of anchor_rect
    Top = 1,
    /// Bottom edge of anchor_rect
    Bottom = 2,
    /// Left edge of anchor_rect
    Left = 3,
    /// Right edge of anchor_rect
    Right = 4,
    /// Top-left corner of anchor_rect
    TopLeft = 5,
    /// Bottom-left corner of anchor_rect
    BottomLeft = 6,
    /// Top-right corner of anchor_rect
    TopRight = 7,
    /// Bottom-right corner of anchor_rect
    BottomRight = 8,
}

/// Gravity direction for the popup surface.
///
/// Based on `xdg_positioner::gravity` from the xdg-shell protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u32)]
pub enum Gravity {
    /// No gravity (centered on anchor point)
    #[default]
    None = 0,
    /// Popup above anchor point
    Top = 1,
    /// Popup below anchor point
    Bottom = 2,
    /// Popup left of anchor point
    Left = 3,
    /// Popup right of anchor point
    Right = 4,
    /// Popup above-left of anchor point
    TopLeft = 5,
    /// Popup below-left of anchor point
    BottomLeft = 6,
    /// Popup above-right of anchor point
    TopRight = 7,
    /// Popup below-right of anchor point
    BottomRight = 8,
}

/// Constraint adjustment flags for popup repositioning.
///
/// When the popup would be constrained (partially outside screen bounds),
/// the compositor can adjust its position based on these flags.
pub mod constraint_adjustment {
    /// No adjustment, the popup is clipped or fails
    pub const NONE: u32 = 0;
    /// Slide the popup along the X axis
    pub const SLIDE_X: u32 = 1;
    /// Slide the popup along the Y axis
    pub const SLIDE_Y: u32 = 2;
    /// Flip the anchor and gravity on the X axis
    pub const FLIP_X: u32 = 4;
    /// Flip the anchor and gravity on the Y axis
    pub const FLIP_Y: u32 = 8;
    /// Resize the popup on the X axis
    pub const RESIZE_X: u32 = 16;
    /// Resize the popup on the Y axis
    pub const RESIZE_Y: u32 = 32;
}

/// Settings for creating a popup surface.
#[derive(Debug, Clone)]
pub struct PopupSettings {
    /// ID of the parent surface (must exist)
    ///
    /// Only `iced_winit` also takes a popup here, to nest under; other backends need a window.
    /// The parent popup must have been drawn, or the new popup is reported closed at once.
    /// Nested popups should use `grab: false`: xdg-shell only lets a popup grab under the topmost
    /// grab, so a grab is dropped otherwise. `positioner.anchor_rect` is relative to the parent
    /// popup, and hiding or replacing a popup closes the popups nested under it first.
    pub parent: Id,
    /// Unique ID for this popup
    pub id: Id,
    /// Positioning information for the popup
    pub positioner: Positioner,
    /// Whether to grab keyboard and pointer input
    pub grab: bool,
    /// Ask the compositor to blur what is behind the popup.
    ///
    /// Needs a compositor implementing a blur protocol, and content that is
    /// translucent — an opaque card has nothing to show the blur through. The
    /// blurred area is the whole surface, so pair this with `shadow` rather
    /// than padding the surface out for a client-drawn one.
    pub blur: bool,
    /// Ask the compositor to draw a drop shadow behind the popup.
    ///
    /// The alternative to `Positioner::shadow_padding` and drawing one, and
    /// the only one that combines with `blur`.
    pub shadow: bool,
    /// Corner radius hinted to the compositor, in logical pixels.
    ///
    /// Rounds the blur and the shadow to match the content drawn on the
    /// surface; without it both are square while the content is rounded.
    pub corner_radius: Option<u32>,
    /// When true, the popup surface will have an empty input region so
    /// pointer events pass through to the surface below. Useful for tooltips.
    pub input_passthrough: bool,
    /// When set, the compositor will position this popup at the pointer cursor
    /// plus the given (x, y) offset, eliminating client-side reposition round-trips.
    /// Requires compositor support for zcosmic_tooltip_manager_v1.
    pub tooltip_offset: Option<(i32, i32)>,
    /// Tooltip anchor corner (protocol enum value):
    /// 0 = top_left (default), 1 = top_right, 2 = bottom_left, 3 = bottom_right.
    /// Determines which corner of the tooltip aligns to the computed position.
    pub tooltip_anchor: Option<u32>,
    /// Show delay in milliseconds. When set to a non-zero value, the tooltip
    /// appears only after the pointer has hovered over the parent surface for
    /// this duration. The tooltip is positioned at the pointer location when
    /// the delay expires and stays fixed until hidden.
    /// 0 or None = immediate (follows pointer).
    pub tooltip_delay_ms: Option<u32>,
}

impl Hash for PopupSettings {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

/// Positioner configuration for popup placement.
#[derive(Debug, Clone)]
pub struct Positioner {
    /// Size of the popup (if None, will be auto-sized)
    pub size: Option<(u32, u32)>,
    /// Size limits for auto-sizing
    pub size_limits: Limits,
    /// Rectangle on the parent surface to anchor to
    pub anchor_rect: Rectangle<i32>,
    /// Where on the anchor_rect to attach
    pub anchor: Anchor,
    /// Direction the popup extends from the anchor point
    pub gravity: Gravity,
    /// How to adjust if popup would be constrained
    pub constraint_adjustment: u32,
    /// Offset from the calculated position
    pub offset: (i32, i32),
    /// Whether popup should reposition when parent moves
    pub reactive: bool,
    /// Window geometry (x, y, width, height) in logical pixels.
    ///
    /// Tells the compositor which part of the surface is visible content
    /// (excluding shadows/decorations). The compositor uses this for
    /// constraint adjustment (SLIDE/FLIP) — only the geometry rect is
    /// kept on-screen, not the full surface.
    pub window_geometry: Option<(i32, i32, i32, i32)>,
    /// Shadow padding in logical pixels.
    ///
    /// When set and `size` is None (auto-sizing), iced will automatically
    /// compute `window_geometry` from the measured content size minus this
    /// padding on each side. This tells the compositor to only constrain
    /// the visible content area, not the shadow.
    pub shadow_padding: u32,
}

impl Hash for Positioner {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.size.hash(state);
        self.anchor_rect.x.hash(state);
        self.anchor_rect.y.hash(state);
        self.anchor_rect.width.hash(state);
        self.anchor_rect.height.hash(state);
        self.anchor.hash(state);
        self.gravity.hash(state);
        self.constraint_adjustment.hash(state);
        self.offset.hash(state);
        self.reactive.hash(state);
        self.window_geometry.hash(state);
        self.shadow_padding.hash(state);
    }
}

impl Default for Positioner {
    fn default() -> Self {
        Self {
            size: None,
            size_limits: Limits::NONE
                .min_height(1.0)
                .min_width(1.0)
                .max_width(400.0)
                .max_height(600.0),
            anchor_rect: Rectangle {
                x: 0,
                y: 0,
                width: 1,
                height: 1,
            },
            anchor: Anchor::None,
            gravity: Gravity::None,
            constraint_adjustment: constraint_adjustment::SLIDE_X
                | constraint_adjustment::SLIDE_Y
                | constraint_adjustment::FLIP_X
                | constraint_adjustment::FLIP_Y,
            offset: (0, 0),
            reactive: true,
            window_geometry: None,
            shadow_padding: 0,
        }
    }
}

/// Actions for popup surfaces.
#[derive(Clone)]
pub enum Action {
    /// Create a popup surface
    Show {
        /// Popup configuration
        settings: PopupSettings,
    },
    /// Destroy a popup surface
    Hide {
        /// ID of the popup to destroy
        id: Id,
    },
    /// Resize a popup surface
    Resize {
        /// ID of the popup
        id: Id,
        /// New width
        width: u32,
        /// New height
        height: u32,
    },
    /// Reposition a popup surface using xdg_popup.reposition (v3).
    ///
    /// Updates the popup's positioner and asks the compositor to
    /// reposition it accordingly. Useful for tooltips that follow
    /// the cursor or need to adjust position dynamically.
    Reposition {
        /// ID of the popup to reposition
        id: Id,
        /// New positioner configuration
        positioner: Positioner,
    },
}

impl fmt::Debug for Action {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Action::Show { settings } => f
                .debug_struct("Action::Show")
                .field("settings", settings)
                .finish(),
            Action::Hide { id } => f.debug_struct("Action::Hide").field("id", id).finish(),
            Action::Resize { id, width, height } => f
                .debug_struct("Action::Resize")
                .field("id", id)
                .field("width", width)
                .field("height", height)
                .finish(),
            Action::Reposition { id, positioner } => f
                .debug_struct("Action::Reposition")
                .field("id", id)
                .field("positioner", positioner)
                .finish(),
        }
    }
}

// ============================================================================
// Task Helpers
// ============================================================================

use crate::Task;
use crate::platform_specific;
use crate::task;

/// Show a popup surface.
///
/// The popup will be positioned relative to the parent surface using
/// the positioner settings.
pub fn show<Message>(settings: PopupSettings) -> Task<Message> {
    task::effect(crate::Action::PlatformSpecific(
        platform_specific::Action::Wayland(super::Action::Popup(Action::Show { settings })),
    ))
}

/// Hide (destroy) a popup surface.
pub fn hide<Message>(id: Id) -> Task<Message> {
    task::effect(crate::Action::PlatformSpecific(
        platform_specific::Action::Wayland(super::Action::Popup(Action::Hide { id })),
    ))
}

/// Resize a popup surface.
pub fn resize<Message>(id: Id, width: u32, height: u32) -> Task<Message> {
    task::effect(crate::Action::PlatformSpecific(
        platform_specific::Action::Wayland(super::Action::Popup(Action::Resize {
            id,
            width,
            height,
        })),
    ))
}

/// Reposition a popup surface.
///
/// Uses xdg_popup.reposition (protocol v3) to update the popup's
/// position without destroying and recreating it.
pub fn reposition<Message>(id: Id, positioner: Positioner) -> Task<Message> {
    task::effect(crate::Action::PlatformSpecific(
        platform_specific::Action::Wayland(super::Action::Popup(Action::Reposition {
            id,
            positioner,
        })),
    ))
}

/// Popups that take keyboard input, by ID.
///
/// A register rather than a [`PopupSettings`] field or an [`Action`] variant: backends match that
/// enum and apps build that struct field by field, so either would stop them compiling.
static KEYBOARD_INPUT: Mutex<BTreeSet<Id>> = Mutex::new(BTreeSet::new());

/// Deliver or drop the keyboard input of the popup `id`, which is dropped by default.
///
/// Delivered, the popup's key presses, modifiers and focus changes reach its widgets, and
/// subscriptions under its own ID, while the compositor gives it keyboard focus (a grabbing
/// popup, on most). Set it before [`show`]; only `iced_winit` reads it.
pub fn keyboard_input(id: Id, enabled: bool) {
    let mut popups = KEYBOARD_INPUT
        .lock()
        .unwrap_or_else(PoisonError::into_inner);
    let _ = if enabled {
        popups.insert(id)
    } else {
        popups.remove(&id)
    };
}

/// Whether the popup `id` takes keyboard input; a backend asks as it creates the popup.
pub fn takes_keyboard_input(id: Id) -> bool {
    KEYBOARD_INPUT
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .contains(&id)
}

/// Forget the popup `id`'s keyboard input; a backend drops it with the popup.
pub fn forget_keyboard_input(id: Id) {
    let _ = KEYBOARD_INPUT
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .remove(&id);
}