1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! A wrapper around [`gtk::ComboBoxText`] that makes it easier to use
//! from regular Rust code.

use std::fmt::Debug;

use relm4::gtk::prelude::{ComboBoxExt, ComboBoxExtManual};
use relm4::{gtk, ComponentSender};
use relm4::{Component, ComponentParts};

#[derive(Debug, Clone, PartialEq, Eq)]
/// A simple wrapper around [`gtk::ComboBox`].
///
/// This can be used with enums, [`String`]s or any custom type you want.
/// The only requirement is that the inner type implements [`ToString`] and [`Debug`].
///
/// To get notified when the selection changed, you can use
/// [`Connector::forward()`](relm4::component::Connector::forward())
/// after launching the component.
pub struct SimpleComboBox<E: ToString> {
    /// The variants that can be selected.
    pub variants: Vec<E>,
    /// The index of the active element or [`None`] is nothing is selected.
    pub active_index: Option<usize>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// The message type of [`SimpleComboBox`].
pub enum SimpleComboBoxMsg<E: ToString> {
    /// Overwrite the current values.
    UpdateData(SimpleComboBox<E>),
    /// Set the index of the active element.
    SetActiveIdx(usize),
    #[doc(hidden)]
    UpdateIndex(usize),
}

impl<E> Component for SimpleComboBox<E>
where
    E: ToString + 'static + Debug,
{
    type CommandOutput = ();
    type Input = SimpleComboBoxMsg<E>;
    type Output = usize;
    type Init = Self;
    type Root = gtk::ComboBoxText;
    type Widgets = gtk::ComboBoxText;

    fn init_root() -> Self::Root {
        gtk::ComboBoxText::default()
    }

    fn init(
        model: Self::Init,
        root: &Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let widgets = root.clone();

        model.render(&widgets);

        widgets.connect_changed(move |combo_box| {
            if let Some(active_idx) = combo_box.active() {
                sender.input(Self::Input::UpdateIndex(active_idx as usize));
            }
        });

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        input: Self::Input,
        sender: ComponentSender<Self>,
        _root: &Self::Root,
    ) {
        match input {
            SimpleComboBoxMsg::UpdateIndex(idx) => {
                // Ignore send errors because the component might
                // be detached.
                sender.output(idx).ok();
                self.active_index = Some(idx);
            }
            SimpleComboBoxMsg::SetActiveIdx(idx) => {
                if idx < self.variants.len() {
                    self.active_index = Some(idx);
                    widgets.set_active(u32::try_from(idx).ok());
                }
            }
            SimpleComboBoxMsg::UpdateData(data) => {
                *self = data;
                self.render(widgets);
            }
        }
    }
}

impl<E> SimpleComboBox<E>
where
    E: ToString,
{
    fn render(&self, combo_box: &gtk::ComboBoxText) {
        combo_box.remove_all();

        for (idx, e) in self.variants.iter().enumerate() {
            combo_box.insert_text(idx as i32, &e.to_string());
        }

        combo_box.set_active(self.active_index.and_then(|val| u32::try_from(val).ok()));
    }

    /// Return the value of the currently selected element or [`None`] if nothing is selected.
    #[must_use]
    pub fn get_active_elem(&self) -> Option<&E> {
        self.active_index.map(|idx| &self.variants[idx])
    }
}