oriel-iced 0.1.0

iced integrator for the oriel headless data grid: satisfies the data and render seams for iced 0.14.
Documentation
//! The sort arrow — ag-grid's shape, drawn from quads: a straight stem
//! under a sharp **chevron** head (two 45° strokes meeting at the apex —
//! not a filled triangle, not curved), in the ambient text color.
//!
//! Why not a font glyph: iced's bundled FiraSans-Regular has only the
//! hairline `↑` (its tip vanishes at header sizes), the triangles `▲`/`▼`
//! aren't in it at all (they render via system-font fallback — a different
//! face per machine), and Iced-Icons ships no sort arrows. Why not
//! canvas/svg: both are opt-in iced features pulling heavy deps (lyon /
//! resvg) into every integrator. So the arrow is hand-rasterized with
//! `fill_quad`, like the resize grip's ticks: exact integer-pixel geometry,
//! renderer-agnostic, zero deps.
//!
//! The chevron's 45° strokes are antialiased analytically — a half-alpha
//! shoulder pixel beside every solid cell, the same coverage a font
//! rasterizer would produce — so they read as smooth, not stair-stepped.

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

/// Ink box: 9×9 — exactly the cap height of the 13 px header text beside
/// it (the calibrated 9 px ink), so the apex and the stem's base land on
/// the label's cap top and baseline rows, and the odd ink height centers
/// on integer pixels the same way the text's does (the parity chain).
const WIDTH: f32 = 9.0;
const HEIGHT: f32 = 9.0;
/// Chevron arm length in rows; each row steps 1 px outward (45°).
const CHEVRON_ROWS: u32 = 5;
/// Coverage of the ideal 45° edge through a pixel's corner.
const EDGE_ALPHA: f32 = 0.5;
/// Field-measured: pure centered arithmetic landed the ink 1 px high next
/// to the header text. The last step of optical centering is a measured
/// nudge, not arithmetic (see `column::TEXT_BOX_TOP` for the text version).
const NUDGE_Y: f32 = 1.0;

/// A sort arrow pointing `up` or down — chevron head, straight stem — in
/// the inherited text color (the header label's color, matching ag-grid).
/// Fixed 9×9 px ink, cap-height for the 13 px header text beside it.
#[derive(Debug, Clone, Copy)]
pub struct SortArrow {
    up: bool,
}

impl SortArrow {
    /// An upward (ascending) arrow.
    pub fn up() -> Self {
        Self { up: true }
    }

    /// A downward (descending) arrow.
    pub fn down() -> Self {
        Self { up: false }
    }
}

impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for SortArrow
where
    Renderer: renderer::Renderer,
{
    fn size(&self) -> Size<Length> {
        Size::new(Length::Fixed(WIDTH), Length::Fixed(HEIGHT))
    }

    fn layout(
        &mut self,
        _tree: &mut widget::Tree,
        _renderer: &Renderer,
        limits: &layout::Limits,
    ) -> layout::Node {
        layout::atomic(limits, WIDTH, HEIGHT)
    }

    fn draw(
        &self,
        _tree: &widget::Tree,
        renderer: &mut Renderer,
        _theme: &Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        _cursor: mouse::Cursor,
        _viewport: &Rectangle,
    ) {
        let bounds = layout.bounds();
        // Centering hands us a fractional origin; 1 px strips on fractional
        // pixels smear. Snap once so every strip shares the same rounding.
        let x0 = bounds.x.round();
        let y0 = bounds.y.round() + NUDGE_Y;
        let color = style.text_color;
        let edge = Color {
            a: color.a * EDGE_ALPHA,
            ..color
        };

        let mut quad = |x: f32, y: f32, w: f32, h: f32, color: Color| {
            renderer.fill_quad(
                renderer::Quad {
                    bounds: Rectangle {
                        x: x0 + x,
                        y: y0 + y,
                        width: w,
                        height: h,
                    },
                    border: Border::default(),
                    shadow: Shadow::default(),
                    snap: false,
                },
                color,
            );
        };

        // Rows count from the apex; flip for the down arrow.
        let fy = |row: u32| {
            if self.up {
                row as f32
            } else {
                HEIGHT - 1.0 - row as f32
            }
        };
        // The stem/apex column — the ink center.
        let center = (WIDTH - 1.0) / 2.0;

        // Stem: a crisp 1 px vertical, from under the apex to the base.
        let stem_y = if self.up { 1.0 } else { 0.0 };
        quad(center, stem_y, 1.0, HEIGHT - 1.0, color);

        // Chevron: each arm is 1 px solid stepping 1 px out per row, with a
        // half-alpha pixel on both sides of every cell — the analytic
        // rendering of a ~1.4 px 45° stroke (what a font rasterizer emits
        // for a thin diagonal). Shoulders that land on solid ink composite
        // invisibly. The apex row is the arms' shared origin, drawn once.
        for r in 0..CHEVRON_ROWS {
            let y = fy(r);
            let step = r as f32;
            if r == 0 {
                quad(center, y, 1.0, 1.0, color);
                quad(center - 1.0, y, 1.0, 1.0, edge);
                quad(center + 1.0, y, 1.0, 1.0, edge);
            } else {
                for side in [-1.0f32, 1.0] {
                    let x = center + side * step;
                    quad(x, y, 1.0, 1.0, color);
                    for shoulder in [-1.0f32, 1.0] {
                        let ex = x + shoulder;
                        if (0.0..WIDTH).contains(&ex) {
                            quad(ex, y, 1.0, 1.0, edge);
                        }
                    }
                }
            }
        }
    }
}

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