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: >k::Widget) -> (i32, i32) {
(50, 100)
}
fn get_preferred_height(&self, _widget: >k::Widget) -> (i32, i32) {
(50, 100)
}
fn get_request_mode(&self, _widget: >k::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: >k::Widget, allocation: >k::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: >k::Widget) -> gtk::SizeRequestMode {
gtk::SizeRequestMode::HeightForWidth
}
fn get_preferred_height(&self, widget: >k::Widget) -> (i32, i32) {
widget.get_preferred_height_for_width(widget.get_preferred_width().0)
}
fn get_preferred_width_for_height(&self, widget: >k::Widget, _height: i32) -> (i32, i32) {
widget.get_preferred_width()
}
fn get_preferred_width(&self, _widget: >k::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: >k::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: >k::Container, widget: >k::Widget) {
self.children.borrow_mut().push(widget.clone());
widget.set_parent(container);
if container.get_visible() {
container.queue_resize();
}
}
fn remove(&self, container: >k::Container, widget: >k::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: >k::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(>k::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(&[]);
}