//! `PropertyPanel` — right-rail node inspector (Step 6).
//!
//! Mirrors `apps/web/src/components/panels/right-panel.tsx` and the
//! per-section TS files (`*-section.tsx`). The bulk of the paint
//! logic lives in [`super::property_panel_sections`] — this file
//! holds the `PropertyPanel` struct, the `Widget` impl, and wiring
//! around snapshot extraction. Splitting the file keeps the pieces
//! under the openpencil 800-line ceiling.
//!
//! Sections (top → bottom, mirroring TS order):
//!   1. Tab strip (Design / Code)
//!   2. Header (kind label) + Create component button
//!   3. Position — X / Y / rotation / R
//!   4. Flex layout — 3 layout-mode buttons
//!   5. Size — W / H + 5 sizing checkboxes
//!   6. Layer — opacity row
//!   7. Fill — solid color rows + add affordance
//!   8. Stroke — color + width row
//!   9. Effects — empty list + add affordance
//!  10. Export — scale + format dropdowns
//!
//! Conditional rendering: TS app does `{hasSelection && <RightPanel/>}`.
//! Host calls [`PropertyPanel::for_selection`] which returns
//! `Option<Self>`; `None` = panel hidden entirely.

use crate::theme::Theme;
use crate::widgets::editor_state_ext::theme_for;
use crate::widgets::property_panel_sections as sections;
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
use crate::{Point2D, Rect};
use op_editor_core::PropertyFocus;

use op_editor_core::EditorState;

pub const PROPERTY_PANEL_WIDTH: f32 = 280.0;

// `PropertyPanelAction` lives in `property_panel_action.rs` (split
// out for the 800-line ceiling); re-exported so every existing
// `widgets::PropertyPanelAction` / `property_panel::PropertyPanelAction`
// path is unchanged.
pub use crate::widgets::property_panel_action::{
    LayoutAlignValue, LayoutJustifyValue, PropertyPanelAction, TextAlignValue, TextGrowthValue,
    TextVerticalAlignValue,
};

// `SectionCapabilities` lives in `property_panel_layout.rs`
// alongside `VisibleSections` (the section-visibility mask it
// feeds); re-exported so `property_panel::SectionCapabilities`
// resolves unchanged.
pub(crate) use crate::widgets::property_panel_layout::SectionCapabilities;
pub use crate::widgets::property_panel_snapshot::{
    EffectKind, EffectSummary, EllipseArcSummary, GradientStopSummary, NodeSnapshot,
};

pub struct PropertyPanel {
    pub id: WidgetId,
    pub snapshot: NodeSnapshot,
    pub theme: Theme,
    /// Localised chrome strings — `Document::t` lookups resolved
    /// once at construction time so every section paint hands
    /// straight to the renderer without re-walking the i18n table.
    pub labels: sections::PropertyLabels,
    /// Which input row the user is editing. `None` when no input
    /// is focused (panel paints all values from the snapshot).
    pub focus: Option<PropertyFocus>,
    /// Live edit-buffer for the focused input. Empty when nothing
    /// is focused. The host fills this on click + mutates on
    /// keystroke; the panel paints it as the field's value.
    pub draft: String,
    /// Caret byte-offset into `draft` (ASCII drafts → char index).
    pub caret_pos: usize,
    /// Caret-blink anchor (ms since host start) for the focused
    /// input. Drives the same `jian_core::anim::blink_visible`
    /// helper the chat caret uses.
    pub caret_anchor_ms: u64,
    /// Host clock ms; paired with `caret_anchor_ms` for caret blink.
    pub now_ms: u64,
    /// Active flex-layout button.
    pub flex_layout: op_editor_core::FlexLayout,
    /// 5 size checkboxes — fill / hug / clip.
    pub size_flags: sections::SizeFlags,
    /// Active fill type — drives the dropdown label + picker.
    pub fill_type: op_editor_core::FillType,
    pub fill_type_picker_open: bool,
    pub image_fill_popover_open: bool,
    pub font_family_picker_open: bool,
    pub font_family_picker_scroll: f32,
    pub system_font_families: Vec<String>,
    /// True for multi-select aggregate (inputs inert, "N items").
    pub is_multi: bool,
    /// Active header tab — toggled by Cmd+Shift+C.
    pub tab: op_editor_core::PropertyTab,
    /// Current export format + scale, shown on the Export section's
    /// two dropdowns. Clicking a dropdown opens its inline select
    /// popup (NOT the Export modal).
    pub export_format: op_editor_core::ExportFormat,
    pub export_scale: f32,
    /// Whether the Export section's scale / format inline select
    /// popups are open.
    pub export_scale_picker_open: bool,
    pub export_format_picker_open: bool,
    /// Row index the cursor is over in the open Export select
    /// popup — `None` when no popup is open or no row is hovered.
    pub export_picker_hover: Option<usize>,
    /// Vertical scroll offset (px, ≥ 0) — paint + hit-test shift the
    /// section content up by this so a tall inspector stays usable.
    pub scroll: f32,
    /// Active UI locale — threaded into the Fill section so its
    /// type label / picker / body sub-labels translate.
    pub locale: op_editor_core::Locale,
    /// Focused effect-parameter value, if any — drives the Effects
    /// section's editable value boxes.
    pub effect_param_focus: Option<op_editor_core::editor_ui_state::EffectParamFocus>,
}

