0
votes

In GTK I am trying to align widgets of different widths in a box such that they are next to each other in a line and flow into the next line if no more space is left. Basically I am trying to get similar results to Qt's Flow Layout as seen here:

Qt Flow Layout Example

(Source: This Qt documentation page)

I need each widget to be as small (width-wise) as possible, even allowing widths of only 20 pixels.

To automatically go to the next line if more space is needed, GTK provides the GtkFlowBox. My problem here is that it appears like the GtkFlowBox aligns all widgets in a grid of a dynamic width (cell-wise). The following image shows that even very wide widgets force widgets directly below it to use the exact same width:

GTK aligning items grid wise

(Mind that each "..." is in its own Label just like each text sequence.)

In the picture the second long label clearly results in the last label getting much more space than needed. The GTK documentation did not really make it clear to me that the GtkFlowBox works like a dynamic grid, but other images in the internet also show a grid-wise alignment everytime.

I am searching for a way where Widgets flow into the next row if there is no space left but all rows are independent from each other in other aspects. Qt appears to achieve this as the last button would contradict the widgets being aligned in a grid. Is there a way achieving what I am trying with GtkFlowBox, or with another existing Layout, or only by manually implementing it? I think that the easiest manual way would be to use horizontal Boxes and dynamically map the widgets to different boxes so that no box is overfull. But clearly this would be drastically less easy and elegant than using an existing Layout.

(Note in case that anyone has a third-party solution written for specific environments: I am working with Rust (Gtk-rs))

2

2 Answers

0
votes

The solution to my problem was implementing my own container. I want to try documenting how to approach this problem and will provide my code here.

The main concept is subclassing a container. This guide gives a good overview using C. The main ideas can easily be transferred to other languages if you inform yourself about subclassing in your language binding. There are two main problems to solve. One is the size-calculation needed for the container. The other one is how the children actually are aligned in the container.

Regarding the size-calculation, the request mode should be set to HeightForWidth as the width influences how many children fit into the rows and therefore also how many rows are needed. A problem here is that while larger widths imply smaller heights vice versa, gtk uses the minimum width to calculate the minimum height. This problem is documented in this post. I solved it by adding a property indicating how many children should be placed in one row at least such that the actual width can be approximated yielding acceptable height requirements. My get_preferred_width assumes that the minimum number of children is placed in each row to calculate the minimum width. A more natural approach would be to set it to the width of the largest child, but this results in a much larger minimum height. get_preferred_height_for_width directly depends on the allocation algorithm that it uses without really allocating size for its children.

I ran into some problems implementing the size allocation regarding minimum and natural sizes. It is not possible to use space remaining when using the minumum height evenly among the children to reach natural size in standard ways as children may flow to other rows at any time and the distribution shall be fair globally and not only per row. I assign the minimum width to each child and add a ratio of the width remaining to reach the natural width which is the same for all children. Due to reflowing, I found no nice algorithm to do this not being simple trial and error. Thus, I just try if the available height is enough for different ratios where I test those ratios down to a sufficiently low resolution. This yields a tradeoff between quality and efficiency. I use binary search to make it somehow efficient.

If you have any specific questions, feel free to ask. As code examples, I want to provide a third party solution in C which user James Westman proposed in my other post. I wanted to provide it as my own code is in Rust. Thus, it could be useful regarding the ideas while for instance the subclassing has a lot of differences to subclassing with gtk in C making it non-trivial just to use this code in other languages. My example also contains a dummy container allowing to test different natural and minumum sizes of the container's children. It works as standalone. Also mind that some functionality is not contained in the latest stable release of gtk-rs, use the newest version by e.g.

[dependencies.gtk]
git = "https://github.com/gtk-rs/gtk"

(Code in another answer as answer size is limited and I cannot share the git repo in the moment)

0
votes

Read the description in another answer first. This code is excluded as the answer length is limited; the code and the text won't fit into one.

#[macro_use]
extern crate glib;
extern crate gdk;
extern crate gio;
extern crate gtk;

use gio::prelude::*;
use glib::subclass;
use glib::subclass::prelude::*;
use glib::translate::*;
use gtk::prelude::*;

