ccbrown / iocraft

A Rust crate for beautiful, artisanally crafted CLIs, TUIs, and text-based IO.
https://crates.io/crates/iocraft
Apache License 2.0
306 stars 4 forks source link

Add stacked widget support #22

Closed skreborn closed 2 weeks ago

skreborn commented 2 weeks ago

Stacked widgets are important to allow multiple components to be drawn on top of each other, including floating buttons, titles on borders, or popups.

It's fairly easy to implement stacks using grid layout - although the result is a little too verbose and error-prone.

#[derive(Default, Props)]
pub struct StackProps<'a> {
  pub children: Vec<AnyElement<'a>>,
}

#[derive(Default)]
pub struct Stack {}

impl Component for Stack {
  type Props<'a> = StackProps<'a>;

  fn new(_props: &Self::Props<'_>) -> Self {
    Default::default()
  }

  fn update(&mut self, props: &mut Self::Props<'_>, _hooks: Hooks, updater: &mut ComponentUpdater) {
    let mut style = Style::DEFAULT;

    style.display = Display::Grid;

    style.grid_template_rows = vec![TrackSizingFunction::Single(MinMax {
      min: MinTrackSizingFunction::MinContent,
      max: MaxTrackSizingFunction::MaxContent,
    })];

    style.grid_template_columns = vec![TrackSizingFunction::Single(MinMax {
      min: MinTrackSizingFunction::MinContent,
      max: MaxTrackSizingFunction::MaxContent,
    })];

    updater.set_layout_style(style);
    updater.update_children(props.children.iter_mut(), None);
  }
}

#[derive(Default, Props)]
pub struct StackChildProps<'a> {
  pub children: Vec<AnyElement<'a>>,
}

#[derive(Default)]
pub struct StackChild {}

impl Component for StackChild {
  type Props<'a> = StackChildProps<'a>;

  fn new(_props: &Self::Props<'_>) -> Self {
    Default::default()
  }

  fn update(&mut self, props: &mut Self::Props<'_>, _hooks: Hooks, updater: &mut ComponentUpdater) {
    let mut style = Style::DEFAULT;

    style.grid_row = Line::from_line_index(1);
    style.grid_column = Line::from_line_index(1);

    updater.set_layout_style(style);
    updater.update_children(props.children.iter_mut(), None);
  }
}
  element! {
    Stack {
      StackChild {
        Box(
          width,
          height,
          border_style: BorderStyle::Round,
          flex_direction: FlexDirection::Column,
          align_items: AlignItems::Center,
          justify_content: JustifyContent::Center,
        ) {
          Text(content: "Hello world!")
        }
      }
      StackChild {
        Box(width, padding_left: 1, padding_right: 1) {
          Text(content: "Title")
        }
      }
    }
  }

However, since the instanced components are internally stored in unordered HashMaps, the render order is nondeterministic, and as such the stacked children are drawn in arbitrary orders. For example, the previous code can render either of the following two output variants.

╭Title───────────────────────────────────╮
│                                        │
│                                        │
│                                        │
│              Hello world!              │
│                                        │
│                                        │
│                                        │
╰────────────────────────────────────────╯
╭────────────────────────────────────────╮
│                                        │
│                                        │
│                                        │
│              Hello world!              │
│                                        │
│                                        │
│                                        │
╰────────────────────────────────────────╯

Other tricks that would allow drawing order the border of a parent include negative margins, but those seem to simply crash the application.

ccbrown commented 2 weeks ago

Thanks for the issue! I definitely want to add support for this, and probably very soon.

For now, I've gone ahead and pushed a fix to main for the negative margins issue, so now this will work consistently for the border title use-case:

use iocraft::prelude::*;

fn main() {
    element! {
        Box(
            border_style: BorderStyle::Round,
            border_color: Color::Blue,
            flex_direction: FlexDirection::Column,
        ) {
            Box(
                margin_top: -1,
            ) {
                Text(content: "Title")
            }
            Text(content: "Hello, world!")
        }
    }
    .print();
}
╭Title────────╮
│Hello, world!│
╰─────────────╯

I'll cut the release with this in the next few days as well.

ccbrown commented 2 weeks ago

I just pushed out a minor release with the margin fix and with support for additional Box properties: position, inset, top, left, right, bottom. Elements are also drawn in deterministic order now.

Stacking elements should be really easy now, and I've added an "overlap" example to demonstrate:

overlap

https://github.com/ccbrown/iocraft/blob/main/examples/overlap.rs

skreborn commented 1 week ago

Excellent work, much appreciated!