1
1
Fork 0
mirror of https://github.com/griffi-gh/hUI.git synced 2025-03-14 03:46:27 -05:00
hUI/hui/src/element/builtin/container.rs

492 lines
18 KiB
Rust

//! a container element that can hold and layout multiple children elements
use derive_setters::Setters;
use glam::{Vec2, vec2};
use crate::{
background::BackgroundColor,
draw::{ImageHandle, RoundedCorners, UiDrawCommand},
element::{ElementList, MeasureContext, ProcessContext, UiElement},
layout::{Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d},
measure::{Hints, Response},
rect::{Corners, Sides},
};
// pub struct Border {
// pub color: Vec4,
// pub width: f32,
// }
//XXX: add Order/Direction::Forward/Reverse or sth?
//TODO: clip children flag
//TODO: borders
//TODO: min/max size
#[derive(Clone, Copy)]
struct CudLine {
start_idx: usize,
content_size: Vec2,
}
struct ContainerUserData {
lines: Vec<CudLine>,
}
/// A container element that can hold and layout multiple children elements
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Container {
/// Size of the container
#[setters(into)]
pub size: Size2d,
/// Layout direction (horizontal/vertical)
pub direction: Direction,
//XXX: should we have separate gap value for primary and secondary (when wrapped, between lines of elements) axis?
/// Gap between children elements
pub gap: f32,
/// Padding inside the container (distance from the edges to the children elements)
#[setters(into)]
pub padding: Sides<f32>,
/// Alignment of the children elements on X and Y axis
#[setters(into)]
pub align: Alignment2d,
/// Background color of the container\
///
/// If the container has a background texture, it will be multiplied by this color
#[setters(into)]
pub background: BackgroundColor,
/// Background texture of the container
///
/// Can be used in conjunction with the background color\
/// In this case, the texture will be shaded by the color
///
/// Please note that if the background color is NOT set (or set to transparent), the texture will NOT be visible\
/// This is because the texture is multiplied by the color, and if the color is transparent, the texture will be too\
//TODO: fix this flaw, if background_image is called for the first time, bg wasnt explicitly set and background is transparent, set it to white
#[setters(into)]
pub background_image: Option<ImageHandle>,
/// Corner radius of the background rectangle
#[setters(into)]
pub corner_radius: Corners<f32>,
/// Set this to `true` to allow the elements wrap automatically
///
/// Disabling/enabling this does not affect explicit wrapping\
/// (for example, `Br`, or any other element with `should_wrap` set to `true`)
///
/// This is an experimental feature and may not work as expected
pub wrap: bool,
/// List of children elements
#[setters(skip)]
pub children: ElementList,
}
impl Container {
pub fn with_children(mut self, ui: impl FnOnce(&mut ElementList)) -> Self {
self.children.0.extend(ElementList::from_callback(ui).0);
self
}
}
impl Default for Container {
fn default() -> Self {
Self {
size: (Size::Auto, Size::Auto).into(),
direction: Direction::Vertical,
gap: 0.,
padding: Sides::all(0.),
align: Alignment2d::default(),
background: Default::default(),
background_image: None,
children: ElementList(Vec::new()),
wrap: false,
corner_radius: Corners::all(0.),
}
}
}
impl Container {
pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
let outer_size_x = match self.size.width {
Size::Auto => layout.max_size.x,
Size::Fraction(p) => layout.max_size.x * p,
Size::Static(p) => p,
};
let outer_size_y = match self.size.height {
Size::Auto => layout.max_size.y,
Size::Fraction(p) => layout.max_size.y * p,
Size::Static(p) => p,
};
vec2(
outer_size_x - (self.padding.left + self.padding.right),
outer_size_y - (self.padding.top + self.padding.bottom),
)
}
}
impl UiElement for Container {
fn name(&self) -> &'static str {
"Container"
}
fn measure(&self, ctx: MeasureContext) -> Response {
// XXX: If both axes are NOT set to auto, we should be able quickly return the size
// ... but we can't, because we need to measure the children to get the inner_content_size and user_data values
// this is a potential optimization opportunity, maybe we could postpone this to the process call
// as it's guaranteed to be called only ONCE, while measure is assumed to be cheap and called multiple times
// ... we could also implement some sort of "global" caching for the measure call (to prevent traversal of the same tree multiple times),
// but that's a bit more complex and probably impossible with the current design of the measure/process calls
// In case wrapping is enabled, elements cannot exceed this size on the primary axis
let max_line_pri = match self.direction {
Direction::Horizontal => match self.size.width {
Size::Auto => ctx.layout.max_size.x,
Size::Fraction(p) => ctx.layout.max_size.x * p,
Size::Static(p) => p,
},
Direction::Vertical => match self.size.height {
Size::Auto => ctx.layout.max_size.y,
Size::Fraction(p) => ctx.layout.max_size.y * p,
Size::Static(p) => p,
}
};
//size of AABB containing all lines
let mut total_size = Vec2::ZERO;
//Size of the current row/column (if wrapping)
let mut line_size = Vec2::ZERO;
//Size of previous sec. axes combined
//(basically, in case of the horizontal layout, this is the height of the tallest element in the line)
//This is a vec2, but only one axis is used, depending on the layout direction
let mut line_sec_offset: Vec2 = Vec2::ZERO;
//Amount of elements in the current line
let mut line_element_count = 0;
//Leftover gap from the previous element on the primary axis
let mut leftover_gap = Vec2::ZERO;
//line metadata for the user_data
let mut lines = vec![
CudLine {
start_idx: 0,
content_size: Vec2::ZERO,
}
];
for (idx, element) in self.children.0.iter().enumerate() {
let measure = element.measure(MeasureContext{
state: ctx.state,
layout: &LayoutInfo {
//XXX: if the element gets wrapped, this will be inaccurate.
//But, we cant know the size of the line until we measure it, and also
//We dont make any guarantees about this value being valid during the `measure` call
//For all intents and purposes, this is just a *hint* for the element to use
//(and could be just set to 0 for all we care)
position: ctx.layout.position + line_size + line_sec_offset,
//TODO: subtract size already taken by previous children
max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction,
},
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//Check the position of the side of element closest to the end on the primary axis
let end_pos_pri = match self.direction {
Direction::Horizontal => line_size.x + measure.size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + measure.size.y + self.padding.top + self.padding.bottom,
};
//Wrap the element if it exceeds container's size and is not the first element in the line
if ((self.wrap && (end_pos_pri > max_line_pri)) || measure.should_wrap) && (line_element_count > 0) {
// >>>>>>> WRAP THAT B*TCH!
//Negate the leftover gap from the previous element
line_size -= leftover_gap;
//update the previous line metadata
lines.last_mut().unwrap().content_size = line_size;
//push the line metadata
lines.push(CudLine {
start_idx: idx,
content_size: Vec2::ZERO,
});
//Update the total size accordingly
match self.direction {
Direction::Horizontal => {
total_size.x = total_size.x.max(line_size.x);
total_size.y += line_size.y + self.gap;
},
Direction::Vertical => {
total_size.x += line_size.x + self.gap;
total_size.y = total_size.y.max(line_size.y);
}
}
//Now, update line_sec_offset
match self.direction {
Direction::Horizontal => {
line_sec_offset.y += measure.size.y + self.gap;
},
Direction::Vertical => {
line_sec_offset.x += measure.size.x + self.gap;
}
};
//Reset the line size and element count
line_size = Vec2::ZERO;
line_element_count = 0;
}
//Increment element count
line_element_count += 1;
//Sset the leftover gap in case this is the last element in the line
match self.direction {
Direction::Horizontal => {
line_size.x += measure.size.x + self.gap;
line_size.y = line_size.y.max(measure.size.y);
leftover_gap = vec2(self.gap, 0.);
},
Direction::Vertical => {
line_size.x = line_size.x.max(measure.size.x);
line_size.y += measure.size.y + self.gap;
leftover_gap = vec2(0., self.gap);
}
}
}
line_size -= leftover_gap;
//Update the content size of the last line
lines.last_mut().unwrap().content_size = line_size;
//Update the total size according to the size of the last line
match self.direction {
Direction::Horizontal => {
total_size.x = total_size.x.max(line_size.x);
total_size.y += line_size.y;
},
Direction::Vertical => {
total_size.x += line_size.x;
total_size.y = total_size.y.max(line_size.y);
}
}
//Now, total_size should hold the size of the AABB containing all lines
//This is exactly what inner_content_size hint should be set to
let inner_content_size = Some(total_size);
//After setting the inner_content_size, we can calculate the size of the container
//Including padding, and in case the size is set to non-auto, override the size
total_size += vec2(
self.padding.left + self.padding.right,
self.padding.top + self.padding.bottom,
);
match self.size.width {
Size::Auto => (),
Size::Fraction(percentage) => total_size.x = ctx.layout.max_size.x * percentage,
Size::Static(pixels) => total_size.x = pixels,
}
match self.size.height {
Size::Auto => (),
Size::Fraction(percentage) => total_size.y = ctx.layout.max_size.y * percentage,
Size::Static(pixels) => total_size.y = pixels,
}
Response {
size: total_size,
hints: Hints {
inner_content_size,
..Default::default()
},
user_data: Some(Box::new(ContainerUserData { lines })),
..Default::default()
}
}
fn process(&self, ctx: ProcessContext) {
let user_data: &ContainerUserData = ctx.measure.user_data
.as_ref().expect("no user data attached to container")
.downcast_ref().expect("invalid user data type");
let mut position = ctx.layout.position;
//background
if !self.background.is_transparent() {
let corner_colors = self.background.corners();
ctx.draw.add(UiDrawCommand::Rectangle {
position,
size: ctx.measure.size,
color: corner_colors,
texture: self.background_image,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
RoundedCorners::from_radius(self.corner_radius)
}),
});
}
//padding
position += vec2(self.padding.left, self.padding.top);
//convert alignment to pri/sec axis based
//.0 = primary, .1 = secondary
let pri_sec_align = match self.direction {
Direction::Horizontal => (self.align.horizontal, self.align.vertical),
Direction::Vertical => (self.align.vertical, self.align.horizontal),
};
//alignment (on sec. axis)
// match pri_sec_align.1 {
// Alignment::Begin => (),
// Alignment::Center => {
// position += match self.direction {
// UiDirection::Horizontal => vec2(0., (ctx.measure.size.y - self.padding.top - self.padding.bottom - user_data.lines.last().unwrap().content_size.y) / 2.),
// UiDirection::Vertical => vec2((ctx.measure.size.x - self.padding.left - self.padding.right - user_data.lines.last().unwrap().content_size.x) / 2., 0.),
// };
// },
// Alignment::End => {
// position += match self.direction {
// UiDirection::Horizontal => vec2(0., ctx.measure.size.y - user_data.lines.last().unwrap().content_size.y - self.padding.bottom - self.padding.top),
// UiDirection::Vertical => vec2(ctx.measure.size.x - user_data.lines.last().unwrap().content_size.x - self.padding.right - self.padding.left, 0.),
// };
// }
// }
for (line_idx, cur_line) in user_data.lines.iter().enumerate() {
let mut local_position = position;
//alignment on primary axis
match (pri_sec_align.0, self.direction) {
(Alignment::Begin, _) => (),
(Alignment::Center, Direction::Horizontal) => {
local_position.x += (ctx.measure.size.x - cur_line.content_size.x) / 2. - self.padding.left;
},
(Alignment::Center, Direction::Vertical) => {
local_position.y += (ctx.measure.size.y - cur_line.content_size.y) / 2. - self.padding.top;
},
(Alignment::End, Direction::Horizontal) => {
local_position.x += ctx.measure.size.x - cur_line.content_size.x - self.padding.right - self.padding.left;
},
(Alignment::End, Direction::Vertical) => {
local_position.y += ctx.measure.size.y - cur_line.content_size.y - self.padding.bottom - self.padding.top;
}
}
let next_line_begin = user_data.lines
.get(line_idx + 1)
.map(|l| l.start_idx)
.unwrap_or(self.children.0.len());
for element_idx in cur_line.start_idx..next_line_begin {
let element = &self.children.0[element_idx];
//(passing max size from layout rather than actual known bounds for the sake of consistency with measure() above)
//... as this must match!
let mut el_layout = LayoutInfo {
position: local_position,
max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction,
};
//measure
let el_measure = element.measure(MeasureContext {
state: ctx.state,
layout: &el_layout,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//align (on sec. axis)
//TODO separate align withing the line and align of the whole line
let inner_content_size = ctx.measure.hints.inner_content_size.unwrap();
match (pri_sec_align.1, self.direction) {
(Alignment::Begin, _) => (),
(Alignment::Center, Direction::Horizontal) => {
//Align whole row
el_layout.position.y += ((ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y) / 2.;
//Align within row
el_layout.position.y += (cur_line.content_size.y - el_measure.size.y) / 2.;
},
(Alignment::Center, Direction::Vertical) => {
//Align whole row
el_layout.position.x += ((ctx.measure.size.x - self.padding.left - self.padding.right) - inner_content_size.x) / 2.;
//Align within row
el_layout.position.x += (cur_line.content_size.x - el_measure.size.x) / 2.;
},
//TODO update these two cases:
(Alignment::End, Direction::Horizontal) => {
//Align whole row
el_layout.position.y += (ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y;
//Align within row
el_layout.position.y += cur_line.content_size.y - el_measure.size.y;
},
(Alignment::End, Direction::Vertical) => {
//Align whole row
el_layout.position.x += (ctx.measure.size.x - self.padding.right - self.padding.left) - inner_content_size.x;
//Align within row
el_layout.position.x += cur_line.content_size.x - el_measure.size.x;
}
}
//process
element.process(ProcessContext {
measure: &el_measure,
state: ctx.state,
layout: &el_layout,
draw: ctx.draw,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
//HACK: i have no idea what to do with this
//this sucks
signal: ctx.signal,
});
//layout
match self.direction {
Direction::Horizontal => {
local_position.x += el_measure.size.x + self.gap;
},
Direction::Vertical => {
local_position.y += el_measure.size.y + self.gap;
}
}
}
//Move to the next line
match self.direction {
Direction::Horizontal => {
position.y += cur_line.content_size.y + self.gap;
//position.x -= cur_line.content_size.x;
// leftover_line_gap = vec2(0., self.gap);
}
Direction::Vertical => {
position.x += cur_line.content_size.x + self.gap;
//position.y -= cur_line.content_size.y;
// leftover_line_gap = vec2(self.gap, 0.);
}
};
}
}
}