impl PropertyPanel {
    /// Capability mask that drives which sections paint for this
    /// panel state. Multi-select uses a dedicated mask (`for_multi`)
    /// that keeps Size + Position + Layer + Effects + Export and
    /// hides Flex + Fill + Stroke; single-select falls back to the
    /// snapshot's `kind_variant` (`for_kind`). Paint + hit-test
    /// must call this rather than `SectionCapabilities::for_kind`
    /// directly so the multi-select carve-out can't regress
    /// silently.
    pub(crate) fn capabilities(&self) -> SectionCapabilities {
        if self.is_multi {
            SectionCapabilities::for_multi()
        } else {
            SectionCapabilities::for_kind(&self.snapshot.kind_variant)
        }
    }
}

impl PropertyPanel {
    /// Conditional builder — returns `Some` only when the editor
    /// has an active selection. Mirrors TS `{hasSelection && ...}`.
    pub fn for_selection(state: &EditorState) -> Option<Self> {
        Self::for_selection_at(state, 0)
    }

    /// Same as [`for_selection`] but threads the host's monotonic
    /// millisecond clock through so the focused-input caret can
    /// blink off the same animation timer as the chat input.
    pub fn for_selection_at(state: &EditorState, now_ms: u64) -> Option<Self> {
        if state.selection_count() == 1 {
            let node = state.selected_node()?;
            let fill_type = op_editor_core::first_fill_type(node);
            return Some(Self::build_from_snapshot(
                state,
                NodeSnapshot::from_node(node),
                fill_type,
                now_ms,
                false,
            ));
        }
        if state.selection_count() >= 2 {
            let snapshot = NodeSnapshot::from_multi_selection(state)?;
            return Some(Self::build_from_snapshot(
                state,
                snapshot,
                op_editor_core::FillType::Solid,
                now_ms,
                true,
            ));
        }
        None
    }

    fn build_from_snapshot(
        state: &EditorState,
        snapshot: NodeSnapshot,
        fill_type: op_editor_core::FillType,
        now_ms: u64,
        is_multi: bool,
    ) -> Self {
        let ui = &state.editor_ui;
        let flex_layout = snapshot.flex_layout;
        let size_flags = sections::SizeFlags {
            fill_width: snapshot.size_fill_width,
            fill_height: snapshot.size_fill_height,
            hug_width: snapshot.size_hug_width,
            hug_height: snapshot.size_hug_height,
            clip_content: snapshot.size_clip_content,
        };
        Self {
            id: WidgetId::new(2000),
            snapshot,
            theme: theme_for(ui),
            labels: sections::PropertyLabels::for_editor_ui(ui),
            // Multi-select inputs are inert in v1 — broadcast edits
            // to all selected nodes lands later. Force focus to None
            // so the panel paints all values muted and hit_test
            // returns None (see `hit_test` is_multi short-circuit).
            focus: if is_multi {
                None
            } else {
                state.ui.property_focus
            },
            draft: if is_multi {
                String::new()
            } else {
                state.ui.property_input_draft.clone()
            },
            caret_pos: if is_multi {
                0
            } else {
                state.ui.property_caret_pos
            },
            caret_anchor_ms: state.ui.property_caret_anchor_ms,
            now_ms,
            flex_layout,
            size_flags,
            fill_type,
            fill_type_picker_open: ui.fill_type_picker_open,
            image_fill_popover_open: ui.image_fill_popover_open,
            font_family_picker_open: ui.font_family_picker_open,
            font_family_picker_scroll: ui.font_family_picker_scroll,
            system_font_families: ui.system_font_families.clone(),
            is_multi,
            tab: ui.property_tab,
            export_format: ui.export_format,
            export_scale: ui.export_scale,
            export_scale_picker_open: ui.export_scale_picker_open,
            export_format_picker_open: ui.export_format_picker_open,
            export_picker_hover: ui.export_picker_hover,
            scroll: ui.property_panel_scroll.max(0.0),
            locale: ui.locale,
            // Inert in the multi-select aggregate view.
            effect_param_focus: if is_multi {
                None
            } else {
                ui.effect_param_focus
            },
        }
    }

