Search code examples
rustgtkgtk3

Let GtkComboBox entries be Pixbuf or String


I am trying to implement a dropdown-menu where each entry is either text or an image. As it seems, each column in the underlying model has one and only one type. Is it still possible to allow entries to be text or image? My idea is to use two columns where one is empty and a matching renderer. I just do not know how.

This question's answer shows how to add two renderers to one column for a TreeStore in kind of a different context. While I am using a ListStore I thought I could solve it the same way where one of the two renderers renders something emtpy. But for the ComboBox, I run into the problem that the width of the non-extended ComboBox just depends on the first renderer (result of the second one flows out) and the first renderer still creates a lot of spacing even for empty content.

Some sources implied that it could be a good idea to implement my own Renderer which dynamically uses either a Renderer for a Pixbuf or for text. My problem here is that I am using Rust with gtk-rs. While there are some examples to be found about implementing your own renderers in general, the gtk-rs documentation seems not to have such documentation. Due to conceptual differences (gtk-inheritance modelled by Traits etc.) it is not straightforwards to transfer examples to Rust and having not so much experience with this language and gtk I must admit that I do not know where to start for this case.

Any help and/or information about into which direction to proceed is greatly appreciated!


Solution

  • It seems like the best option is to implement a custom type to use as model column together with a custom renderer. The custom type holds an optional String and an optional Pixbuf where one of those two parameters should be set to some value. I did not use an enum to ensure compatibility with glib-types (maybe it would also be possible this way but I am unsure). The custom renderer holds renderers for Strings and Pixbufs and can dynamically choose which one to use depending on the given custom type.

    Basically, besides of storage in the custom type the only functionality to be added is providing functions to render a cell and to provide an entry's preferred size. In those cases the content of the custom type is used to decide from which of the already given renderers the behaviour regarding rendering- and size-request-functionality should be copied.

    Implementing the custom type and renderer is a bit challenging as the documentation of gtk-rs is not the best especially regarding using subclasses of renderers for instance. My code is based on the examples for subclasses and a custom model from the gtk-rs example repo. Due to a lack of experience with Rust and gtk on my side, there may be a lot of parts which could be solved more elegant and consistent, but I'd like to share my solution in case anyone having a similar problem is searching for a direction on how to proceed:

    #[macro_use]
    extern crate glib;
    extern crate gdk_pixbuf;
    extern crate gio;
    extern crate gtk;
    
    use gio::prelude::*;
    use gtk::prelude::*;
    
    use custom_glib_string_or_pixbuf::StringOrPixbuf;
    use custom_glib_string_or_pixbuf_renderer::StringOrPixbufRenderer;
    use gdk_pixbuf::Pixbuf;
    
    // Content type
    mod custom_glib_string_or_pixbuf {
        use gdk_pixbuf::Pixbuf;
        use gio::prelude::*;
        use glib::subclass;
        use glib::subclass::prelude::*;
        use glib::translate::*;
    
        mod internal {
            use super::*;
            use std::cell::RefCell;
    
            pub struct StringOrPixbuf {
                string: RefCell<Option<String>>,
                pixbuf: RefCell<Option<Pixbuf>>,
            }
    
            static PROPERTIES: [subclass::Property; 2] = [
                subclass::Property("string", |name| {
                    glib::ParamSpec::string(name, "String", "String", None, glib::ParamFlags::READWRITE)
                }),
                subclass::Property("pixbuf", |name| {
                    glib::ParamSpec::object(
                        name,
                        "Pixbuf",
                        "Pixbuf",
                        Pixbuf::static_type(),
                        glib::ParamFlags::READWRITE,
                    )
                }),
            ];
    
            impl ObjectSubclass for StringOrPixbuf {
                const NAME: &'static str = "StringOrPixbuf";
                type ParentType = glib::Object;
                type Instance = subclass::simple::InstanceStruct<Self>;
                type Class = subclass::simple::ClassStruct<Self>;
    
                glib_object_subclass!();
    
                fn class_init(class: &mut Self::Class) {
                    class.install_properties(&PROPERTIES);
                }
    
                fn new() -> Self {
                    Self {
                        string: RefCell::new(None),
                        pixbuf: RefCell::new(None),
                    }
                }
            }
    
            impl ObjectImpl for StringOrPixbuf {
                glib_object_impl!();
    
                fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
                    let prop = &PROPERTIES[id];
    
                    match *prop {
                        subclass::Property("string", ..) => {
                            self.string.replace(value.get().unwrap());
                        }
                        subclass::Property("pixbuf", ..) => {
                            self.pixbuf.replace(value.get().unwrap());
                        }
                        _ => panic!("Tried to set unknown property of StringOrPixbuf"),
                    }
                }
    
                fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
                    let prop = &PROPERTIES[id];
    
                    match *prop {
                        subclass::Property("string", ..) => Ok(self.string.borrow().to_value()),
                        subclass::Property("pixbuf", ..) => Ok(self.pixbuf.borrow().to_value()),
                        _ => panic!("Tried to get unknown property of StringOrPixbuf"),
                    }
                }
            }
        }
    
        glib_wrapper! {
            pub struct StringOrPixbuf(Object<subclass::simple::InstanceStruct<internal::StringOrPixbuf>,subclass::simple::ClassStruct<internal::StringOrPixbuf>, StringOrPixbufClass>);
    
            match fn {
                get_type => || internal::StringOrPixbuf::get_type().to_glib(),
            }
        }
    
        impl StringOrPixbuf {
            pub fn new(string: Option<String>, pixbuf: Option<Pixbuf>) -> StringOrPixbuf {
                glib::Object::new(
                    StringOrPixbuf::static_type(),
                    &[("string", &string), ("pixbuf", &pixbuf)],
                )
                .expect("Failed to create StringOrPixbuf instance")
                .downcast()
                .unwrap()
            }
    
            pub fn is_string(&self) -> bool {
                let string_option = self
                    .get_property("string")
                    .unwrap()
                    .get::<String>()
                    .unwrap();
                let pixbuf_option = self
                    .get_property("pixbuf")
                    .unwrap()
                    .get::<Pixbuf>()
                    .unwrap();
    
                if string_option.is_some() == pixbuf_option.is_some() {
                    panic!("Illegal StringOrPixbuf-state")
                } else if let Some(_) = string_option {
                    true
                } else {
                    false
                }
            }
        }
    }
    
    // Renderer
    mod custom_glib_string_or_pixbuf_renderer {
        use custom_glib_string_or_pixbuf::StringOrPixbuf;
        use gdk_pixbuf::Pixbuf;
        use gio::prelude::*;
        use glib::subclass;
        use glib::subclass::prelude::*;
        use glib::translate::*;
        use gtk::prelude::*;
    
        mod internal {
            use super::*;
            use std::cell::RefCell;
    
            pub struct StringOrPixbufRenderer {
                text_renderer: gtk::CellRendererText,
                pixbuf_renderer: gtk::CellRendererPixbuf,
                string_or_pixbuf: RefCell<Option<StringOrPixbuf>>,
            }
    
            static PROPERTIES: [subclass::Property; 1] =
                [subclass::Property("string_or_pixbuf", |name| {
                    glib::ParamSpec::object(
                        name,
                        "string_or_pixbuf",
                        "string_or_pixbuf",
                        StringOrPixbuf::static_type(),
                        glib::ParamFlags::READWRITE,
                    )
                })];
    
            impl ObjectSubclass for StringOrPixbufRenderer {
                const NAME: &'static str = "StringOrPixbufRenderer";
                type ParentType = gtk::CellRenderer;
                type Instance = subclass::simple::InstanceStruct<Self>;
                type Class = subclass::simple::ClassStruct<Self>;
                glib_object_subclass!();
                fn class_init(class: &mut Self::Class) {
                    class.install_properties(&PROPERTIES);
                }
                fn new() -> Self {
                    Self {
                        text_renderer: gtk::CellRendererText::new(),
                        pixbuf_renderer: gtk::CellRendererPixbuf::new(),
                        string_or_pixbuf: RefCell::new(None),
                    }
                }
            }
    
            impl ObjectImpl for StringOrPixbufRenderer {
                glib_object_impl!();
    
                fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
                    let prop = &PROPERTIES[id];
                    match *prop {
                        subclass::Property("string_or_pixbuf", ..) => {
                            self.string_or_pixbuf.replace(value.get().unwrap());
                        }
                        _ => panic!("Tried to set unknown property of StringOrPixbufRenderer"),
                    }
                }
    
                fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
                    let prop = &PROPERTIES[id];
                    match *prop {
                        subclass::Property("string_or_pixbuf", ..) => {
                            Ok(self.string_or_pixbuf.borrow().to_value())
                        }
                        _ => panic!("Tried to get unknown property of StringOrPixbufRenderer"),
                    }
                }
            }
    
            impl gtk::subclass::cell_renderer::CellRendererImpl for StringOrPixbufRenderer {
                fn render<P: IsA<gtk::Widget>>(
                    &self,
                    _renderer: &gtk::CellRenderer,
                    cr: &cairo::Context,
                    widget: &P,
                    background_area: &gdk::Rectangle,
                    cell_area: &gdk::Rectangle,
                    flags: gtk::CellRendererState,
                ) {
                    self.update_renderers();
                    if self.is_content_string() {
                        self.text_renderer
                            .render(cr, widget, background_area, cell_area, flags);
                    } else {
                        self.pixbuf_renderer
                            .render(cr, widget, background_area, cell_area, flags);
                    }
                }
    
                fn get_preferred_width<P: IsA<gtk::Widget>>(
                    &self,
                    _renderer: &gtk::CellRenderer,
                    widget: &P,
                ) -> (i32, i32) {
                    self.update_renderers();
                    if self.is_content_string() {
                        self.text_renderer.get_preferred_width(widget)
                    } else {
                        self.pixbuf_renderer.get_preferred_width(widget)
                    }
                }
    
                fn get_preferred_height<P: IsA<gtk::Widget>>(
                    &self,
                    _renderer: &gtk::CellRenderer,
                    widget: &P,
                ) -> (i32, i32) {
                    self.update_renderers();
                    if self.is_content_string() {
                        self.text_renderer.get_preferred_height(widget)
                    } else {
                        self.pixbuf_renderer.get_preferred_height(widget)
                    }
                }
            }
    
            impl StringOrPixbufRenderer {
                fn is_content_string(&self) -> bool {
                    self.string_or_pixbuf
                        .borrow()
                        .as_ref()
                        .expect("No StringOrPixbuf known to StringOrPixbufRenderer")
                        .is_string()
                }
    
                fn update_renderers(&self) {
                    if self.is_content_string() {
                        self.text_renderer.set_property_text(Some(
                            &self
                                .string_or_pixbuf
                                .borrow()
                                .as_ref()
                                .unwrap()
                                .get_property("string")
                                .unwrap()
                                .get::<String>()
                                .unwrap()
                                .unwrap()[..],
                        ));
                    } else {
                        self.pixbuf_renderer.set_property_pixbuf(Some(
                            &self
                                .string_or_pixbuf
                                .borrow()
                                .as_ref()
                                .unwrap()
                                .get_property("pixbuf")
                                .unwrap()
                                .get::<Pixbuf>()
                                .unwrap()
                                .unwrap(),
                        ));
                    }
                }
            }
        }
    
        glib_wrapper! {
            pub struct StringOrPixbufRenderer(
                Object<subclass::simple::InstanceStruct<internal::StringOrPixbufRenderer>,
                subclass::simple::ClassStruct<internal::StringOrPixbufRenderer>,
                SimpleAppWindowClass>)
                @extends gtk::CellRenderer;
            match fn {
                get_type => || internal::StringOrPixbufRenderer::get_type().to_glib(),
            }
        }
    
        impl StringOrPixbufRenderer {
            pub fn new() -> StringOrPixbufRenderer {
                glib::Object::new(StringOrPixbufRenderer::static_type(), &[])
                    .expect("Failed to create StringOrPixbufRenderer instance")
                    .downcast::<StringOrPixbufRenderer>()
                    .unwrap()
            }
        }
    }
    
    fn main() {
        let application =
            gtk::Application::new(None, Default::default())
                .expect("failed to initialize GTK application");
    
        application.connect_activate(|app| {
            let window = gtk::ApplicationWindow::new(app);
            window.set_title("Mixed Pixbuf and String ComboBox Demo");
            window.set_default_size(350, 70);
    
            let model = gtk::ListStore::new(&[StringOrPixbuf::static_type()]);
            let combo = gtk::ComboBox::with_model(&model);
    
            let row = model.append();
            let image = Pixbuf::from_file("image.png")
                .unwrap()
                .scale_simple(100, 70, gdk_pixbuf::InterpType::Bilinear)
                .unwrap();
            model.set(&row, &[0], &[&StringOrPixbuf::new(None, Some(image))]);
            combo.set_active(Some(0));
    
            let row = model.append();
            model.set(
                &row,
                &[0],
                &[&StringOrPixbuf::new(
                    Some(String::from("Hello World")),
                    None,
                )],
            );
    
            let row = model.append();
            let image = Pixbuf::from_file("another_image.png")
                .unwrap()
                .scale_simple(100, 70, gdk_pixbuf::InterpType::Bilinear)
                .unwrap();
            model.set(&row, &[0], &[&StringOrPixbuf::new(None, Some(image))]);
    
            let renderer = StringOrPixbufRenderer::new();
            combo.pack_start(&renderer, true);
            combo.add_attribute(&renderer, "string_or_pixbuf", 0);
    
            window.add(&combo);
    
            window.show_all();
        });
    
        application.run(&[]);
    }