Skip to main content

ez_tui/utils/
subscription.rs

1use crate::types::cpt_ids::{EzCptId, EzCptIds};
2use crate::types::event::EzMsg;
3use crate::types::ship_ids::EzShipId;
4use crate::{AttrValue, Attribute, EzEvent, EzShipIds};
5use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6use std::fmt::Debug;
7use std::ops::Range;
8
9/// Every subscription target is either for a [`Component`] or a [`Ship`].
10/// We wrap the id of that target inside this enum to know if it refers to a component or a ship.
11#[derive(Debug, PartialEq)]
12pub enum SubId<CID, SID>
13where
14    CID: EzCptIds,
15    SID: EzShipIds,
16{
17    /// Component
18    CPt(EzCptId<CID>),
19    /// Ship
20    Ship(EzShipId<SID>),
21}
22
23/// A subscription describe if a target should be forwarded an event and under which conditions.
24/// The target is described by a [`SubId`] so it can be either a [`Component`] or a [`Ship`].
25/// The event is forwarded only if
26/// * the event matches the [`EventClause`]
27/// * and the target current state matches the [`SubClause`].
28#[derive(Debug)]
29pub struct Subscription<CID, SID, CM>
30where
31    CID: EzCptIds,
32    SID: EzShipIds,
33    CM: EzMsg,
34{
35    target: SubId<CID, SID>,
36    ev: EventClause<CID, CM>,
37    when: SubClause<CID, SID>,
38}
39
40impl<CID, SID, CM> Subscription<CID, SID, CM>
41where
42    CID: EzCptIds,
43    SID: EzShipIds,
44    CM: EzMsg,
45{
46    /// Creates a new subscription.
47    pub fn new(
48        target: SubId<CID, SID>,
49        ev: EventClause<CID, CM>,
50        when: SubClause<CID, SID>,
51    ) -> Self {
52        Self { target, ev, when }
53    }
54
55    pub(crate) fn target(&self) -> &SubId<CID, SID> {
56        &self.target
57    }
58
59    pub(crate) fn event(&self) -> &EventClause<CID, CM> {
60        &self.ev
61    }
62
63    pub(crate) fn forward<QueryFn, IsActiveFn>(
64        &self,
65        ev: &EzEvent<CID, CM>,
66        query_fn: QueryFn,
67        is_active_fn: IsActiveFn,
68    ) -> bool
69    where
70        QueryFn: Fn(&SubId<CID, SID>, Attribute) -> Option<AttrValue>,
71        IsActiveFn: Fn(&SubId<CID, SID>) -> bool,
72    {
73        self.ev.forward(ev) & self.when.forward(query_fn, is_active_fn)
74    }
75}
76
77/// Struct to be more specific on the mouse event clause. Allow checking the range of the event and any modifiers.
78#[derive(Debug, PartialEq, Eq)]
79pub struct MouseEventClause {
80    /// the [`MouseEventKind`] of the event (click, drag, etc.)
81    pub kind: MouseEventKind,
82    /// the [`KeyModifiers`] of the event (ctrl, shift, etc.)
83    pub modifiers: KeyModifiers,
84    /// range of acceptable columns
85    pub column: Range<u16>,
86    /// range of acceptable rows
87    pub row: Range<u16>,
88}
89
90impl MouseEventClause {
91    fn is_in_range(&self, ev: MouseEvent) -> bool {
92        self.column.contains(&ev.column) && self.row.contains(&ev.row)
93    }
94}
95
96#[derive(Debug, PartialEq, Eq)]
97/// [`EventClause`] can be seen as categories of events. When you want a target to listen for events, you don't subscribe to this event explicitly but to its category.
98//FEAT: they are as well copied from tui-realm. After my POC I realize they may not be relevant anymore. Do I keep ?
99pub enum EventClause<CID, CM>
100where
101    CID: EzCptIds,
102    CM: EzMsg,
103{
104    /// Any event
105    Any,
106    /// Any keyboard event
107    Keyboard(KeyEvent),
108    /// Any mouse event
109    Mouse(MouseEventClause),
110    /// Only window resize events
111    WindowResize,
112    /// Only init events
113    Init,
114    /// Only render events
115    Lifecycle,
116    /// Any client event
117    AnyClient,
118    /// A specific client event
119    Client(CM),
120
121    /// A specific event
122    Specific(EzEvent<CID, CM>),
123}
124
125impl<CID, CM> EventClause<CID, CM>
126where
127    CID: EzCptIds,
128    CM: EzMsg,
129{
130    fn forward(&self, ev: &EzEvent<CID, CM>) -> bool {
131        match self {
132            EventClause::<CID, CM>::Any => true,
133            EventClause::<CID, CM>::Keyboard(_) => ev.as_kb().is_some(),
134            EventClause::<CID, CM>::Mouse(m) => ev.as_mouse().is_some_and(|ev| m.is_in_range(*ev)),
135            EventClause::<CID, CM>::WindowResize => ev.as_resize().is_some(),
136            EventClause::<CID, CM>::Init => ev == &EzEvent::Init,
137            EventClause::<CID, CM>::Lifecycle => ev == &EzEvent::Render || ev == &EzEvent::Tick,
138            EventClause::<CID, CM>::AnyClient => ev.as_client().is_some(),
139            EventClause::<CID, CM>::Client(u) => Some(u) == ev.as_client(),
140            EventClause::<CID, CM>::Specific(e) => ev == e,
141        }
142    }
143}
144
145/// [`SubClause`] defines unde what conditions the event should be forwarded to the target based on its state.
146#[derive(Debug, PartialEq)]
147pub enum SubClause<CID, SID>
148where
149    CID: EzCptIds,
150    SID: EzShipIds,
151{
152    /// Always true
153    Always,
154    /// Check if the target has an attribute with a specific value
155    HasAttrValue(SubId<CID, SID>, Attribute, AttrValue),
156    /// Check if the target is mounted/registerd depending if it's a component or a ship
157    IsActive(SubId<CID, SID>),
158    /// Negate another clause
159    Not(Box<SubClause<CID, SID>>),
160    /// Combine two clauses with AND
161    And(Box<SubClause<CID, SID>>, Box<SubClause<CID, SID>>),
162    /// Combine two clauses with OR
163    Or(Box<SubClause<CID, SID>>, Box<SubClause<CID, SID>>),
164    /// Combine two clauses with XOR
165    Xor(Box<SubClause<CID, SID>>, Box<SubClause<CID, SID>>),
166}
167
168impl<CID, SID> SubClause<CID, SID>
169where
170    CID: EzCptIds,
171    SID: EzShipIds,
172{
173    /// Create a new clause that is the inverse of the given clause
174    #[allow(clippy::should_implement_trait)]
175    pub fn not(clause: Self) -> Self {
176        Self::Not(Box::new(clause))
177    }
178
179    /// Create a new clause that is true if the two clauses are true
180    pub fn and(a: Self, b: Self) -> Self {
181        Self::And(Box::new(a), Box::new(b))
182    }
183
184    /// Create a new clause that is true if at least one of the two clauses is true
185    pub fn or(a: Self, b: Self) -> Self {
186        Self::Or(Box::new(a), Box::new(b))
187    }
188
189    /// Create a new clause that is true if one and only one of the two clauses is true
190    pub fn xor(a: Self, b: Self) -> Self {
191        Self::Xor(Box::new(a), Box::new(b))
192    }
193
194    pub(crate) fn forward<QueryFn, IsActiveFn>(
195        &self,
196        query_fn: QueryFn,
197        is_active_fn: IsActiveFn,
198    ) -> bool
199    where
200        QueryFn: Fn(&SubId<CID, SID>, Attribute) -> Option<AttrValue>,
201        IsActiveFn: Fn(&SubId<CID, SID>) -> bool,
202    {
203        self.check_forwarding(query_fn, is_active_fn).0
204    }
205
206    fn check_forwarding<QueryFn, IsActiveFn>(
207        &self,
208        query_fn: QueryFn,
209        is_active_fn: IsActiveFn,
210    ) -> (bool, QueryFn, IsActiveFn)
211    where
212        QueryFn: Fn(&SubId<CID, SID>, Attribute) -> Option<AttrValue>,
213        IsActiveFn: Fn(&SubId<CID, SID>) -> bool,
214    {
215        match self {
216            Self::Always => (true, query_fn, is_active_fn),
217            Self::HasAttrValue(id, query, value) => {
218                let (fwd, query_fn) = Self::has_attribute(id, query, value, query_fn);
219                (fwd, query_fn, is_active_fn)
220            }
221            Self::IsActive(id) => {
222                let (fwd, is_active_fn) = Self::is_mounted(id, is_active_fn);
223                (fwd, query_fn, is_active_fn)
224            }
225            Self::Not(clause) => {
226                let (fwd, query_fn, is_active_fn) = clause.check_forwarding(query_fn, is_active_fn);
227                (!fwd, query_fn, is_active_fn)
228            }
229            Self::And(a, b) => {
230                let (fwd_a, query_fn, is_active_fn) = a.check_forwarding(query_fn, is_active_fn);
231                let (fwd_b, query_fn, is_active_fn) = b.check_forwarding(query_fn, is_active_fn);
232                (fwd_a && fwd_b, query_fn, is_active_fn)
233            }
234            Self::Or(a, b) => {
235                let (fwd_a, query_fn, is_active_fn) = a.check_forwarding(query_fn, is_active_fn);
236                let (fwd_b, query_fn, is_active_fn) = b.check_forwarding(query_fn, is_active_fn);
237                (fwd_a || fwd_b, query_fn, is_active_fn)
238            }
239            Self::Xor(a, b) => {
240                let (fwd_a, query_fn, is_active_fn) = a.check_forwarding(query_fn, is_active_fn);
241                let (fwd_b, query_fn, is_active_fn) = b.check_forwarding(query_fn, is_active_fn);
242                (fwd_a ^ fwd_b, query_fn, is_active_fn)
243            }
244        }
245    }
246
247    fn has_attribute<QueryFn>(
248        id: &SubId<CID, SID>,
249        query: &Attribute,
250        value: &AttrValue,
251        query_fn: QueryFn,
252    ) -> (bool, QueryFn)
253    where
254        QueryFn: Fn(&SubId<CID, SID>, Attribute) -> Option<AttrValue>,
255    {
256        (
257            match query_fn(id, *query) {
258                None => false,
259                Some(v) => *value == v,
260            },
261            query_fn,
262        )
263    }
264
265    fn is_mounted<IsActiveFn>(id: &SubId<CID, SID>, mounted_fn: IsActiveFn) -> (bool, IsActiveFn)
266    where
267        IsActiveFn: Fn(&SubId<CID, SID>) -> bool,
268    {
269        (mounted_fn(id), mounted_fn)
270    }
271}