    /// `self.scroll` clamped to the current content's scrollable
    /// range. The host only re-clamps the stored offset on a wheel
    /// event, so selecting a shorter node (fewer sections / effects)
    /// could otherwise leave the panel scrolled past its end —
    /// every paint / hit-test reads through this so the view
    /// self-corrects on the very next frame.
    fn effective_scroll(&self, panel_rect: Rect) -> f32 {
        let max = (self.content_height(panel_rect) - panel_rect.size.y).max(0.0);
        self.scroll.clamp(0.0, max)
    }

    /// `panel_rect` shifted up by the (clamped) scroll offset. Both
    /// paint and every hit-test walker start their y-walk from this
    /// rect, so the panel scrolls as one piece and clicks stay
    /// aligned with what is drawn.
    fn scrolled_rect(&self, panel_rect: Rect) -> Rect {
        Rect {
            origin: Point2D::new(
                panel_rect.origin.x,
                panel_rect.origin.y - self.effective_scroll(panel_rect),
            ),
            size: panel_rect.size,
        }
    }

    /// Whether `point` is inside the scrolling section viewport —
    /// the panel below the pinned tab strip. A click in the tab-strip
    /// band must not fall through to a section row scrolled up
    /// under it (paint clips there; hit-test must agree).
    fn point_in_section_viewport(&self, panel_rect: Rect, point: Point2D) -> bool {
        point.y >= panel_rect.origin.y + crate::widgets::property_panel_inputs::TAB_HEIGHT
    }

    /// Total height (px) of the panel's section content — drives the
    /// scroll clamp so the inspector can't scroll past its end.
    pub fn content_height(&self, panel_rect: Rect) -> f32 {
        sections::property_panel_content_height(
            panel_rect,
            self.visible_sections(),
            &self.snapshot.effects,
        )
    }

    /// Section-visibility mask for the current selection, threaded
    /// into every layout walker so paint + hit-test stay aligned.
    fn visible_sections(&self) -> sections::VisibleSections {
        let caps = self.capabilities();
        sections::VisibleSections {
            create_component: caps.create_component && self.snapshot.can_create_component,
            flex_layout: caps.flex_layout,
            flex_layout_mode: self.snapshot.flex_layout,
            layout_justify: self.snapshot.layout_justify,
            layout_align: self.snapshot.layout_align,
            size_options: caps.size_options,
            clip_content: self.snapshot.can_clip_content,
            text: caps.text && self.snapshot.text.is_some(),
            icon: self.snapshot.icon.is_some(),
            image: caps.image && self.snapshot.is_image_node,
            opacity: caps.opacity,
            corner_radius: self.snapshot.has_corner_radius,
            polygon_sides: self.snapshot.polygon_sides.is_some(),
            ellipse_arc: self.snapshot.ellipse_arc.is_some(),
            fill: caps.fill,
            stroke: caps.stroke,
            effects: caps.effects,
            export: caps.export,
            fill_type: self.fill_type,
            gradient_stop_count: self.snapshot.gradient_stops.len(),
        }
    }

