liora-components 0.1.8

Enterprise-style native GPUI component library for Liora applications.
Documentation
//! Popconfirm module.
//!
//! This public module implements the Liora popover confirmation component for guarded actions. It keeps the reusable
//! component logic inside `liora-components` rather than Gallery or Docs so
//! downstream GPUI applications can compose the same behavior with their own
//! app state, assets, and release policy.
//!
//! ## Usage model
//!
//! Components in this module render native GPUI element trees. Stateless builder
//! values can be constructed inline, while controls with focus, selection,
//! popup, drag, or editing state should be stored as `gpui::Entity<T>` fields in
//! the parent view so state survives GPUI render passes.
//!
//! ## Design contract
//!
//! The implementation should use Liora theme tokens from `liora-core` and
//! `liora-theme`, keep accessibility-oriented keyboard/pointer behavior close to
//! the component, and avoid app-specific Gallery/Docs resources in this SDK
//! crate.

use crate::{Button, Popover};
use gpui::{
    AnyElement, App, Component, IntoElement, RenderOnce, SharedString, Window, div, prelude::*,
};
use liora_core::{Config, Placement, clear_popover};
use std::sync::Arc;

/// Fluent native GPUI component for rendering Liora popconfirm.
pub struct Popconfirm {
    trigger: AnyElement,
    title: SharedString,
    confirm_text: SharedString,
    cancel_text: SharedString,
    on_confirm: Option<Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
    on_cancel: Option<Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
    placement: Placement,
    close_on_click_outside: bool,
    close_on_escape: bool,
    trigger_id: Option<SharedString>,
}

impl Popconfirm {
    /// Creates `Popconfirm` initialized from the supplied trigger.
    pub fn new(trigger: impl IntoElement) -> Self {
        Self {
            trigger: trigger.into_any_element(),
            title: SharedString::default(),
            confirm_text: "Confirm".into(),
            cancel_text: "Cancel".into(),
            on_confirm: None,
            on_cancel: None,
            placement: Placement::Top,
            close_on_click_outside: true,
            close_on_escape: true,
            trigger_id: None,
        }
    }

    /// Sets the primary title text displayed by the component.
    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
        self.title = title.into();
        self
    }

    /// Sets the confirm text value used by the component.
    pub fn confirm_text(mut self, text: impl Into<SharedString>) -> Self {
        self.confirm_text = text.into();
        self
    }

    /// Sets the cancel text value used by the component.
    pub fn cancel_text(mut self, text: impl Into<SharedString>) -> Self {
        self.cancel_text = text.into();
        self
    }

    /// Registers a callback that runs when confirm occurs.
    pub fn on_confirm(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
        self.on_confirm = Some(Arc::new(f));
        self
    }

    /// Registers a callback that runs when cancel occurs.
    pub fn on_cancel(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
        self.on_cancel = Some(Arc::new(f));
        self
    }

    /// Selects the popup, label, or overlay placement.
    pub fn placement(mut self, placement: Placement) -> Self {
        self.placement = placement;
        self
    }

    /// Assigns a stable element id used by GPUI state, hit testing, and automated interaction tests.
    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
        self.trigger_id = Some(id.into());
        self
    }

    /// Toggles whether the popup closes when escape occurs.
    pub fn close_on_escape(mut self, close: bool) -> Self {
        self.close_on_escape = close;
        self
    }

    /// Toggles whether the popup closes when click outside occurs.
    pub fn close_on_click_outside(mut self, close: bool) -> Self {
        self.close_on_click_outside = close;
        self
    }
}

impl RenderOnce for Popconfirm {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let title = self.title.clone();
        let confirm_text = self.confirm_text.clone();
        let cancel_text = self.cancel_text.clone();
        let on_confirm = self.on_confirm.clone();
        let on_cancel = self.on_cancel.clone();
        let close_on_click_outside = self.close_on_click_outside;
        let close_on_escape = self.close_on_escape;
        let theme = cx.global::<Config>().theme.clone();
        let trigger_id = self
            .trigger_id
            .clone()
            .unwrap_or_else(|| format!("popconfirm-trigger-{}", title).into());
        let popover_id = trigger_id.clone();

        Popover::new(self.trigger)
            .id(trigger_id)
            .placement(self.placement)
            .close_on_click_outside(close_on_click_outside)
            .close_on_escape(close_on_escape)
            .content(move |_window, _cx| {
                let on_confirm = on_confirm.clone();
                let on_cancel = on_cancel.clone();
                let confirm_text = confirm_text.clone();
                let cancel_text = cancel_text.clone();
                let cancel_popover_id = popover_id.clone();
                let confirm_popover_id = popover_id.clone();

                div()
                    .p_4()
                    .flex()
                    .flex_col()
                    .gap_3()
                    .child(
                        div()
                            .flex()
                            .flex_row()
                            .items_center()
                            .gap_2()
                            .child(div().text_color(theme.warning.base).child("⚠️"))
                            .child(
                                div()
                                    .font_weight(gpui::FontWeight::BOLD)
                                    .child(title.clone()),
                            ),
                    )
                    .child(
                        div()
                            .flex()
                            .flex_row()
                            .justify_end()
                            .gap_2()
                            .child(Button::new(cancel_text.clone()).small().on_click(
                                move |_event, _window, _cx| {
                                    if let Some(ref f) = on_cancel {
                                        f(_window, _cx);
                                    }
                                    clear_popover(&cancel_popover_id, _cx);
                                },
                            ))
                            .child(
                                Button::new(confirm_text.clone())
                                    .primary()
                                    .small()
                                    .on_click(move |_event, _window, _cx| {
                                        if let Some(ref f) = on_confirm {
                                            f(_window, _cx);
                                        }
                                        clear_popover(&confirm_popover_id, _cx);
                                    }),
                            ),
                    )
            })
    }
}

impl IntoElement for Popconfirm {
    type Element = Component<Self>;
    fn into_element(self) -> Self::Element {
        Component::new(self)
    }
}