Custom fltk-rs horizontal split container and window resizing problem

Hey guys, it's been a while since my last topic.
I'm trying to create a horizontal split container with fltk-rs, everything works fine, you can set whether the child in the container is fixed-width or scalable, and set a minimum width for each child, and the splitters can be moved to change the width of the children, however, after moving the splitters, when I change the size of the window, I find that all the splitters have returned to their original positions as if I had not moved any of them.

my_fltk_project/
├── src/
│ ├── layouts/
│ │ └── h_split_container.rs
│ └── main.rs
├── Cargo.toml

Cargo.toml

[package]
name = "my_fltk_project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fltk = "1.4.32"

h_split_container.rs:

use fltk::{
    app,
    button::Button,
    enums::{Color, Event, FrameType},
    group::Group,
    prelude::*,
    widget::Widget,
};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Clone)]
struct Child {
    widget: Rc<RefCell<dyn WidgetExt>>,
    min_width: i32,
    fixed: bool,
    last_width: i32,
}

pub struct HSplitContainer {
    group: Group,
    children: Vec<Child>,
    splitters: Vec<Button>,
    last_width: i32,
}

impl HSplitContainer {
    pub fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
        let mut group = Group::new(x, y, width, height, "");
        group.set_frame(FrameType::FlatBox);

