oriel-iced 0.1.0

iced integrator for the oriel headless data grid: satisfies the data and render seams for iced 0.14.
Documentation
//! A phantom measurer — the auto-size (fit-to-content) mechanism.
//!
//! Cells are **opaque** elements (the render seam), so the grid cannot ask
//! "how wide is your content?" — but it can *lay an element out* against
//! generous limits and read its natural size. [`Measure`] does exactly that:
//! it owns fresh copies of one column's header + visible cell elements,
//! measures them during its own `layout` pass (where a renderer is available),
//! occupies zero space, draws nothing, and publishes the maximum natural width
//! once via its `update` pass. The grid then commits the width through the
//! core primitive (`ColumnLayout::set_width`) — measurement is inherently
//! integrator-side; the state transition is core.
//!
//! Elements that declare `Length::Fill` have no natural width; they resolve
//! to the measuring cap, and the grid treats a cap-hitting report as
//! "unmeasurable" and leaves the column's width alone (committing would slam
//! it to the policy maximum — clearly wrong when a theme switch auto-sizes
//! every column at once).

use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, renderer};
use iced::{Element, Event, Length, Rectangle, Size};

/// Width limit for measurement — effectively unbounded for real content while
/// keeping `Length::Fill` children finite. A report AT the cap means some
/// child asked to fill (no natural width).
pub(crate) const MEASURE_CAP: f32 = 2_000.0;

#[derive(Debug, Default)]
struct State {
    /// Max natural width found by the last layout pass (`None` while idle).
    measured: Option<f32>,
    /// The request epoch the current measurement was published for. One
    /// publish per epoch — an idle frame is NOT required between requests:
    /// a re-queue that lands in the same message batch as the previous
    /// drain bumps the epoch and re-publishes (the alternative — a plain
    /// one-shot flag re-armed only by an idle frame — wedged the column's
    /// fit permanently).
    published_for: Option<u64>,
}

/// The phantom measurer widget. Idle (no children) it is a zero-size no-op.
#[allow(missing_debug_implementations)]
pub struct Measure<'a, Message, Theme, Renderer> {
    children: Vec<Element<'a, Message, Theme, Renderer>>,
    epoch: u64,
    on_measured: Box<dyn Fn(f32) -> Message + 'a>,
}

impl<'a, Message, Theme, Renderer> Measure<'a, Message, Theme, Renderer> {
    /// Measures `children` (empty = idle) and publishes
    /// `on_measured(max natural width)` once per `epoch` — bump the epoch
    /// on every new measurement request.
    pub fn new(
        children: Vec<Element<'a, Message, Theme, Renderer>>,
        epoch: u64,
        on_measured: impl Fn(f32) -> Message + 'a,
    ) -> Self {
        Self {
            children,
            epoch,
            on_measured: Box::new(on_measured),
        }
    }
}

impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
    for Measure<'_, Message, Theme, Renderer>
where
    Renderer: renderer::Renderer,
{
    fn tag(&self) -> widget::tree::Tag {
        widget::tree::Tag::of::<State>()
    }

    fn state(&self) -> widget::tree::State {
        widget::tree::State::new(State::default())
    }

    fn children(&self) -> Vec<widget::Tree> {
        self.children.iter().map(widget::Tree::new).collect()
    }

    fn diff(&self, tree: &mut widget::Tree) {
        tree.diff_children(&self.children);
    }

    fn size(&self) -> Size<Length> {
        // NOT Fixed(0)×Fixed(0): iced's containers silently DISCARD children
        // whose size hint is void (`!child_size.is_void()`,
        // iced_widget-0.14.2/src/column.rs:147) — the phantom would never
        // enter the tree. Shrink escapes the filter, and the layout node is
        // zero-sized regardless.
        Size::new(Length::Shrink, Length::Shrink)
    }

    fn layout(
        &mut self,
        tree: &mut widget::Tree,
        renderer: &Renderer,
        _limits: &layout::Limits,
    ) -> layout::Node {
        let state = tree.state.downcast_mut::<State>();
        if self.children.is_empty() {
            // Idle: nothing to measure. (No re-arm needed — publishing is
            // per-epoch, not per-activation.)
            state.measured = None;
        } else {
            let unbounded =
                layout::Limits::new(Size::ZERO, Size::new(MEASURE_CAP, f32::INFINITY));
            let mut max = 0.0f32;
            for (element, tree) in self.children.iter_mut().zip(&mut tree.children) {
                let node = element.as_widget_mut().layout(tree, renderer, &unbounded);
                max = max.max(node.size().width);
            }
            state.measured = Some(max);
        }
        layout::Node::new(Size::ZERO)
    }

    fn update(
        &mut self,
        tree: &mut widget::Tree,
        _event: &Event,
        _layout: Layout<'_>,
        _cursor: mouse::Cursor,
        _renderer: &Renderer,
        _clipboard: &mut dyn iced::advanced::Clipboard,
        shell: &mut iced::advanced::Shell<'_, Message>,
        _viewport: &Rectangle,
    ) {
        // Events are never forwarded to the phantom children — they exist
        // only to be measured. Publish the result once per epoch.
        let state = tree.state.downcast_mut::<State>();
        if let Some(width) = state.measured {
            if state.published_for != Some(self.epoch) {
                state.published_for = Some(self.epoch);
                shell.publish((self.on_measured)(width));
            }
        }
    }

    fn draw(
        &self,
        _tree: &widget::Tree,
        _renderer: &mut Renderer,
        _theme: &Theme,
        _style: &renderer::Style,
        _layout: Layout<'_>,
        _cursor: mouse::Cursor,
        _viewport: &Rectangle,
    ) {
        // Phantom: draws nothing.
    }
}

impl<'a, Message, Theme, Renderer> From<Measure<'a, Message, Theme, Renderer>>
    for Element<'a, Message, Theme, Renderer>
where
    Message: 'a,
    Theme: 'a,
    Renderer: renderer::Renderer + 'a,
{
    fn from(measure: Measure<'a, Message, Theme, Renderer>) -> Self {
        Element::new(measure)
    }
}