    /// Hit-test the flex / size buttons + checkboxes. Returns the
    /// action the host should dispatch, or `None` if the cursor
    /// missed every clickable shape. Called AFTER `hit_test` so
    /// text inputs win over the action rects they overlap with.
    pub fn hit_test_action(&self, panel_rect: Rect, point: Point2D) -> Option<PropertyPanelAction> {
        if self.is_multi {
            // Multi-select inputs / toggles are inert in v1.
            return None;
        }
        if self.image_fill_popover_open {
            if let Some(action) = sections::image_fill_popover_action_at(
                self.scrolled_rect(panel_rect),
                self.visible_sections(),
                &self.snapshot,
                point,
            ) {
                return Some(action);
            }
        }
        if self.font_family_picker_open {
            if let Some(text) = self.snapshot.text.as_ref() {
                if let Some(action) =
                    crate::widgets::property_panel_font_picker::font_family_picker_action_at(
                        self.scrolled_rect(panel_rect),
                        self.visible_sections(),
                        &text.font_family,
                        &self.system_font_families,
                        self.font_family_picker_scroll,
                        point,
                    )
                {
                    return Some(action);
                }
            }
        }
        if !self.point_in_section_viewport(panel_rect, point) {
            return None;
        }
        let rects = sections::action_button_rects_with_font_picker(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            &self.snapshot.effects,
            sections::ActionButtonRectOptions {
                system_fonts: &self.system_font_families,
                active_font_family: self
                    .snapshot
                    .text
                    .as_ref()
                    .map(|text| text.font_family.as_str())
                    .unwrap_or(""),
                fill_picker_open: self.fill_type_picker_open,
                font_family_picker_open: self.font_family_picker_open,
                export_scale_picker_open: self.export_scale_picker_open,
                export_format_picker_open: self.export_format_picker_open,
            },
        );
        // Picker rows live in `rects` AFTER the dropdown rect, so
        // a row hit takes priority — `rev()` makes the picker rows
        // tested first and short-circuits before the dropdown
        // toggle, otherwise clicking a row would just re-toggle.
        for (action, rect) in rects.into_iter().rev() {
            if rect_contains(rect, point) {
                return Some(action);
            }
        }
        None
    }

    /// Row index of the open Export select popup under `point`, or
    /// `None` when no popup is open / the cursor is off every row.
    /// The index counts only the option rows (`SetExportScale` /
    /// `SetExportFormat`), matching `paint_select_popup`'s row walk,
    /// so it can drive the popup's hover highlight.
    pub fn export_picker_row_at(&self, panel_rect: Rect, point: Point2D) -> Option<usize> {
        if !self.export_scale_picker_open && !self.export_format_picker_open {
            return None;
        }
        if !self.point_in_section_viewport(panel_rect, point) {
            return None;
        }
        sections::action_button_rects_with_font_picker(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            &self.snapshot.effects,
            sections::ActionButtonRectOptions {
                system_fonts: &self.system_font_families,
                active_font_family: self
                    .snapshot
                    .text
                    .as_ref()
                    .map(|text| text.font_family.as_str())
                    .unwrap_or(""),
                fill_picker_open: self.fill_type_picker_open,
                font_family_picker_open: self.font_family_picker_open,
                export_scale_picker_open: self.export_scale_picker_open,
                export_format_picker_open: self.export_format_picker_open,
            },
        )
        .into_iter()
        .filter(|(a, _)| {
            matches!(
                a,
                PropertyPanelAction::SetExportScale(_) | PropertyPanelAction::SetExportFormat(_)
            )
        })
        .position(|(_, rect)| rect_contains(rect, point))
    }

    pub fn font_family_picker_bounds(&self, panel_rect: Rect) -> Option<Rect> {
        let text = self.snapshot.text.as_ref()?;
        crate::widgets::property_panel_font_picker::font_family_picker_rect(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            &text.font_family,
            &self.system_font_families,
        )
    }

    pub fn font_family_picker_max_scroll(&self, panel_rect: Rect) -> f32 {
        let Some(text) = self.snapshot.text.as_ref() else {
            return 0.0;
        };
        crate::widgets::property_panel_font_picker::font_family_picker_max_scroll(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            &text.font_family,
            &self.system_font_families,
        )
    }

    pub fn font_family_picker_row_rect(&self, panel_rect: Rect, family: &str) -> Option<Rect> {
        let text = self.snapshot.text.as_ref()?;
        crate::widgets::property_panel_font_picker::font_family_picker_row_rect(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            &text.font_family,
            &self.system_font_families,
            self.font_family_picker_scroll,
            family,
        )
    }

    pub fn image_adjustment_drag_action(
        &self,
        panel_rect: Rect,
        field: op_editor_core::ImageAdjustmentField,
        x: f32,
    ) -> Option<PropertyPanelAction> {
        if self.is_multi || !self.image_fill_popover_open {
            return None;
        }
        sections::image_fill_popover_adjustment_action_for_drag(
            self.scrolled_rect(panel_rect),
            self.visible_sections(),
            field,
            x,
        )
    }

    pub fn image_fill_popover_contains(&self, panel_rect: Rect, point: Point2D) -> bool {
        !self.is_multi
            && self.image_fill_popover_open
            && sections::image_fill_popover_contains(
                self.scrolled_rect(panel_rect),
                self.visible_sections(),
                point,
            )
    }

