message_stream/
message_stream.rs

1// Don't show GTK 4.10 deprecations.
2// We can't replace them without raising the GTK requirement to 4.10.
3#![allow(deprecated)]
4
5use gtk::prelude::*;
6use relm4::{Sender, prelude::*};
7
8struct Dialog {
9    buffer: gtk::EntryBuffer,
10}
11
12#[derive(Debug)]
13enum DialogMsg {
14    Accept,
15    Cancel,
16}
17
18#[relm4::component]
19impl SimpleComponent for Dialog {
20    type Init = ();
21    type Input = DialogMsg;
22    type Output = String;
23    type Widgets = DialogWidgets;
24
25    view! {
26        #[root]
27        dialog = gtk::MessageDialog {
28            set_margin_all: 12,
29            set_modal: true,
30            set_text: Some("Enter a search query"),
31            add_button: ("Search", gtk::ResponseType::Accept),
32            present: (),
33
34            connect_response[sender] => move |dialog, resp| {
35                dialog.set_visible(false);
36                sender.input(if resp == gtk::ResponseType::Accept {
37                    DialogMsg::Accept
38                } else {
39                    DialogMsg::Cancel
40                });
41            }
42        },
43        dialog.content_area() -> gtk::Box {
44            gtk::Entry {
45                set_buffer: &model.buffer,
46            }
47        }
48    }
49
50    fn init(
51        _: Self::Init,
52        root: Self::Root,
53        sender: ComponentSender<Self>,
54    ) -> ComponentParts<Self> {
55        let model = Dialog {
56            buffer: gtk::EntryBuffer::default(),
57        };
58        let widgets = view_output!();
59
60        ComponentParts { model, widgets }
61    }
62
63    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
64        match msg {
65            DialogMsg::Accept => {
66                sender.output(self.buffer.text().into()).unwrap();
67            }
68            DialogMsg::Cancel => {
69                sender.output(String::default()).unwrap();
70            }
71        }
72    }
73
74    fn shutdown(&mut self, _widgets: &mut Self::Widgets, _output: Sender<Self::Output>) {
75        println!("Dialog shutdown");
76    }
77}
78
79#[derive(Debug)]
80enum AppMsg {
81    StartSearch,
82}
83
84struct App {
85    result: Option<String>,
86    searching: bool,
87}
88
89#[relm4::component]
90impl Component for App {
91    type Init = ();
92    type Input = AppMsg;
93    type Output = ();
94    type Widgets = AppWidgets;
95    type CommandOutput = Option<String>;
96
97    view! {
98        main_window = gtk::ApplicationWindow {
99            set_default_size: (300, 100),
100
101            gtk::Box {
102                set_orientation: gtk::Orientation::Vertical,
103                set_margin_all: 12,
104                set_spacing: 12,
105
106                if let Some(result) = &model.result {
107                    gtk::LinkButton {
108                        set_label: "Your search result",
109                        #[watch]
110                        set_uri: result,
111                    }
112                } else {
113                    gtk::Label {
114                        set_label: "Click the button to start a web-search"
115                    }
116                },
117                gtk::Button {
118                    set_label: "Start search",
119                    connect_clicked => AppMsg::StartSearch,
120                    #[watch]
121                    set_sensitive: !model.searching,
122                }
123            }
124        }
125    }
126
127    fn init(
128        _: Self::Init,
129        root: Self::Root,
130        _sender: ComponentSender<Self>,
131    ) -> ComponentParts<Self> {
132        let model = App {
133            result: None,
134            searching: false,
135        };
136        let widgets = view_output!();
137
138        ComponentParts { model, widgets }
139    }
140
141    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
142        match msg {
143            AppMsg::StartSearch => {
144                self.searching = true;
145
146                let stream = Dialog::builder()
147                    .transient_for(root)
148                    .launch(())
149                    .into_stream();
150                sender.oneshot_command(async move {
151                    // Use the component as stream
152                    let result = stream.recv_one().await;
153
154                    if let Some(search) = result {
155                        let response =
156                            reqwest::get(format!("https://duckduckgo.com/lite/?q={search}"))
157                                .await
158                                .unwrap();
159                        let response_text = response.text().await.unwrap();
160
161                        // Extract the url of the first search result.
162                        if let Some(url) = response_text.split("<a rel=\"nofollow\" href=\"").nth(1)
163                        {
164                            let url = url.split('\"').next().unwrap().replace("amp;", "");
165                            Some(format!("https:{url}"))
166                        } else {
167                            None
168                        }
169                    } else {
170                        None
171                    }
172                });
173            }
174        }
175    }
176
177    fn update_cmd(
178        &mut self,
179        message: Self::CommandOutput,
180        _sender: ComponentSender<Self>,
181        _root: &Self::Root,
182    ) {
183        self.searching = false;
184        self.result = message;
185    }
186}
187
188fn main() {
189    let app = RelmApp::new("relm4.example.message_stream");
190    app.run::<App>(());
191}