        HSplitContainer {
            group,
            children: Vec::new(),
            splitters: Vec::new(),
            last_width: width,
        }
    }

    pub fn add_child<W: WidgetExt + Clone + 'static>(&mut self, child: W, min_width: i32, fixed: bool) {
        let child_ref = Rc::new(RefCell::new(child.clone()));
        self.children.push(Child {
            widget: child_ref.clone(),
            min_width,
            fixed,
            last_width: min_width, // Initialize last_width with min_width
        });

        self.group.add(&child);

        if self.children.len() > 1 {
            self.add_splitter();
        }

        self.layout_children();
    }

    fn add_splitter(&mut self) {
        let mut splitter = Button::new(0, 0, 5, self.group.height(), "|");
        splitter.set_frame(FrameType::FlatBox);
        splitter.set_color(Color::Red);

        let mut group_clone = self.group.clone();
        let index = self.splitters.len(); // Current splitter index
        let children_clone = Rc::new(RefCell::new(self.children.clone()));

        splitter.handle({
            let children_clone = Rc::clone(&children_clone);
            move |s, ev| match ev {
                Event::Push => true,
                Event::Drag => {
                    let mut children = children_clone.borrow_mut();
                    if children[index].fixed || children[index + 1].fixed {
                        return true;
                    }

                    let diff = app::event_x() - s.x();
                    let new_x = s.x() + diff;

                    // Calculate minimum and maximum x positions for the splitter
                    let prev_child_pos = children[index].widget.borrow().x();
                    let prev_child_width = children[index].widget.borrow().width();
                    let next_child_pos = children[index + 1].widget.borrow().x();
                    let next_child_width = children[index + 1].widget.borrow().width();

                    let min_x = prev_child_pos + children[index].min_width;
                    let max_x = next_child_pos + next_child_width - children[index + 1].min_width - s.width();

                    // Ensure that moving the splitter maintains the minimum widths of both children
                    if new_x >= min_x && new_x <= max_x {
                        let new_first_child_width = new_x - prev_child_pos;
                        let new_second_child_pos = new_x + s.width();
                        let new_second_child_width = next_child_pos + next_child_width - new_second_child_pos;

                        if new_first_child_width >= children[index].min_width && new_second_child_width >= children[index + 1].min_width {
                            s.set_pos(new_x, s.y());

                            // Apply changes to children
                            children[index].widget.borrow_mut().set_size(new_first_child_width, group_clone.height());
                            children[index + 1].widget.borrow_mut().set_pos(new_second_child_pos, 0);
                            children[index + 1].widget.borrow_mut().set_size(new_second_child_width, group_clone.height());

                            children[index].last_width = new_first_child_width; // Update last_width
                            children[index + 1].last_width = new_second_child_width; // Update last_width

                            println!("Splitter moved: {:?}", s.x());
                            println!("First child width: {:?}", new_first_child_width);
                            println!("Second child width: {:?}", new_second_child_width);

                            group_clone.redraw();
                        }
                    }

                    true
                }
                _ => false,
            }
        });

        self.group.add(&splitter);
        self.splitters.push(splitter);
        self.layout_children();
    }

    fn layout_children(&mut self) {
        let num_children = self.children.len();
        if num_children > 1 {
            let splitter_width = 5;
            let mut fixed_width = 0;
            let mut resizable_count = 0;

            for child in &self.children {
                if child.fixed {
                    fixed_width += child.min_width;
                } else {
                    resizable_count += 1;
                }
            }

            let available_width = self.group.width() - fixed_width - (splitter_width * (num_children - 1) as i32);
            let resizable_width = if resizable_count > 0 {
                available_width / resizable_count as i32
            } else {
                0
            };

            let mut x_offset = 0;

            for (index, child) in self.children.iter_mut().enumerate() {
                let mut child_ref = child.widget.borrow_mut();

                if child.fixed {
                    child_ref.set_pos(x_offset, 0);
                    child_ref.set_size(child.min_width, self.group.height());
                    child.last_width = child.min_width; // Ensure last_width is updated
                    x_offset += child.min_width;
                } else {
                    let child_width = if resizable_width >= child.min_width {
                        resizable_width
                    } else {
                        child.min_width
                    };

                    child_ref.set_pos(x_offset, 0);
                    child_ref.set_size(child_width, self.group.height());
                    child.last_width = child_width; // Ensure last_width is updated
                    x_offset += child_width;
                }

                println!("Child {} width set to: {}", index, child_ref.width());

                if index < num_children - 1 {
                    if let Some(splitter_ref) = self.splitters.get_mut(index) {
                        splitter_ref.set_pos(x_offset, 0);
                        splitter_ref.set_size(splitter_width, self.group.height());
                        x_offset += splitter_width;
                    }
                }

                child_ref.redraw();
            }
        } else if num_children == 1 {
            if let Some(child) = self.children.get_mut(0) {
                let mut child_ref = child.widget.borrow_mut();
                child_ref.set_pos(0, 0);
                child_ref.set_size(self.group.width(), self.group.height());
                child.last_width = self.group.width(); // Ensure last_width is updated

                println!("Single child width set to: {}", self.group.width());
                child_ref.redraw();
            }
        }

        self.group.redraw();
    }

    pub fn group(&self) -> &Group {
        &self.group
    }

    pub fn set_resizable(&mut self) {
        self.group.set_frame(FrameType::FlatBox);
        self.group.set_color(Color::White);
        self.group.resizable(&self.group);
    }


}

main.rs:

use fltk::{app, prelude::*, window::Window};
mod layouts;

use crate::layouts::h_split_container::HSplitContainer;

fn main() {
    let app = app::App::default().with_scheme(app::AppScheme::Gleam);

    let mut wind = Window::new(100, 100, 800, 600, "HSplitContainer Example");

    let mut h_split = HSplitContainer::new(0, 0, 800, 600);
    h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 1"), 100,true); // Fixed width
    h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 2"), 100, false);  
    h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 3"), 100, false);
    h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 4"), 100, false);

    wind.end();
    wind.make_resizable(true);
    wind.show();



    app.run().unwrap();
}

Because Group keeps track of the size of its internal widgets. You need to tell it about the update, not just the children.

diff --git a/src/layouts/h_split_container.rs b/src/layouts/h_split_container.rs
index ef2894c..6ca19cd 100644
--- a/src/layouts/h_split_container.rs
+++ b/src/layouts/h_split_container.rs
@@ -111,6 +111,7 @@ impl HSplitContainer {
                         }
                     }
 
+                    group_clone.init_sizes();
                     true
                 }
                 _ => false,
1 Like

