layer_shika_composition/
selector.rs

1use crate::{OutputHandle, OutputInfo};
2use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId;
3use std::fmt::{Debug, Formatter, Result as FmtResult};
4use std::sync::Arc;
5
6/// Runtime information about a surface instance
7#[derive(Debug, Clone)]
8pub struct SurfaceInfo {
9    /// Surface component name
10    pub name: String,
11    /// Handle to the output displaying this surface
12    pub output: OutputHandle,
13    /// Unique identifier for this surface instance
14    pub instance_id: SurfaceInstanceId,
15}
16
17/// Selector for targeting surfaces in callbacks and runtime configuration
18///
19/// # Examples
20///
21/// ```ignore
22/// // Single surface by name
23/// shell.select(Surface::named("bar"))
24///     .on_callback("clicked", |ctx| { /* ... */ });
25///
26/// // Multiple surfaces
27/// shell.select(Surface::any(["bar", "panel"]))
28///     .set_property("visible", &Value::Bool(true));
29///
30/// // All except one
31/// shell.select(Surface::all().except(Surface::named("hidden")))
32///     .on_callback("update", |ctx| { /* ... */ });
33///
34/// // Custom filter
35/// shell.select(Surface::matching(|info| info.name.starts_with("popup")))
36///     .set_property("layer", &Value::String("overlay".into()));
37///
38/// // Combine with output selector
39/// Surface::named("bar").on(Output::primary())
40/// ```
41#[derive(Clone)]
42pub enum Surface {
43    /// Select all surfaces
44    All,
45    /// Select surface by exact name
46    Named(String),
47    /// Select any surface matching one of the given names
48    Any(Vec<String>),
49    /// Select surfaces matching a custom predicate
50    Filter(Arc<dyn Fn(&SurfaceInfo) -> bool + Send + Sync>),
51    /// Invert selection
52    Not(Box<Surface>),
53    /// Union of multiple selectors
54    Or(Vec<Surface>),
55}
56
57impl Surface {
58    /// Selects all surfaces
59    pub fn all() -> Self {
60        Self::All
61    }
62
63    /// Selects a surface by exact name
64    pub fn named(name: impl Into<String>) -> Self {
65        Self::Named(name.into())
66    }
67
68    /// Selects surfaces matching any of the given names
69    pub fn any(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
70        Self::Any(names.into_iter().map(Into::into).collect())
71    }
72
73    /// Selects surfaces matching a custom predicate
74    ///
75    /// ```ignore
76    /// Surface::matching(|info| info.name.starts_with("widget"))
77    /// ```
78    pub fn matching<F>(predicate: F) -> Self
79    where
80        F: Fn(&SurfaceInfo) -> bool + Send + Sync + 'static,
81    {
82        Self::Filter(Arc::new(predicate))
83    }
84
85    /// Combines this surface selector with an output selector
86    ///
87    /// ```ignore
88    /// Surface::named("bar").on(Output::primary())
89    /// Surface::all().on(Output::named("HDMI-1"))
90    /// ```
91    pub fn on(self, output: Output) -> Selector {
92        Selector {
93            surface: self,
94            output,
95        }
96    }
97
98    /// Inverts the selection to exclude matching surfaces
99    ///
100    /// ```ignore
101    /// Surface::all().except(Surface::named("hidden"))
102    /// ```
103    #[must_use]
104    pub fn except(self, other: impl Into<Surface>) -> Self {
105        Self::Not(Box::new(other.into()))
106    }
107
108    /// Combines this selector with another using OR logic
109    ///
110    /// ```ignore
111    /// Surface::named("bar").or(Surface::named("panel"))
112    /// ```
113    #[must_use]
114    pub fn or(self, other: impl Into<Surface>) -> Self {
115        match self {
116            Self::Or(mut selectors) => {
117                selectors.push(other.into());
118                Self::Or(selectors)
119            }
120            _ => Self::Or(vec![self, other.into()]),
121        }
122    }
123
124    pub(crate) fn matches(&self, info: &SurfaceInfo) -> bool {
125        match self {
126            Self::All => true,
127            Self::Named(name) => &info.name == name,
128            Self::Any(names) => names.iter().any(|name| name == &info.name),
129            Self::Filter(predicate) => predicate(info),
130            Self::Not(selector) => !selector.matches(info),
131            Self::Or(selectors) => selectors.iter().any(|s| s.matches(info)),
132        }
133    }
134}
135
136impl Debug for Surface {
137    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
138        match self {
139            Self::All => write!(f, "Surface::All"),
140            Self::Named(name) => write!(f, "Surface::Named({:?})", name),
141            Self::Any(names) => write!(f, "Surface::Any({:?})", names),
142            Self::Filter(_) => write!(f, "Surface::Filter(<fn>)"),
143            Self::Not(selector) => write!(f, "Surface::Not({:?})", selector),
144            Self::Or(selectors) => write!(f, "Surface::Or({:?})", selectors),
145        }
146    }
147}
148
149/// Selector for targeting outputs (monitors)
150///
151/// # Examples
152///
153/// ```ignore
154/// // Primary monitor only
155/// shell.select(Surface::named("bar").on(Output::primary()))
156///     .on_callback("clicked", |ctx| { /* ... */ });
157///
158/// // Specific output by name
159/// shell.select(Output::named("HDMI-1"))
160///     .set_property("scale", &Value::Number(1.5));
161///
162/// // Custom filter
163/// shell.select(Output::matching(|info| info.scale().unwrap_or(1) > 1))
164///     .set_property("enable-hidpi", &Value::Bool(true));
165/// ```
166#[derive(Clone)]
167pub enum Output {
168    /// Select all outputs
169    All,
170    /// Select the primary output
171    Primary,
172    /// Select the currently active output
173    Active,
174    /// Select output by handle
175    Handle(OutputHandle),
176    /// Select output by name
177    Named(String),
178    /// Select outputs matching a custom predicate
179    Filter(Arc<dyn Fn(&OutputInfo) -> bool + Send + Sync>),
180    /// Invert selection
181    Not(Box<Output>),
182    /// Union of multiple selectors
183    Or(Vec<Output>),
184}
185
186impl Output {
187    /// Selects all outputs
188    pub fn all() -> Self {
189        Self::All
190    }
191
192    /// Selects the primary output
193    pub fn primary() -> Self {
194        Self::Primary
195    }
196
197    /// Selects the currently active output
198    pub fn active() -> Self {
199        Self::Active
200    }
201
202    /// Selects an output by handle
203    pub fn handle(handle: OutputHandle) -> Self {
204        Self::Handle(handle)
205    }
206
207    /// Selects an output by name
208    pub fn named(name: impl Into<String>) -> Self {
209        Self::Named(name.into())
210    }
211
212    /// Selects outputs matching a custom predicate
213    ///
214    /// ```ignore
215    /// Output::matching(|info| info.is_primary())
216    /// ```
217    pub fn matching<F>(predicate: F) -> Self
218    where
219        F: Fn(&OutputInfo) -> bool + Send + Sync + 'static,
220    {
221        Self::Filter(Arc::new(predicate))
222    }
223
224    /// Inverts the selection to exclude matching outputs
225    ///
226    /// ```ignore
227    /// Output::all().except(Output::primary())
228    /// ```
229    #[must_use]
230    pub fn except(self, other: impl Into<Output>) -> Self {
231        Self::Not(Box::new(other.into()))
232    }
233
234    /// Combines this selector with another using OR logic
235    ///
236    /// ```ignore
237    /// Output::named("HDMI-1").or(Output::named("HDMI-2"))
238    /// ```
239    #[must_use]
240    pub fn or(self, other: impl Into<Output>) -> Self {
241        match self {
242            Self::Or(mut selectors) => {
243                selectors.push(other.into());
244                Self::Or(selectors)
245            }
246            _ => Self::Or(vec![self, other.into()]),
247        }
248    }
249
250    pub(crate) fn matches(
251        &self,
252        handle: OutputHandle,
253        info: Option<&OutputInfo>,
254        primary: Option<OutputHandle>,
255        active: Option<OutputHandle>,
256    ) -> bool {
257        match self {
258            Self::All => true,
259            Self::Primary => primary == Some(handle),
260            Self::Active => active == Some(handle),
261            Self::Handle(h) => *h == handle,
262            Self::Named(name) => info.is_some_and(|i| i.name() == Some(name.as_str())),
263            Self::Filter(predicate) => info.is_some_and(|i| predicate(i)),
264            Self::Not(selector) => !selector.matches(handle, info, primary, active),
265            Self::Or(selectors) => selectors
266                .iter()
267                .any(|s| s.matches(handle, info, primary, active)),
268        }
269    }
270}
271
272impl Debug for Output {
273    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
274        match self {
275            Self::All => write!(f, "Output::All"),
276            Self::Primary => write!(f, "Output::Primary"),
277            Self::Active => write!(f, "Output::Active"),
278            Self::Handle(h) => write!(f, "Output::Handle({:?})", h),
279            Self::Named(name) => write!(f, "Output::Named({:?})", name),
280            Self::Filter(_) => write!(f, "Output::Filter(<fn>)"),
281            Self::Not(selector) => write!(f, "Output::Not({:?})", selector),
282            Self::Or(selectors) => write!(f, "Output::Or({:?})", selectors),
283        }
284    }
285}
286
287/// Combined surface and output selector for precise targeting
288///
289/// Created by combining `Surface` and `Output` selectors:
290///
291/// ```ignore
292/// Surface::named("bar").on(Output::primary())
293/// ```
294///
295/// Or implicitly from either selector:
296///
297/// ```ignore
298/// shell.select(Surface::named("bar"))  // All outputs
299/// shell.select(Output::primary())      // All surfaces
300/// ```
301#[derive(Clone, Debug)]
302pub struct Selector {
303    pub surface: Surface,
304    pub output: Output,
305}
306
307impl Selector {
308    /// Creates a selector matching all surfaces on all outputs
309    pub fn all() -> Self {
310        Self {
311            surface: Surface::All,
312            output: Output::All,
313        }
314    }
315
316    pub(crate) fn matches(
317        &self,
318        surface_info: &SurfaceInfo,
319        output_info: Option<&OutputInfo>,
320        primary: Option<OutputHandle>,
321        active: Option<OutputHandle>,
322    ) -> bool {
323        self.surface.matches(surface_info)
324            && self
325                .output
326                .matches(surface_info.output, output_info, primary, active)
327    }
328}
329
330impl From<Surface> for Selector {
331    fn from(surface: Surface) -> Self {
332        Self {
333            surface,
334            output: Output::All,
335        }
336    }
337}
338
339impl From<Output> for Selector {
340    fn from(output: Output) -> Self {
341        Self {
342            surface: Surface::All,
343            output,
344        }
345    }
346}