settings_list/
settings_list.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MIT or Apache-2.0
3
4//! A component which allows the caller to define what options ae in its list.
5//!
6//! On init of the view, an output is sent to the caller to request to load widgets.
7//!
8//! Clicking a button will open the webpage to that option.
9//!
10//! Clicking the clear button will clear the list box and send a command to the
11//! background that waits 2 seconds before issuing a reload command back to the
12//! component, which forwards the reload command back to the caller of the
13//! component, which then issues to reload the widgets again.
14
15use gtk::prelude::*;
16use relm4::*;
17
18fn main() {
19    gtk::Application::builder()
20        .application_id("org.relm4.SettingsListExample")
21        .launch(|_app, window| {
22            // Initialize a component's root widget
23            let mut component = App::builder()
24                // Attach the root widget to the given window.
25                .attach_to(&window)
26                // Start the component service with an initial parameter
27                .launch("Settings List Demo".into())
28                // Attach the returned receiver's messages to this closure.
29                .connect_receiver(move |sender, message| match message {
30                    Output::Clicked(id) => {
31                        eprintln!("ID {id} Clicked");
32
33                        match id {
34                            0 => xdg_open("https://github.com/Relm4/Relm4".into()),
35                            1 => xdg_open("https://docs.rs/relm4/".into()),
36                            2 => {
37                                sender.send(Input::Clear).unwrap();
38                            }
39                            _ => (),
40                        }
41                    }
42
43                    Output::Reload => {
44                        sender
45                            .send(Input::AddSetting {
46                                description: "Browse GitHub Repository".into(),
47                                button: "GitHub".into(),
48                                id: 0,
49                            })
50                            .unwrap();
51
52                        sender
53                            .send(Input::AddSetting {
54                                description: "Browse Documentation".into(),
55                                button: "Docs".into(),
56                                id: 1,
57                            })
58                            .unwrap();
59
60                        sender
61                            .send(Input::AddSetting {
62                                description: "Clear List".into(),
63                                button: "Clear".into(),
64                                id: 2,
65                            })
66                            .unwrap();
67                    }
68                });
69
70            // Keep runtime alive after the component is dropped
71            component.detach_runtime();
72
73            println!("parent is {:?}", component.widget().toplevel_window());
74        });
75}
76
77#[derive(Default)]
78pub struct App {
79    pub options: Vec<(String, String, u32)>,
80}
81
82pub struct Widgets {
83    pub list: gtk::ListBox,
84    pub options: Vec<gtk::Box>,
85    pub button_sg: gtk::SizeGroup,
86}
87
88#[derive(Debug)]
89pub enum Input {
90    AddSetting {
91        description: String,
92        button: String,
93        id: u32,
94    },
95    Clear,
96    Reload,
97}
98
99#[derive(Debug)]
100pub enum Output {
101    Clicked(u32),
102    Reload,
103}
104
105#[derive(Debug)]
106pub enum CmdOut {
107    Reload,
108}
109
110impl Component for App {
111    type Init = String;
112    type Input = Input;
113    type Output = Output;
114    type CommandOutput = CmdOut;
115    type Widgets = Widgets;
116    type Root = gtk::Box;
117
118    fn init_root() -> Self::Root {
119        gtk::Box::builder()
120            .halign(gtk::Align::Center)
121            .hexpand(true)
122            .orientation(gtk::Orientation::Vertical)
123            .build()
124    }
125
126    fn init(
127        title: Self::Init,
128        root: Self::Root,
129        sender: ComponentSender<Self>,
130    ) -> ComponentParts<Self> {
131        // Request the caller to reload its options.
132        sender.output(Output::Reload).unwrap();
133
134        let label = gtk::Label::builder().label(title).margin_top(24).build();
135
136        let list = gtk::ListBox::builder()
137            .halign(gtk::Align::Center)
138            .margin_bottom(24)
139            .margin_top(24)
140            .selection_mode(gtk::SelectionMode::None)
141            .build();
142
143        root.append(&label);
144        root.append(&list);
145
146        ComponentParts {
147            model: App::default(),
148            widgets: Widgets {
149                list,
150                button_sg: gtk::SizeGroup::new(gtk::SizeGroupMode::Both),
151                options: Default::default(),
152            },
153        }
154    }
155
156    fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _root: &Self::Root) {
157        match message {
158            Input::AddSetting {
159                description,
160                button,
161                id,
162            } => {
163                self.options.push((description, button, id));
164            }
165
166            Input::Clear => {
167                self.options.clear();
168
169                // Perform this async operation.
170                sender.oneshot_command(async move {
171                    tokio::time::sleep(std::time::Duration::from_secs(2)).await;
172                    CmdOut::Reload
173                });
174            }
175
176            Input::Reload => {
177                sender.output(Output::Reload).unwrap();
178            }
179        }
180    }
181
182    fn update_cmd(
183        &mut self,
184        message: Self::CommandOutput,
185        sender: ComponentSender<Self>,
186        _root: &Self::Root,
187    ) {
188        match message {
189            CmdOut::Reload => {
190                sender.output(Output::Reload).unwrap();
191            }
192        }
193    }
194
195    fn update_view(&self, widgets: &mut Self::Widgets, sender: ComponentSender<Self>) {
196        if self.options.is_empty() && !widgets.options.is_empty() {
197            widgets.list.remove_all();
198        } else if self.options.len() != widgets.options.len() {
199            if let Some((description, button_label, id)) = self.options.last() {
200                let id = *id;
201                relm4::view! {
202                    widget = gtk::Box {
203                        set_orientation: gtk::Orientation::Horizontal,
204                        set_margin_start: 20,
205                        set_margin_end: 20,
206                        set_margin_top: 8,
207                        set_margin_bottom: 8,
208                        set_spacing: 24,
209
210                        append = &gtk::Label {
211                            set_label: description,
212                            set_halign: gtk::Align::Start,
213                            set_hexpand: true,
214                            set_valign: gtk::Align::Center,
215                            set_ellipsize: gtk::pango::EllipsizeMode::End,
216                        },
217
218                        append: button = &gtk::Button {
219                            set_label: button_label,
220                            set_size_group: &widgets.button_sg,
221
222                            connect_clicked[sender] => move |_| {
223                                sender.output(Output::Clicked(id)).unwrap();
224                            }
225                        }
226                    }
227                }
228
229                widgets.list.append(&widget);
230                widgets.options.push(widget);
231            }
232        }
233    }
234}
235
236fn xdg_open(item: String) {
237    std::thread::spawn(move || {
238        let _ = std::process::Command::new("xdg-open").arg(item).status();
239    });
240}