relm4_components/
web_image.rs

1//! Reusable and easily configurable component for loading images from the web.
2
3use std::collections::VecDeque;
4use std::fmt::Debug;
5
6use relm4::gtk::prelude::{BoxExt, Cast, WidgetExt};
7use relm4::{Component, ComponentParts, ComponentSender, gtk};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10/// Reusable component for loading images from the web.
11pub struct WebImage {
12    current_id: usize,
13    current_widget: gtk::Widget,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17/// Load or unload a web image.
18pub enum WebImageMsg {
19    /// Load an image from an url.
20    LoadImage(String),
21    /// Unload the current image.
22    Unload,
23}
24
25impl Component for WebImage {
26    type CommandOutput = Option<(usize, VecDeque<u8>)>;
27    type Input = WebImageMsg;
28    type Output = ();
29    type Init = String;
30    type Root = gtk::Box;
31    type Widgets = ();
32
33    fn init_root() -> Self::Root {
34        gtk::Box::default()
35    }
36
37    fn init(
38        url: Self::Init,
39        root: Self::Root,
40        sender: ComponentSender<Self>,
41    ) -> ComponentParts<Self> {
42        let widget = gtk::Box::default();
43        root.append(&widget);
44        let current_widget = Self::set_spinner(&root, widget.upcast_ref());
45
46        let model = Self {
47            current_id: 0,
48            current_widget,
49        };
50
51        model.load_image(&sender, url);
52
53        ComponentParts { model, widgets: () }
54    }
55
56    fn update(&mut self, input: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
57        self.current_widget = Self::set_spinner(root, &self.current_widget);
58        self.current_id = self.current_id.wrapping_add(1);
59
60        match input {
61            WebImageMsg::LoadImage(url) => {
62                self.load_image(&sender, url);
63            }
64            WebImageMsg::Unload => (),
65        }
66    }
67
68    fn update_cmd(
69        &mut self,
70        message: Self::CommandOutput,
71        sender: ComponentSender<Self>,
72        root: &Self::Root,
73    ) {
74        if let Some((id, data)) = message
75            && id == self.current_id
76            && let Some(img) = Self::generate_image(data)
77        {
78            self.current_widget = Self::set_image(root, &self.current_widget, &img);
79            sender.output(()).ok();
80        }
81    }
82}
83
84impl WebImage {
85    #[must_use]
86    fn set_spinner(root: &<Self as Component>::Root, widget: &gtk::Widget) -> gtk::Widget {
87        root.remove(widget);
88        relm4::view! {
89            #[local_ref]
90            root -> gtk::Box {
91                set_halign: gtk::Align::Center,
92                set_valign: gtk::Align::Center,
93
94                #[name(spinner)]
95                gtk::Spinner {
96                    start: (),
97                    set_hexpand: true,
98                    set_vexpand: true,
99                }
100            }
101        }
102        spinner.upcast()
103    }
104
105    #[must_use]
106    fn set_image(
107        root: &<Self as Component>::Root,
108        widget: &gtk::Widget,
109        img: &gtk::Image,
110    ) -> gtk::Widget {
111        root.remove(widget);
112        relm4::view! {
113            #[local_ref]
114            root -> gtk::Box {
115                set_halign: gtk::Align::Fill,
116                set_valign: gtk::Align::Fill,
117
118                #[local_ref]
119                img -> gtk::Image {
120                    set_hexpand: true,
121                    set_vexpand: true,
122                }
123            }
124        }
125        img.clone().upcast()
126    }
127
128    fn load_image(&self, sender: &ComponentSender<Self>, url: String) {
129        sender.oneshot_command(Self::get_img_data(self.current_id, url));
130    }
131
132    fn generate_image(data: VecDeque<u8>) -> Option<gtk::Image> {
133        let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_read(data).ok()?;
134        let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
135        Some(gtk::Image::from_paintable(Some(&texture)))
136    }
137
138    async fn get_img_data(id: usize, url: String) -> Option<(usize, VecDeque<u8>)> {
139        let response = reqwest::get(url).await.ok()?;
140        let bytes = response.bytes().await.ok()?;
141        Some((id, bytes.into_iter().collect()))
142    }
143}