    /// Hit-test the panel at `point` and return which input row
    /// (if any) contains the click. The layout walk mirrors the
    /// per-kind section filtering applied in `paint`, so rects
    /// after a skipped section don't drift out of alignment.
    pub fn hit_test(&self, panel_rect: Rect, point: Point2D) -> Option<PropertyFocus> {
        if self.is_multi {
            // Inputs inert in v1 multi-select aggregate view.
            return None;
        }
        if !self.point_in_section_viewport(panel_rect, point) {
            return None;
        }
        for (focus, rect) in
            sections::editable_input_rects(self.scrolled_rect(panel_rect), self.visible_sections())
        {
            if rect_contains(rect, point) {
                return Some(focus);
            }
        }
        None
    }
}

fn rect_contains(r: Rect, p: Point2D) -> bool {
    p.x >= r.origin.x
        && p.x <= r.origin.x + r.size.x
        && p.y >= r.origin.y
        && p.y <= r.origin.y + r.size.y
}

use crate::widgets::property_panel_code::paint_code_placeholder;

impl Widget for PropertyPanel {
    fn id(&self) -> WidgetId {
        self.id
    }

    fn layout(&self, cx: &LayoutCx) -> LayoutBox {
        // Vertical extent is "as much as you give me" — the host
        // clips at the rail rect. Reporting 800 here is just a
        // placeholder for the abstract widget tree.
        LayoutBox {
            rect: Rect {
                origin: Point2D::new(0.0, 0.0),
                size: Point2D::new(cx.available_width, 800.0),
            },
        }
    }

