Skip to main content

ez_tui/core/
port.rs

1use crate::types::args::EzArgs;
2use crate::types::event::EzMsg;
3use crate::types::ship_ids::{EzShipId, EzShipIds};
4use crate::types::state::EzState;
5use crate::{AttrValue, Attribute, BoxedShip, Error, EzCptIds, EzEvent, Result, Sender, State};
6use displaydoc::Display;
7use hashlink::LinkedHashMap;
8use std::fmt::Debug;
9use tracing::warn;
10
11/// A ship is a struct that can be subscribed to some events; execute some - probably async - code; and emit events.
12/// It is used if your app is dependent on some api calls, or other IO operations, they'll certainly have to be done done via a Ship.
13///
14//FEAT: when unregistering follow same principle as View
15/// # Examples
16/// ```
17//DOC : Add a basic demo with ship and include it here
18/// ```
19pub trait Ship<CID, CA, CS, CM>: Debug
20where
21    CID: EzCptIds,
22    CA: EzArgs,
23    CS: EzState,
24    CM: EzMsg,
25{
26    /// Method to implement to handle events.
27    /// The ship will receive only events it is subscribed to.
28    #[allow(unused_variables)]
29    fn on_event(
30        &mut self,
31        event: EzEvent<CID, CM>,
32        state: &mut State<CS>,
33    ) -> Vec<EzEvent<CID, CM>> {
34        vec![]
35    }
36
37    /// Method called on first tick.
38    #[allow(unused_variables)]
39    fn init(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
40        vec![]
41    }
42
43    /// Method called on every tick. You should implement it only if you need to save a reference to the state or if your business logic needs to execute some code on a regular basis.
44    #[allow(unused_variables)]
45    fn tick(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
46        vec![]
47    }
48
49    /// The method called when the ship is mounted to save the [`Sender`] channel
50    fn set_tx(&mut self, msg_tx: Sender<EzEvent<CID, CM>>);
51}
52
53/// The errors that could happen when using the [`Port`]
54#[derive(Debug, Display, Eq, PartialEq, Clone, PartialOrd)]
55pub enum PortError {
56    /// ship already registered
57    ShipAlreadyRegistered,
58    /// ship not found
59    ShipNotFound,
60}
61
62/// The port is the struct that holds all your ships. Like the [`View`], you'll only have one port in your app but as many ships as you want.
63/// Again like the [`View`], the port is the orchestrator of your ships. It will send the forward to/from the ships and your app.
64#[derive(Debug)]
65pub struct Port<CID, SID, CA, CS, CM>
66where
67    SID: EzShipIds,
68    CA: EzArgs,
69    CS: EzState,
70    CM: EzMsg,
71{
72    ships: LinkedHashMap<EzShipId<SID>, BoxedShip<CID, CA, CS, CM>>,
73}
74
75impl<CID, SID, CA, CS, CM> Default for Port<CID, SID, CA, CS, CM>
76where
77    CID: EzCptIds,
78    SID: EzShipIds,
79    CA: EzArgs,
80    CS: EzState,
81    CM: EzMsg,
82{
83    fn default() -> Self {
84        Self {
85            ships: LinkedHashMap::new(),
86        }
87    }
88}
89
90// Public interface
91impl<CID, SID, CA, CS, CM> Port<CID, SID, CA, CS, CM>
92where
93    CID: EzCptIds,
94    SID: EzShipIds,
95    CA: EzArgs,
96    CS: EzState,
97    CM: EzMsg,
98{
99    /// Initialize a new empty port
100    pub fn init(&mut self, tx: &Sender<EzEvent<CID, CM>>) {
101        for (_, ship) in &mut self.ships {
102            ship.set_tx(tx.clone());
103        }
104    }
105    /// Add a new ship to the port
106    ///
107    /// # Errors
108    /// * [`PortError::ShipAlreadyRegistered`] if that ship ID is already present in the port
109    pub fn register(&mut self, id: SID, ship: BoxedShip<CID, CA, CS, CM>) -> Result<()> {
110        self.register_internal(id.into(), ship)
111    }
112
113    /// Remove the ship with given id from the port
114    ///
115    /// # Errors
116    /// * [`PortError::ShipNotFound`] if there is no ship with that id in the port
117    pub fn unregister(&mut self, id: &SID) -> Result<()> {
118        self.unregister_internal(&id.clone().into())
119    }
120
121    /// Check if the ship with given id is registered in the port
122    pub fn registered(&self, id: &SID) -> bool {
123        self.registered_internal(&id.clone().into())
124    }
125
126    /// Query a component for an attribute
127    //FIXME: If we keep Attribute concept, we should integrate it to ships
128    /// # Errors
129    /// None  for now
130    pub fn query_ship(&self, _: &EzShipId<SID>, _: Attribute) -> Result<Option<AttrValue>> {
131        Ok(None)
132    }
133
134    /// Set an attribute to a component
135    //FIXME: If we keep Attribute concept, we should integrate it to ships
136    /// # Errors
137    /// None  for now
138    pub fn set_ship_attr(&mut self, _: &EzShipId<SID>, _: Attribute, _: AttrValue) -> Result<()> {
139        Ok(())
140    }
141}
142// Crate specific interface
143impl<CID, SID, CA, CS, CM> Port<CID, SID, CA, CS, CM>
144where
145    CID: EzCptIds,
146    SID: EzShipIds,
147    CA: EzArgs,
148    CS: EzState,
149    CM: EzMsg,
150{
151    pub(crate) fn forward(
152        &mut self,
153        id: &EzShipId<SID>,
154        event: EzEvent<CID, CM>,
155        state: &mut State<CS>,
156    ) -> Vec<EzEvent<CID, CM>> {
157        match self.ships.get_mut(id) {
158            None => {
159                warn!("Forwarding to missing ship {:?}", id);
160                vec![]
161            }
162            Some(port) => port.on_event(event, state),
163        }
164    }
165    pub(crate) fn init_ships(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
166        self.ships
167            .iter_mut()
168            .flat_map(|(_, s)| s.init(state))
169            .collect()
170    }
171
172    /// Forward the tick calls to all ships
173    pub(crate) fn tick(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
174        self.ships
175            .iter_mut()
176            .flat_map(|(_, ship)| ship.tick(state))
177            .collect()
178    }
179
180    pub(crate) fn register_internal(
181        &mut self,
182        ez_id: EzShipId<SID>,
183        ship: BoxedShip<CID, CA, CS, CM>,
184    ) -> Result<()> {
185        if self.registered_internal(&ez_id) {
186            Err(PortError::ShipAlreadyRegistered.into())
187        } else {
188            self.ships.insert(ez_id, ship);
189            Ok(())
190        }
191    }
192
193    pub(crate) fn unregister_internal(&mut self, ez_id: &EzShipId<SID>) -> Result<()> {
194        if !self.registered_internal(ez_id) {
195            return Err(PortError::ShipNotFound.into());
196        }
197        self.ships.remove(ez_id);
198        Ok(())
199    }
200
201    pub(crate) fn registered_internal(&self, ez_id: &EzShipId<SID>) -> bool {
202        self.ships.contains_key(ez_id)
203    }
204}
205
206impl From<PortError> for Error {
207    fn from(value: PortError) -> Self {
208        Error::Port(value)
209    }
210}