mod dummy {
    use super::*;
    glib_wrapper! {
        pub struct GAspectFillFrame(
            Object<subclass::simple::InstanceStruct<internal::GAspectFillFrame>,
            subclass::simple::ClassStruct<internal::GAspectFillFrame>,
            SimpleAppWindowClass>)
            @extends gtk::Bin, gtk::Container, gtk::Widget;

        match fn {
            get_type => || internal::GAspectFillFrame::get_type().to_glib(),
        }
    }

    impl GAspectFillFrame {
        pub fn new() -> GAspectFillFrame {
            glib::Object::new(GAspectFillFrame::static_type(), &[])
                .expect("Failed to create GAspectFillFrame instance")
                .downcast::<GAspectFillFrame>()
                .unwrap()
        }
    }

    mod internal {
        use super::*;
        use gtk::subclass::{bin::BinImpl, container::ContainerImpl, widget::WidgetImpl};
        use gtk::SizeRequestMode;

        pub struct GAspectFillFrame {}

        static PROPERTIES: [subclass::Property; 0] = [];

        impl ObjectSubclass for GAspectFillFrame {
            const NAME: &'static str = "GAspectFillFrame";
            type ParentType = gtk::Bin;
            type Instance = subclass::simple::InstanceStruct<Self>;
            type Class = subclass::simple::ClassStruct<Self>;

            glib_object_subclass!();

            fn class_init(klass: &mut Self::Class) {
                klass.install_properties(&PROPERTIES);
            }

            fn new() -> Self {
                Self {}
            }
        }

        impl ObjectImpl for GAspectFillFrame {
            fn set_property(&self, _obj: &glib::Object, _id: usize, _value: &glib::Value) {
                panic!();
            }

            fn get_property(&self, _obj: &glib::Object, _id: usize) -> Result<glib::Value, ()> {
                panic!();
            }
        }

        impl WidgetImpl for GAspectFillFrame {
            fn get_preferred_width(&self, _widget: &gtk::Widget) -> (i32, i32) {
                (50, 100)
            }

            fn get_preferred_height(&self, _widget: &gtk::Widget) -> (i32, i32) {
                (50, 100)
            }

            fn get_request_mode(&self, _widget: &gtk::Widget) -> gtk::SizeRequestMode {
                SizeRequestMode::ConstantSize
            }
        }

        impl ContainerImpl for GAspectFillFrame {}

        impl BinImpl for GAspectFillFrame {}

        impl GAspectFillFrame {}
    }
}

glib_wrapper! {
    /// A container displaying its children rowwise and dynamically reflowing children to the next row
    /// if there is no space left. In contrast to the `gtk::FlowBox` this container will not act like a grid and instead will
    /// display its rows independent of each other besides of the assignment of the children to the rows.
    ///
    /// The container allows to set the horizontal spacing between children and the vertical spacing between rows.
    ///
    /// While the height of the container is queried for given widths as an increase in the latter decreases the height,
    /// gtk uses the minimal width to calculate the minimal height. This could result in unexpected large heights.
    /// This behaviour can be controlled by influencing the minimal width such that it is reasonably high.
    /// To do so, set the `min-row-children` property. Using it to require a row to be able to contain a minimum of children which is
    /// higher than the default value `1` leads to higher minimum widths and therefore also smaller minimum heights.
    /// Note that the property is used for min width calculations but not actually enforced in size allocation.
    /// It is a soft limit to allow an easier front to back calculation for the allocation.
    /// Requiring a minimal number of children per line would imply new problems, as the minimum width calculation places a constant number
    /// of elements in one line while the real allocation does more calculations allowing a non constant number.
    /// Setting the min number of children as a hard limit could lead to situations where a for the min width calculations e.g. 4 children
    /// are in each row while for the real allocation one row may have some remaining place for one more child than necessary.
    /// In this case, the child would be in this row, the next row requires another child to reach the hard limit which could then overflow
    /// the row if it had a large size.
    /// Note that while the property is not enforced in final allocation, the min space requirements always are sufficient to place
    /// all children with their min width or larger.
    ///
    /// The container expands h-expand children with respect to the remaining space in their row which is evenly
    /// distributed among all expanding children of that row.
    ///
    /// In the case where the natural size requirements of the children cannot be satisfied, the container tries
    /// to set their size such that they get their minimal size and an additional fraction/ratio of the difference between
    /// their minimal and natural size such that this fraction is constant among all children.
    /// Note that this operation is non-trivial as changes in this ratio may lead to children being shifted into
    /// different rows and as rows may have some remaining space if others are full.
    /// The implemented solution implements a tradeoff between a visually nice rescaling behaviour and efficiency.
    pub struct ProperFlowBox(
        Object<subclass::simple::InstanceStruct<internal::ProperFlowBox>,
        subclass::simple::ClassStruct<internal::ProperFlowBox>,
        SimpleAppWindowClass>)
        @extends gtk::Container, gtk::Widget;

    match fn {
        get_type => || internal::ProperFlowBox::get_type().to_glib(),
    }
}

