tab_game/
tab_game.rs

1use std::time::Duration;
2
3use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, WidgetExt};
4use relm4::{
5    Component, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SharedState,
6    factory::{DynamicIndex, FactoryComponent, FactorySender, FactoryVecDeque},
7};
8
9#[derive(Default)]
10enum GameState {
11    #[default]
12    Start,
13    Countdown(u8),
14    Running,
15    Guessing,
16    End(bool),
17}
18
19static GAME_STATE: SharedState<GameState> = SharedState::new();
20
21#[derive(Debug)]
22struct GamePage {
23    id: u8,
24}
25
26#[derive(Debug)]
27enum CounterMsg {
28    Update,
29}
30
31#[derive(Debug)]
32enum CounterOutput {
33    StartGame(DynamicIndex),
34    SelectedGuess(DynamicIndex),
35}
36
37#[relm4::factory]
38impl FactoryComponent for GamePage {
39    type Init = u8;
40    type Input = CounterMsg;
41    type Output = CounterOutput;
42    type CommandOutput = ();
43    type ParentWidget = adw::TabView;
44
45    view! {
46        #[root]
47        root = gtk::Box {
48            set_orientation: gtk::Orientation::Horizontal,
49            set_spacing: 10,
50
51            gtk::CenterBox {
52                set_hexpand: true,
53                set_vexpand: true,
54
55                #[wrap(Some)]
56                set_center_widget = match *state {
57                    GameState::Countdown(value) => {
58                        gtk::Label {
59                            set_valign: gtk::Align::Center,
60
61                            #[watch]
62                            set_label: &value.to_string(),
63                        }
64                    }
65                    GameState::Running => {
66                        gtk::Label {
67                            set_valign: gtk::Align::Center,
68                            set_label: "???",
69                        }
70                    }
71                    GameState::Start => {
72                        gtk::Box {
73                            set_orientation: gtk::Orientation::Vertical,
74                            set_valign: gtk::Align::Center,
75                            set_margin_all: 10,
76                            set_spacing: 10,
77
78                            gtk::Label {
79                                set_label: "Can you still find this tab after is was shuffled?",
80                            },
81                            gtk::Button {
82                                set_label: "Start!",
83                                connect_clicked[sender, index] => move |_| {
84                                    sender.output(CounterOutput::StartGame(index.clone())).unwrap()
85                                }
86                            },
87                        }
88                    }
89                    GameState::Guessing => {
90                        gtk::Button {
91                            set_label: "This was my tab!",
92                            set_valign: gtk::Align::Center,
93
94                            connect_clicked[sender, index] => move |_| {
95                                sender.output(CounterOutput::SelectedGuess(index.clone())).unwrap()
96                            }
97                        }
98                    }
99                    GameState::End(won) => {
100                        gtk::Box {
101                            set_orientation: gtk::Orientation::Vertical,
102                            set_valign: gtk::Align::Center,
103                            set_margin_all: 10,
104                            set_spacing: 10,
105
106                            gtk::Label {
107                                #[watch]
108                                set_label: if won {
109                                        "That's correct, you win!"
110                                    } else {
111                                        "You lose, this wasn't your tab..."
112                                    },
113                            },
114                            gtk::Button {
115                                set_label: "Start again",
116                                connect_clicked => move |_| {
117                                    *GAME_STATE.write() = GameState::Start;
118                                }
119                            },
120                        }
121
122                    }
123                }
124            }
125        },
126        #[local_ref]
127        returned_widget -> adw::TabPage {
128            #[watch]
129            set_title: &match *state {
130                GameState::Running | GameState::Guessing => {
131                    "???".to_string()
132                }
133                _ => format!("Tab {}", self.id),
134            },
135            #[watch]
136            set_loading: matches!(*state, GameState::Running),
137        }
138    }
139
140    fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender<Self>) -> Self {
141        GAME_STATE.subscribe(sender.input_sender(), |_| CounterMsg::Update);
142        Self { id: value }
143    }
144
145    fn init_widgets(
146        &mut self,
147        index: &DynamicIndex,
148        root: Self::Root,
149        returned_widget: &adw::TabPage,
150        sender: FactorySender<Self>,
151    ) -> Self::Widgets {
152        let state = GAME_STATE.read();
153        let widgets = view_output!();
154        widgets
155    }
156
157    fn pre_view() {
158        let state = GAME_STATE.read();
159    }
160
161    fn update(&mut self, msg: Self::Input, _sender: FactorySender<Self>) {
162        match msg {
163            CounterMsg::Update => (),
164        }
165    }
166}
167
168struct App {
169    counters: FactoryVecDeque<GamePage>,
170    start_index: Option<DynamicIndex>,
171}
172
173#[derive(Debug)]
174enum AppMsg {
175    SelectedGuess(DynamicIndex),
176    StartGame(DynamicIndex),
177    StopGame,
178}
179
180#[relm4::component]
181impl Component for App {
182    type Init = ();
183    type Input = AppMsg;
184    type Output = ();
185    type CommandOutput = bool;
186
187    view! {
188        adw::Window {
189            set_title: Some("Tab game!"),
190            set_default_size: (400, 200),
191
192            gtk::Box {
193                set_orientation: gtk::Orientation::Vertical,
194                set_spacing: 5,
195
196                adw::HeaderBar {},
197
198                adw::TabBar {
199                    set_view: Some(tab_view),
200                    set_autohide: false,
201                },
202
203                #[local_ref]
204                tab_view -> adw::TabView {
205                    connect_close_page => |_, _| {
206                        gtk::glib::signal::Propagation::Stop
207                    }
208                }
209            }
210        }
211    }
212
213    fn init(
214        _: Self::Init,
215        root: Self::Root,
216        sender: ComponentSender<Self>,
217    ) -> ComponentParts<Self> {
218        let counters = FactoryVecDeque::builder()
219            .launch(adw::TabView::default())
220            .forward(sender.input_sender(), |output| match output {
221                CounterOutput::StartGame(index) => AppMsg::StartGame(index),
222                CounterOutput::SelectedGuess(guess) => AppMsg::SelectedGuess(guess),
223            });
224
225        let mut model = App {
226            counters,
227            start_index: None,
228        };
229
230        let tab_view = model.counters.widget();
231        let widgets = view_output!();
232
233        let mut counters_guard = model.counters.guard();
234        for i in 0..3 {
235            counters_guard.push_back(i);
236        }
237
238        // Explicitly drop the guard,
239        // so that 'model' is no longer borrowed
240        // and can be moved inside ComponentParts
241        counters_guard.drop();
242
243        ComponentParts { model, widgets }
244    }
245
246    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _root: &Self::Root) {
247        match msg {
248            AppMsg::StartGame(index) => {
249                self.start_index = Some(index);
250                sender.command(|sender, _| async move {
251                    for i in (1..4).rev() {
252                        *GAME_STATE.write() = GameState::Countdown(i);
253                        relm4::tokio::time::sleep(Duration::from_millis(1000)).await;
254                    }
255                    *GAME_STATE.write() = GameState::Running;
256                    for _ in 0..20 {
257                        relm4::tokio::time::sleep(Duration::from_millis(500)).await;
258                        sender.send(false).unwrap();
259                    }
260                    relm4::tokio::time::sleep(Duration::from_millis(1000)).await;
261                    sender.send(true).unwrap();
262                });
263            }
264            AppMsg::StopGame => {
265                *GAME_STATE.write() = GameState::Guessing;
266            }
267            AppMsg::SelectedGuess(index) => {
268                *GAME_STATE.write() = GameState::End(index == self.start_index.take().unwrap());
269            }
270        }
271    }
272
273    fn update_cmd(
274        &mut self,
275        msg: Self::CommandOutput,
276        sender: ComponentSender<Self>,
277        _root: &Self::Root,
278    ) {
279        if msg {
280            sender.input(AppMsg::StopGame);
281        } else {
282            let mut counters_guard = self.counters.guard();
283            match rand::random::<u8>() % 3 {
284                0 => {
285                    counters_guard.swap(1, 2);
286                }
287                1 => {
288                    counters_guard.swap(0, 1);
289                }
290                _ => {
291                    let widget = counters_guard.widget();
292                    if !widget.select_next_page() {
293                        widget.select_previous_page();
294                    }
295                }
296            }
297        }
298    }
299}
300
301fn main() {
302    let app = RelmApp::new("relm4.example.tab_game");
303    app.run::<App>(());
304}