Thank you very much, this solved the problem, but there are other related problems:
1- The width of each splitter should be fixed, but I see that it changes as the window size changes.
2- Each child's width in the container must not be less than its own min_width, but it will be less as the window size is reduced.
3- Is it possible to make the window width non-shrinkable when all children of the container reach the minimum width for each child, i.e. the width of each child = its min_width?

It you really want to do this right, you probably want to:

  • Override the group's resize handler to use your own logic.
  • Keep track of the width that the user actually asks for, instead of just the width things happen to have. Otherwise, the behavior will always be miserable. I'm going to call this the "requested_width".
  • As you already mentioned, keep the window as a whole from getting too small.

Something like this:

diff --git a/src/layouts/h_split_container.rs b/src/layouts/h_split_container.rs
index ef2894c..7e92de6 100644
--- a/src/layouts/h_split_container.rs
+++ b/src/layouts/h_split_container.rs
@@ -14,14 +14,13 @@ struct Child {
     widget: Rc<RefCell<dyn WidgetExt>>,
     min_width: i32,
     fixed: bool,
-    last_width: i32,
+    requested_width: Option<i32>,
 }
 
 pub struct HSplitContainer {
     group: Group,
-    children: Vec<Child>,
+    children: Rc<RefCell<Vec<Child>>>,
     splitters: Vec<Button>,
-    last_width: i32,
 }
 
 impl HSplitContainer {
@@ -31,24 +30,23 @@ impl HSplitContainer {
 
         HSplitContainer {
             group,
-            children: Vec::new(),
+            children: Rc::new(RefCell::new(Vec::new())),
             splitters: Vec::new(),
-            last_width: width,
         }
     }
 
     pub fn add_child<W: WidgetExt + Clone + 'static>(&mut self, child: W, min_width: i32, fixed: bool) {
         let child_ref = Rc::new(RefCell::new(child.clone()));
-        self.children.push(Child {
+        self.children.borrow_mut().push(Child {
             widget: child_ref.clone(),
             min_width,
             fixed,
-            last_width: min_width, // Initialize last_width with min_width
+            requested_width: None,
         });
 
         self.group.add(&child);
 
-        if self.children.len() > 1 {
+        if self.children.borrow().len() > 1 {
             self.add_splitter();
         }
 
@@ -62,7 +60,7 @@ impl HSplitContainer {
 
         let mut group_clone = self.group.clone();
         let index = self.splitters.len(); // Current splitter index
-        let children_clone = Rc::new(RefCell::new(self.children.clone()));
+        let children_clone = self.children.clone();
 
         splitter.handle({
             let children_clone = Rc::clone(&children_clone);
@@ -100,17 +98,14 @@ impl HSplitContainer {
                             children[index + 1].widget.borrow_mut().set_pos(new_second_child_pos, 0);
                             children[index + 1].widget.borrow_mut().set_size(new_second_child_width, group_clone.height());
 
-                            children[index].last_width = new_first_child_width; // Update last_width
-                            children[index + 1].last_width = new_second_child_width; // Update last_width
-
-                            println!("Splitter moved: {:?}", s.x());
-                            println!("First child width: {:?}", new_first_child_width);
-                            println!("Second child width: {:?}", new_second_child_width);
+                            children[index].requested_width = Some(new_first_child_width);
+                            children[index + 1].requested_width = Some(new_second_child_width);
 
                             group_clone.redraw();
                         }
                     }
 
+                    group_clone.init_sizes();
                     true
                 }
                 _ => false,
@@ -123,17 +118,22 @@ impl HSplitContainer {
     }
 
     fn layout_children(&mut self) {
-        let num_children = self.children.len();
+        let mut children = self.children.borrow_mut();
+        let num_children = children.len();
         if num_children > 1 {
             let splitter_width = 5;
             let mut fixed_width = 0;
             let mut resizable_count = 0;
+            let mut min_width_for_requested_items = 0;
 
-            for child in &self.children {
+            for child in &children[..] {
                 if child.fixed {
                     fixed_width += child.min_width;
                 } else {
                     resizable_count += 1;
+                    if child.requested_width.is_some() {
+                        min_width_for_requested_items += child.min_width;
+                    }
                 }
             }
 
@@ -145,15 +145,31 @@ impl HSplitContainer {
             };
 
             let mut x_offset = 0;
+            let mut taken_width = 0;
 
-            for (index, child) in self.children.iter_mut().enumerate() {
+            for (index, child) in children.iter_mut().enumerate() {
                 let mut child_ref = child.widget.borrow_mut();
 
                 if child.fixed {
                     child_ref.set_pos(x_offset, 0);
                     child_ref.set_size(child.min_width, self.group.height());
-                    child.last_width = child.min_width; // Ensure last_width is updated
                     x_offset += child.min_width;
+                } else if let Some(requested_width) = child.requested_width {
+                    println!("Child {} width set to: {}", index, requested_width);
+                    let negotiable_requested_width = requested_width - child.min_width;
+                    let negotiable_available_width = available_width - min_width_for_requested_items;
+                    let negotiable_possible_width = negotiable_available_width - taken_width;
+                    let width = if negotiable_requested_width > negotiable_possible_width {
+                        let possible_width = child.min_width.max(negotiable_possible_width + child.min_width);
+                        println!("    doesn't fit: using {possible_width}");
+                        possible_width
+                    } else {
+                        requested_width
+                    };
+                    child_ref.set_pos(x_offset, 0);
+                    child_ref.set_size(width, self.group.height());
+                    x_offset += width;
+                    taken_width += width;
                 } else {
                     let child_width = if resizable_width >= child.min_width {
                         resizable_width
@@ -163,12 +179,10 @@ impl HSplitContainer {
 
                     child_ref.set_pos(x_offset, 0);
                     child_ref.set_size(child_width, self.group.height());
-                    child.last_width = child_width; // Ensure last_width is updated
                     x_offset += child_width;
+                    taken_width += child_width;
                 }
 
-                println!("Child {} width set to: {}", index, child_ref.width());
-
                 if index < num_children - 1 {
                     if let Some(splitter_ref) = self.splitters.get_mut(index) {
                         splitter_ref.set_pos(x_offset, 0);
@@ -180,17 +194,16 @@ impl HSplitContainer {
                 child_ref.redraw();
             }
         } else if num_children == 1 {
-            if let Some(child) = self.children.get_mut(0) {
+            if let Some(child) = children.get_mut(0) {
                 let mut child_ref = child.widget.borrow_mut();
                 child_ref.set_pos(0, 0);
                 child_ref.set_size(self.group.width(), self.group.height());
-                child.last_width = self.group.width(); // Ensure last_width is updated
 
-                println!("Single child width set to: {}", self.group.width());
                 child_ref.redraw();
             }
         }
 
+        self.group.init_sizes();
         self.group.redraw();
     }
 
@@ -198,10 +211,18 @@ impl HSplitContainer {
         &self.group
     }
 
-    pub fn set_resizable(&mut self) {
-        self.group.set_frame(FrameType::FlatBox);
-        self.group.set_color(Color::White);
-        self.group.resizable(&self.group);
+    pub fn end(mut self) {
+        let mut group = self.group.clone();
+        group.handle(move |s, ev| {
+            match ev {
+                Event::Resize => {
+                    self.layout_children();
+                    true
+                }
+                _ => false,
+            }
+        });
+        group.end();
     }
 
 
diff --git a/src/main.rs b/src/main.rs
index 5d1ac60..3306639 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,12 +13,12 @@ fn main() {
     h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 2"), 100, false);  
     h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 3"), 100, false);
     h_split.add_child(fltk::frame::Frame::new(0, 0, 200, 600, "Child 4"), 100, false);
+    h_split.end();
 
     wind.end();
     wind.make_resizable(true);
     wind.show();
-
-
+    wind.size_range(400, 400, 4096, 4096);
 
     app.run().unwrap();
-}
\ No newline at end of file
+}
1 Like

Thank you very much. I will follow your advice and try to make some modifications and corrections to the code

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.