layer_shika_composition/
popup_builder.rs

1use crate::Result;
2use crate::popup::PopupShell;
3use layer_shika_domain::dimensions::LogicalRect;
4use layer_shika_domain::value_objects::handle::PopupHandle;
5use layer_shika_domain::value_objects::output_target::OutputTarget;
6use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment;
7use layer_shika_domain::value_objects::popup_config::PopupConfig;
8use layer_shika_domain::value_objects::popup_position::{
9    Alignment, AnchorPoint, Offset, PopupPosition,
10};
11use layer_shika_domain::value_objects::popup_size::PopupSize;
12
13/// Type state indicating the builder is not bound to a shell
14pub struct Unbound;
15
16/// Type state indicating the builder is bound to a shell
17pub struct Bound {
18    shell: PopupShell,
19}
20
21/// Builder for configuring popups
22///
23/// The builder uses phantom types to ensure compile-time safety:
24/// - [`PopupBuilder<Unbound>`] - Configuration only, cannot show popups
25/// - [`PopupBuilder<Bound>`] - Has shell reference, can show popups
26///
27/// # Example
28/// ```rust,ignore
29/// shell.on("Main", "open_menu", |control| {
30///     let popup_handle = control.popups().builder("MenuPopup")
31///         .at_cursor()
32///         .grab(true)
33///         .close_on("menu_closed")
34///         .show()?;
35/// });
36/// ```
37pub struct PopupBuilder<State = Unbound> {
38    state: State,
39    config: PopupConfig,
40}
41
42impl PopupBuilder<Unbound> {
43    /// Creates a new popup builder for the specified component
44    ///
45    /// This builder is unbound and cannot show popups directly.
46    /// Use [`PopupShell::builder`] to create a bound builder that can call `.show()`.
47    #[must_use]
48    pub fn new(component: impl Into<String>) -> Self {
49        Self {
50            state: Unbound,
51            config: PopupConfig::new(component),
52        }
53    }
54
55    #[must_use]
56    pub(crate) fn with_shell(self, shell: PopupShell) -> PopupBuilder<Bound> {
57        PopupBuilder {
58            state: Bound { shell },
59            config: self.config,
60        }
61    }
62}
63
64impl<State> PopupBuilder<State> {
65    #[must_use]
66    pub fn position(mut self, position: PopupPosition) -> Self {
67        self.config.position = position;
68        self
69    }
70
71    #[must_use]
72    pub fn at_cursor(self) -> Self {
73        self.position(PopupPosition::Cursor {
74            offset: Offset::default(),
75        })
76    }
77
78    #[must_use]
79    pub fn at_position(self, x: f32, y: f32) -> Self {
80        self.position(PopupPosition::Absolute { x, y })
81    }
82
83    #[must_use]
84    pub fn centered(self) -> Self {
85        self.position(PopupPosition::Centered {
86            offset: Offset::default(),
87        })
88    }
89
90    #[must_use]
91    pub fn relative_to_rect(
92        self,
93        rect: LogicalRect,
94        anchor: AnchorPoint,
95        alignment: Alignment,
96    ) -> Self {
97        self.position(PopupPosition::Element {
98            rect,
99            anchor,
100            alignment,
101        })
102    }
103
104    #[must_use]
105    pub fn offset(mut self, x: f32, y: f32) -> Self {
106        match &mut self.config.position {
107            PopupPosition::Absolute { x: abs_x, y: abs_y } => {
108                *abs_x += x;
109                *abs_y += y;
110            }
111            PopupPosition::Cursor { offset }
112            | PopupPosition::Centered { offset }
113            | PopupPosition::RelativeToParent { offset, .. } => {
114                offset.x += x;
115                offset.y += y;
116            }
117            PopupPosition::Element { .. } => {
118                self.config.position = PopupPosition::Cursor {
119                    offset: Offset { x, y },
120                };
121            }
122        }
123        self
124    }
125
126    #[must_use]
127    pub fn size(mut self, size: PopupSize) -> Self {
128        self.config.size = size;
129        self
130    }
131
132    #[must_use]
133    pub fn fixed_size(self, width: f32, height: f32) -> Self {
134        self.size(PopupSize::Fixed { width, height })
135    }
136
137    #[must_use]
138    pub fn min_size(self, width: f32, height: f32) -> Self {
139        self.size(PopupSize::Minimum { width, height })
140    }
141
142    #[must_use]
143    pub fn max_size(self, width: f32, height: f32) -> Self {
144        self.size(PopupSize::Maximum { width, height })
145    }
146
147    #[must_use]
148    pub fn content_sized(self) -> Self {
149        self.size(PopupSize::Content)
150    }
151
152    #[must_use]
153    pub fn grab(mut self, enable: bool) -> Self {
154        self.config.behavior.grab = enable;
155        self
156    }
157
158    #[must_use]
159    pub fn modal(mut self, enable: bool) -> Self {
160        self.config.behavior.modal = enable;
161        self
162    }
163
164    #[must_use]
165    pub fn close_on_click_outside(mut self) -> Self {
166        self.config.behavior.close_on_click_outside = true;
167        self
168    }
169
170    #[must_use]
171    pub fn close_on_escape(mut self) -> Self {
172        self.config.behavior.close_on_escape = true;
173        self
174    }
175
176    #[must_use]
177    pub fn constraint_adjustment(mut self, adjustment: ConstraintAdjustment) -> Self {
178        self.config.behavior.constraint_adjustment = adjustment;
179        self
180    }
181
182    #[must_use]
183    pub fn on_output(mut self, target: OutputTarget) -> Self {
184        self.config.output = target;
185        self
186    }
187
188    #[must_use]
189    pub fn on_primary(self) -> Self {
190        self.on_output(OutputTarget::Primary)
191    }
192
193    #[must_use]
194    pub fn on_active(self) -> Self {
195        self.on_output(OutputTarget::Active)
196    }
197
198    #[must_use]
199    pub fn parent(mut self, parent: PopupHandle) -> Self {
200        self.config.parent = Some(parent);
201        self
202    }
203
204    #[must_use]
205    pub const fn z_index(mut self, index: i32) -> Self {
206        self.config.z_index = index;
207        self
208    }
209
210    #[must_use]
211    pub fn close_on(mut self, callback_name: impl Into<String>) -> Self {
212        self.config.close_callback = Some(callback_name.into());
213        self
214    }
215
216    #[must_use]
217    pub fn resize_on(mut self, callback_name: impl Into<String>) -> Self {
218        self.config.resize_callback = Some(callback_name.into());
219        self
220    }
221
222    /// Builds the configuration without showing the popup
223    ///
224    /// Returns a [`PopupConfig`] that can be shown later using [`PopupShell::show`].
225    #[must_use]
226    pub fn build(self) -> PopupConfig {
227        self.config
228    }
229}
230
231impl PopupBuilder<Bound> {
232    /// Shows the popup with the configured settings
233    ///
234    /// This method is only available on builders created via [`PopupShell::builder`],
235    /// ensuring at compile time that the builder has access to a shell.
236    pub fn show(self) -> Result<PopupHandle> {
237        self.state.shell.show(self.config)
238    }
239}