    fn paint(&self, cx: &mut PaintCx<'_>, rect: Rect) {
        cx.backend.fill_rect(rect, self.theme.card);
        cx.backend.fill_rect(
            Rect {
                origin: rect.origin,
                size: Point2D::new(1.0, rect.size.y),
            },
            self.theme.border,
        );

        let x = rect.origin.x;
        let w = rect.size.x;
        // The Design / Code tab strip is pinned to the panel top —
        // painted fixed, above (and never scrolled with) the section
        // content.
        let tab_bottom =
            sections::paint_tab_strip(cx, &self.theme, &self.labels, self.tab, x, rect.origin.y, w);
        let edit_ctx = sections::EditContext {
            focus: self.focus,
            draft: self.draft.as_str(),
            caret: self.caret_pos,
            caret_anchor_ms: self.caret_anchor_ms,
            now_ms: self.now_ms,
        };
        let caps = self.capabilities();
        if matches!(self.tab, op_editor_core::PropertyTab::Code) {
            paint_code_placeholder(cx, &self.theme, &self.snapshot, x, tab_bottom, w);
            return;
        }
        // Section content scrolls below the pinned tab strip; clip it
        // so a scrolled-up section can't paint over the tabs or bleed
        // onto the neighbouring rail. Overlays (fill / export pickers)
        // anchor to `scrolled` — the same shifted rect the layout
        // walker uses (it adds `TAB_HEIGHT`), so paint + hit-test of
        // the sections agree.
        cx.backend.save();
        cx.backend.clip_rect(Rect {
            origin: Point2D::new(x, tab_bottom),
            size: Point2D::new(w, (rect.origin.y + rect.size.y - tab_bottom).max(0.0)),
        });
        let scroll = self.effective_scroll(rect);
        let scrolled = Rect {
            origin: Point2D::new(rect.origin.x, rect.origin.y - scroll),
            size: rect.size,
        };
        // First section sits just below the pinned tab strip:
        // `tab_bottom - scroll` == `scrolled.origin.y + TAB_HEIGHT`,
        // matching the layout walker's `+= TAB_HEIGHT` step.
        let mut y = tab_bottom - scroll;
        y = sections::paint_node_header(cx, &self.theme, &self.snapshot, x, y, w);
        if caps.create_component && self.snapshot.can_create_component {
            y = sections::paint_create_component(cx, &self.theme, &self.labels, x, y, w);
        }
        y = sections::paint_position_section(
            cx,
            &self.theme,
            &self.snapshot,
            &edit_ctx,
            &self.labels,
            self.snapshot.has_corner_radius,
            x,
            y,
            w,
        );
        if caps.flex_layout {
            y = crate::widgets::property_panel_flex::paint_flex_section(
                cx,
                &self.theme,
                &self.snapshot,
                &edit_ctx,
                &self.labels,
                self.locale,
                x,
                y,
                w,
            );
        }
        if caps.size_options {
            y = sections::paint_size_section(
                cx,
                &self.theme,
                &self.snapshot,
                &edit_ctx,
                &self.labels,
                self.size_flags,
                self.snapshot.can_clip_content,
                x,
                y,
                w,
            );
        }
        if self.snapshot.icon.is_some() {
            y = crate::widgets::property_panel_icon::paint_icon_section(
                cx,
                &self.theme,
                &self.snapshot,
                self.locale,
                x,
                y,
                w,
            );
        }
        if caps.text && self.snapshot.text.is_some() {
            y = crate::widgets::property_panel_text::paint_text_section(
                cx,
                &self.theme,
                &self.snapshot,
                &edit_ctx,
                self.locale,
                x,
                y,
                w,
            );
        }
        if caps.image && self.snapshot.is_image_node {
            y = crate::widgets::property_panel_image_node::paint_image_node_section(
                cx,
                &self.theme,
                &self.snapshot,
                self.locale,
                x,
                y,
                w,
            );
        }
        if caps.opacity {
            y = sections::paint_layer_section(
                cx,
                &self.theme,
                &self.snapshot,
                &self.labels,
                &edit_ctx,
                x,
                y,
                w,
            );
        }
        if caps.fill {
            y = sections::paint_fill_section(
                cx,
                &self.theme,
                &self.snapshot,
                &edit_ctx,
                &self.labels,
                self.fill_type,
                self.fill_type_picker_open,
                self.locale,
                x,
                y,
                w,
            );
        }
        if caps.stroke {
            y = sections::paint_stroke_section(
                cx,
                &self.theme,
                &self.snapshot,
                &edit_ctx,
                &self.labels,
                x,
                y,
                w,
            );
        }
        if caps.effects {
            y = sections::paint_effects_section(
                cx,
                &self.theme,
                &self.labels,
                &self.snapshot.effects,
                &edit_ctx,
                self.effect_param_focus,
                x,
                y,
                w,
            );
        }
        if caps.export {
            let _ = sections::paint_export_section(
                cx,
                &self.theme,
                &self.labels,
                self.export_format,
                self.export_scale,
                x,
                y,
                w,
            );
        }
        // Fill-type picker overlay sits on top of everything below
        // the Fill section so it can extend past the section divider.
        if caps.fill && self.fill_type_picker_open {
            sections::paint_fill_type_picker(
                cx,
                &self.theme,
                scrolled,
                self.visible_sections(),
                self.fill_type,
                self.locale,
            );
        }
        if caps.text && self.font_family_picker_open {
            if let Some(text) = self.snapshot.text.as_ref() {
                crate::widgets::property_panel_font_picker::paint_font_family_picker(
                    cx,
                    &self.theme,
                    scrolled,
                    self.visible_sections(),
                    &text.font_family,
                    &self.system_font_families,
                    self.font_family_picker_scroll,
                );
            }
        }
        // Export-section inline select popups — painted last so the
        // scale / format dropdown overlays sit above every section.
        if caps.export && (self.export_scale_picker_open || self.export_format_picker_open) {
            sections::paint_export_picker(
                cx,
                &self.theme,
                scrolled,
                self.visible_sections(),
                &self.snapshot.effects,
                self.export_scale_picker_open,
                self.export_format_picker_open,
                self.export_scale,
                self.export_format,
                self.export_picker_hover,
            );
        }
        cx.backend.restore();
    }

    fn access_node(&self) -> accesskit::Node {
        let mut node = accesskit::Node::new(accesskit::Role::Group);
        node.set_label(self.snapshot.kind.clone());
        node
    }
}

impl PropertyPanel {
    /// Paint inspector overlays that are allowed to extend out of the
    /// right rail. Hosts call this late in their composition pass so
    /// the image-fill popover is above floating canvas controls.
    pub fn paint_overlays(&self, cx: &mut PaintCx<'_>, rect: Rect) {
        let caps = self.capabilities();
        if !(caps.fill || caps.image) || !self.image_fill_popover_open {
            return;
        }
        let scroll = self.effective_scroll(rect);
        let scrolled = Rect {
            origin: Point2D::new(rect.origin.x, rect.origin.y - scroll),
            size: rect.size,
        };
        sections::paint_image_fill_popover(
            cx,
            &self.theme,
            scrolled,
            self.visible_sections(),
            &self.snapshot,
            self.locale,
        );
    }
}
