relm4_components/
simple_combo_box.rs

1//! A wrapper around [`gtk::ComboBoxText`] that makes it easier to use
2//! from regular Rust code.
3
4use std::fmt::Debug;
5
6use relm4::gtk::prelude::{ComboBoxExt, ComboBoxExtManual};
7use relm4::{Component, ComponentParts};
8use relm4::{ComponentSender, gtk};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11/// A simple wrapper around [`gtk::ComboBox`].
12///
13/// This can be used with enums, [`String`]s or any custom type you want.
14/// The only requirement is that the inner type implements [`ToString`] and [`Debug`].
15///
16/// To get notified when the selection changed, you can use
17/// [`Connector::forward()`](relm4::component::Connector::forward())
18/// after launching the component.
19pub struct SimpleComboBox<E: ToString> {
20    /// The variants that can be selected.
21    pub variants: Vec<E>,
22    /// The index of the active element or [`None`] is nothing is selected.
23    pub active_index: Option<usize>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27/// The message type of [`SimpleComboBox`].
28pub enum SimpleComboBoxMsg<E: ToString> {
29    /// Overwrite the current values.
30    UpdateData(SimpleComboBox<E>),
31    /// Set the index of the active element.
32    SetActiveIdx(usize),
33    #[doc(hidden)]
34    UpdateIndex(usize),
35}
36
37impl<E> Component for SimpleComboBox<E>
38where
39    E: ToString + 'static + Debug,
40{
41    type CommandOutput = ();
42    type Input = SimpleComboBoxMsg<E>;
43    type Output = usize;
44    type Init = Self;
45    type Root = gtk::ComboBoxText;
46    type Widgets = gtk::ComboBoxText;
47
48    fn init_root() -> Self::Root {
49        gtk::ComboBoxText::default()
50    }
51
52    fn init(
53        model: Self::Init,
54        widgets: Self::Root,
55        sender: ComponentSender<Self>,
56    ) -> ComponentParts<Self> {
57        model.render(&widgets);
58
59        widgets.connect_changed(move |combo_box| {
60            if let Some(active_idx) = combo_box.active() {
61                sender.input(Self::Input::UpdateIndex(active_idx as usize));
62            }
63        });
64
65        ComponentParts { model, widgets }
66    }
67
68    fn update_with_view(
69        &mut self,
70        widgets: &mut Self::Widgets,
71        input: Self::Input,
72        sender: ComponentSender<Self>,
73        _root: &Self::Root,
74    ) {
75        match input {
76            SimpleComboBoxMsg::UpdateIndex(idx) => {
77                // Ignore send errors because the component might
78                // be detached.
79                sender.output(idx).ok();
80                self.active_index = Some(idx);
81            }
82            SimpleComboBoxMsg::SetActiveIdx(idx) => {
83                if idx < self.variants.len() {
84                    self.active_index = Some(idx);
85                    widgets.set_active(u32::try_from(idx).ok());
86                }
87            }
88            SimpleComboBoxMsg::UpdateData(data) => {
89                *self = data;
90                self.render(widgets);
91            }
92        }
93    }
94}
95
96impl<E> SimpleComboBox<E>
97where
98    E: ToString,
99{
100    fn render(&self, combo_box: &gtk::ComboBoxText) {
101        combo_box.remove_all();
102
103        for (idx, e) in self.variants.iter().enumerate() {
104            combo_box.insert_text(idx as i32, &e.to_string());
105        }
106
107        combo_box.set_active(self.active_index.and_then(|val| u32::try_from(val).ok()));
108    }
109
110    /// Return the value of the currently selected element or [`None`] if nothing is selected.
111    #[must_use]
112    pub fn get_active_elem(&self) -> Option<&E> {
113        self.active_index.and_then(|idx| self.variants.get(idx))
114    }
115}