impl ProperFlowBox {
    /// Creates a new instance
    pub fn new() -> ProperFlowBox {
        glib::Object::new(ProperFlowBox::static_type(), &[])
            .expect("Failed to create ProperFlowBox instance")
            .downcast::<ProperFlowBox>()
            .unwrap()
    }

    /// Sets the spacing between two children in a line and between children and the container's borders
    ///
    /// # Arguments
    ///
    /// * `spacing` - Horizontal spacing in pixels
    pub fn set_h_spacing(&self, spacing: i32) {
        self.set_property("h-spacing", &spacing)
            .expect("Error setting h-spacing of ProperFlowBox");
    }

    /// Sets the spacing between two rows and between children and the container's borders
    ///
    /// # Arguments
    ///
    /// * `spacing` - Vertical spacing in pixels
    pub fn set_v_spacing(&self, spacing: i32) {
        self.set_property("v-spacing", &spacing)
            .expect("Error setting v-spacing of ProperFlowBox");
    }

    /// Sets the minimum number of children used to approximate the required width
    ///
    /// The set property is not enforced by the size allocation.
    /// Setting this to higher values may result in less required minimum height of the container.
    ///
    /// # Arguments
    ///
    /// * `min_children` - Minimum number of children per row
    pub fn set_min_row_children(&self, min_children: u32) {
        self.set_property("min-row-children", &min_children)
            .expect("Error setting min-row-children of ProperFlowBox");
    }
}

mod internal {
    use std::cell::RefCell;

    use super::*;
    use gtk::subclass::{
        container::Callback,
        container::ContainerImpl,
        widget::{WidgetImpl, WidgetImplExt},
    };

    const DEFAULT_MIN_ROW_CHILDREN: u32 = 5;

    pub struct ProperFlowBox {
        children: RefCell<Vec<gtk::Widget>>,
        h_spacing: RefCell<i32>,
        v_spacing: RefCell<i32>,
        min_row_children: RefCell<u32>,
    }

    static PROPERTIES: [subclass::Property; 3] = [
        subclass::Property("h-spacing", |h_spacing| {
            glib::ParamSpec::int(
                h_spacing,
                "Horizontal spacing",
                "Space between two children in a row and between children and the container's borders",
                0,
                i32::MAX,
                0,
                glib::ParamFlags::READWRITE,
            )
        }),
        subclass::Property("v-spacing", |v_spacing| {
            glib::ParamSpec::int(
                v_spacing,
                "Vertical spacing",
                "Space between two rows and between rows and the container's borders",
                0,
                i32::MAX,
                0,
                glib::ParamFlags::READWRITE,
            )
        }),
        subclass::Property("min-row-children", |min_row_children| {
            glib::ParamSpec::uint(
                min_row_children,
                "Minimal number of children in one row",
                "Setting this to larger numbers increases the minumum width and decreases the minimum height",
                1,
                u32::MAX,
                DEFAULT_MIN_ROW_CHILDREN,
                glib::ParamFlags::READWRITE,
            )
        }),
    ];

    impl ObjectSubclass for ProperFlowBox {
        const NAME: &'static str = "ProperFlowBox";
        type ParentType = gtk::Container;
        type Instance = subclass::simple::InstanceStruct<Self>;
        type Class = subclass::simple::ClassStruct<Self>;

        glib_object_subclass!();

        fn class_init(klass: &mut Self::Class) {
            klass.install_properties(&PROPERTIES);
        }

        fn new() -> Self {
            Self {
                children: RefCell::new(Vec::new()),
                h_spacing: RefCell::new(0),
                v_spacing: RefCell::new(0),
                min_row_children: RefCell::new(DEFAULT_MIN_ROW_CHILDREN),
            }
        }
    }

    impl ObjectImpl for ProperFlowBox {
        fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
            let prop = &PROPERTIES[id];
            match *prop {
                subclass::Property("h-spacing", ..) => {
                    *self.h_spacing.borrow_mut() = value.get_some().unwrap();
                }
                subclass::Property("v-spacing", ..) => {
                    *self.v_spacing.borrow_mut() = value.get_some().unwrap();
                }
                subclass::Property("min-row-children", ..) => {
                    *self.min_row_children.borrow_mut() = value.get_some().unwrap();
                }
                _ => panic!("Tried to set unknown property of ProperFlowBox"),
            }
        }

        fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
            let prop = &PROPERTIES[id];
            match *prop {
                subclass::Property("h-spacing", ..) => Ok(self.h_spacing.borrow().to_value()),
                subclass::Property("v-spacing", ..) => Ok(self.v_spacing.borrow().to_value()),
                subclass::Property("min-row-children", ..) => {
                    Ok(self.min_row_children.borrow().to_value())
                }
                _ => panic!("Tried to get unknown property of ProperFlowBox"),
            }
        }

        fn constructed(&self, obj: &glib::Object) {
            self.parent_constructed(obj);
            obj.downcast_ref::<gtk::Widget>()
                .unwrap()
                .set_has_window(false);
        }
    }

    impl WidgetImpl for ProperFlowBox {
        fn size_allocate(&self, widget: &gtk::Widget, allocation: &gtk::Allocation) {
            self.parent_size_allocate(widget, &allocation);

            // Search for maximal working natural ratio.
            // Sort out most likely cases (enough for 1.0 or not for more than 0.0) and do
            // binary search otherwise.
            if self.check_height_for_natural_ratio(allocation.width, 1.0, false)
                <= allocation.height
            {
                // Do 1.0
                self.check_height_for_natural_ratio(allocation.width, 1.0, true);
            } else if self.check_height_for_natural_ratio(allocation.width, 0.0, false)
                >= allocation.height
            {
                // Do 0.0
                self.check_height_for_natural_ratio(allocation.width, 0.0, true);
            } else {
                // Do binary search
                // A lower stopping eps yields higher quality by finer transitions but less performance.
                // 0.001 should be a reasonable tradeoff as binary search implies that ca. log_2(0.25/0.001) ~= 8 iterations. 

                let mut current_ratio = 0.5;
                let mut current_step_width = 0.25;
                const STOPPING_EPS: f64 = 0.001;
                let mut max_ratio: f64 = 0.0;
                while current_step_width > STOPPING_EPS {
                    let required_height =
                        self.check_height_for_natural_ratio(allocation.width, current_ratio, false);
                    if allocation.height >= required_height {
                        max_ratio = max_ratio.max(current_ratio);
                        current_ratio += current_step_width;
                    } else {
                        current_ratio -= current_step_width;
                    }
                    current_step_width /= 2.0;
                }
                self.check_height_for_natural_ratio(allocation.width, max_ratio, true);
            }
        }

        fn get_request_mode(&self, _widget: &gtk::Widget) -> gtk::SizeRequestMode {
            gtk::SizeRequestMode::HeightForWidth
        }

        fn get_preferred_height(&self, widget: &gtk::Widget) -> (i32, i32) {
            widget.get_preferred_height_for_width(widget.get_preferred_width().0)
        }

        fn get_preferred_width_for_height(&self, widget: &gtk::Widget, _height: i32) -> (i32, i32) {
            widget.get_preferred_width()
        }

        fn get_preferred_width(&self, _widget: &gtk::Widget) -> (i32, i32) {
            // Calculate an approximation of the required width by exactly placing `min_row_children` many
            // children in each row

            let mut min_width = 0;
            let mut natural_width = 0;

            let mut current_min_width = 0;
            let mut current_natural_width = 0;
            for (index, child) in self
                .children
                .borrow()
                .iter()
                .filter(|c| c.is_visible())
                .enumerate()
            {
                if index as u32 % *self.min_row_children.borrow() == 0 {
                    // Begin a new row
                    current_min_width = *self.h_spacing.borrow();
                    current_natural_width = *self.h_spacing.borrow();
                }

                current_min_width += child.get_preferred_width().0 + *self.h_spacing.borrow();
                current_natural_width += child.get_preferred_width().1 + *self.h_spacing.borrow();

                // Max each time for more consistent code as last row may not contain `min_row_children` children
                min_width = min_width.max(current_min_width);
                natural_width = natural_width.max(current_natural_width);
            }

            (min_width, natural_width)
        }

        fn get_preferred_height_for_width(&self, _widget: &gtk::Widget, width: i32) -> (i32, i32) {
            (
                self.check_height_for_natural_ratio(width, 0.0, false),
                self.check_height_for_natural_ratio(width, 1.0, false),
            )
        }
    }

    impl ContainerImpl for ProperFlowBox {
        fn add(&self, container: &gtk::Container, widget: &gtk::Widget) {
            self.children.borrow_mut().push(widget.clone());
            widget.set_parent(container);
            if container.get_visible() {
                container.queue_resize();
            }
        }

        fn remove(&self, container: &gtk::Container, widget: &gtk::Widget) {
            let index = self.children.borrow().iter().position(|c| c == widget);
            if let Some(index) = index {
                self.children.borrow_mut().remove(index);
                widget.unparent();
            } else {
                println!("Tried to remove non-child from ProperFlowBox")
            }

            if container.get_visible() {
                container.queue_resize();
            }
        }

        fn forall(
            &self,
            _container: &gtk::Container,
            _include_internals: bool,
            callback: &Callback,
        ) {
            // Need to deepcopy children as callbacks may also borrow children
            let children = (*self.children.borrow()).clone();
            for child in children.iter() {
                callback.call(child);
            }
        }
    }

    impl ProperFlowBox {
        /// Tries to fit the visible children for the given available width.
        ///
        /// Given the available width, all visible children get their minimum size plus
        /// the fraction defined by `natural_ratio` of the additional size bringing them to
        /// their natural size. The function then returns the required height for the given width.
        /// It is possible to directly call `size_allocate` for the visible children by enabling
        /// it with the corresponding parameter. Real allocating also respects the `h-expand` property
        /// of the children which does not influence the returned height of the function.
        ///
        /// # Arguments
        ///
        /// * `available_width` - Width available to be filled
        /// * `natural_ratio` - Fraction of the additional size to meet the natural size coming from the minimum size.
        ///                     For a ratio `x` the allocated width and height will be `min + x * (max - min) == (1 - x) * min + x * max`
        /// * `allocate` - Call `size-allocate` on visible children if true
        fn check_height_for_natural_ratio(
            &self,
            available_width: i32,
            natural_ratio: f64,
            allocate: bool,
        ) -> i32 {
            // Coordinates of next child
            let mut x = *self.h_spacing.borrow();
            let mut y = *self.v_spacing.borrow();

            let mut line_height = 0;
            let mut number_row_children = 0;
            let mut number_hexpand_row_children = 0;
            let mut row_start_index = 0;
            for (index, child) in self
                .children
                .borrow()
                .iter()
                .enumerate()
                .filter(|(_, c)| c.is_visible())
            {
                let width = ProperFlowBox::get_barycentric_combination(
                    child.get_preferred_width(),
                    natural_ratio,
                );
                let height = ProperFlowBox::get_barycentric_combination(
                    match child.get_request_mode() {
                        gtk::SizeRequestMode::ConstantSize => child.get_preferred_height(),
                        gtk::SizeRequestMode::HeightForWidth
                        | gtk::SizeRequestMode::WidthForHeight => {
                            child.get_preferred_height_for_width(width)
                        }
                        _ => panic!("Unknown size request mode"),
                    },
                    natural_ratio,
                );
                if number_row_children > 0 && x + width + *self.h_spacing.borrow() > available_width
                {
                    // Not enough space in current line => Go to next line
                    // Exception: Current child will be only one in line, then assign as there is no
                    // valid assignment to any line for this child

                    // Allocate finished line
                    if allocate {
                        self.allocate_row(
                            &self.children.borrow()[row_start_index..index],
                            y,
                            line_height,
                            number_hexpand_row_children,
                            available_width - x,
                            natural_ratio,
                        );
                    }

                    // Start next line
                    x = *self.h_spacing.borrow();
                    y += line_height + *self.v_spacing.borrow();
                    line_height = 0;
                    number_row_children = 0;
                    number_hexpand_row_children = 0;
                    row_start_index = index;
                }
                line_height = line_height.max(height);
                x += width + *self.h_spacing.borrow();
                number_row_children += 1;
                if child.get_hexpand() {
                    number_hexpand_row_children += 1;
                }
            }

            // Allocate last line
            if allocate {
                self.allocate_row(
                    &self.children.borrow()[row_start_index..],
                    y,
                    line_height,
                    number_hexpand_row_children,
                    available_width - x,
                    natural_ratio,
                );
            }

            y + line_height + *self.v_spacing.borrow()
        }

        /// Allocates the size for the children in the given slice
        ///
        /// # Arguments
        ///
        /// * `children` - Slice of which the visible children inside form a row
        /// * `y` - y coordinate of the row
        /// * `height` - height of the row
        /// * `number_hexpand_children` - Number of children with h-expand
        /// * `remaining_space` - Unneeded space to be distributed among h-expand children
        /// * `natural_ratio` - Fraction of the additional size to meet the natural size coming from the minimum size.
        ///                     For a ratio `x` the allocated width and height will be `min + x * (max - min) == (1 - x) * min + x * max`
        fn allocate_row(
            &self,
            children: &[gtk::Widget],
            y: i32,
            height: i32,
            number_hexpand_children: i32,
            remaining_space: i32,
            natural_ratio: f64,
        ) {
            let mut x = *self.h_spacing.borrow();
            let additional_width_per_child = if number_hexpand_children > 0 {
                remaining_space / number_hexpand_children
            } else {
                0
            };
            for child in children.iter().filter(|c| c.is_visible()) {
                let mut width = ProperFlowBox::get_barycentric_combination(
                    child.get_preferred_width(),
                    natural_ratio,
                );
                if child.get_hexpand() {
                    width += additional_width_per_child;
                }
                child.size_allocate(&gtk::Allocation {
                    x,
                    y,
                    width,
                    height,
                });
                x += width + *self.h_spacing.borrow();
            }
        }

        /// Returns the barycentric combination of `min` and `max` with the given ratio,
        /// namely `min + ratio * (max - min) == (1 - ratio) * min + ratio * max`
        ///
        /// # Arguments
        ///
        /// * `min` - Min value
        /// * `max` - Max value
        /// * `ratio` - barycentric parameter, should be in `[0,1]` such that result is in `[min,max]`
        fn get_barycentric_combination((min, max): (i32, i32), ratio: f64) -> i32 {
            ((1.0 - ratio) * min as f64 + ratio * max as f64) as i32
        }
    }
}

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("Custom FlowBox Demo");
        let style = ".bordered {border: 1px solid black;}";
        let provider = gtk::CssProvider::new();
        provider
            .load_from_data(style.as_bytes())
            .expect("Failed to load CSS");
        gtk::StyleContext::add_provider_for_screen(
            &gdk::Screen::get_default().expect("Failed to load css provider"),
            &provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
        );

        let flow = ProperFlowBox::new();
        flow.set_h_spacing(10);
        flow.set_v_spacing(10);

        for i in 1..15 {
            let dummy = dummy::GAspectFillFrame::new();
            let label = gtk::Label::new(Some("Hello world"));
            dummy.add(&label);
            if i % 3 == 0 {
                dummy.set_hexpand(true);
            }
            label.get_style_context().add_class("bordered");
            flow.add(&dummy);
        }

        window.add(&flow);
        window.show_all();
    });

    application.run(&[]);
}