wrflib 0.0.3

🐢⚡ Wrflib is a fast, cross-platform (web+native), GPU-based application framework, written in Rust.
Documentation
//! Layout system. 🐢

use crate::debug_log::DebugLog;
use crate::*;

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CxBoxType {
    Normal,
    RightBox,
    BottomBox,
    CenterXAlign,
    CenterYAlign,
    CenterXYAlign,
    PaddingBox,
    Row,
    Column,
    AbsoluteBox,
    WrappingBox,
    View,
}

impl Default for CxBoxType {
    fn default() -> CxBoxType {
        CxBoxType::Normal
    }
}

/// A [`CxLayoutBox`] is an internal implementation details for layouting system.
#[derive(Clone, Default, Debug)]
pub(crate) struct CxLayoutBox {
    /// The layout that is associated directly with this box, which determines a lot of its
    /// behavior.
    pub(crate) layout: Layout,

    /// The index within Cx::layout_box_align_list, which contains all the things that we draw within this
    /// box, and which needs to get aligned at some point. We have a separate list for x/y
    /// because you can manually trigger an alignment (aside from it happening automatically at the
    /// end), which resets this list to no longer align those areas again.
    pub(crate) align_list_x_start_index: usize,

    /// Same as [`CxLayoutBox::align_list_x_start_index`] but for vertical alignment.
    pub(crate) align_list_y_start_index: usize,

    /// The current position of the current box.
    /// This is an absolute position, and starts out at [`CxLayoutBox::origin`]
    /// plus padding.
    pub(crate) pos: Vec2,

    /// The origin of the current box. Starts off at the parent's box [`CxLayoutBox::pos`]
    pub(crate) origin: Vec2,

    /// The inherent width of the current box's walking area. Is [`f32::NAN`] if the width is computed,
    /// and can get set explicitly later.
    pub(crate) width: f32,

    /// The inherent height of the current box's walking area. Is [`f32::NAN`] if the height is computed,
    /// and can get set explicitly later.
    pub(crate) height: f32,

    /// Seems to only be used to be passed down to child boxes, so if one of them gets an absolute
    /// origin passed in, we can just use the entire remaining absolute canvas as the width/height.
    ///
    /// TODO(JP): seems pretty unnecessary; why not just grab this field from the current [`Pass`
    /// directly if necessary? Or just always have the caller pass it in (they can take it from the
    /// current [`Pass`] if they want)?
    pub(crate) abs_size: Vec2,

    /// Keeps track of the bottom right corner where we have walked so far, including the width/height
    /// of the walk, whereas [`CxLayoutBox::pos`] stays in the top left position of what we have last drawn.
    ///
    /// TODO(JP): [`CxLayoutBox::pos`] and [`CxLayoutBox::bound_right_bottom`] can (and seem to regularly and intentionally do)
    /// get out of sync, which makes things more confusing.
    pub(crate) bound_right_bottom: Vec2,

    /// We keep track of the [`LayoutSize`] with the greatest height (or width, when walking down), so that
    /// we know how much to move the box's y-position when wrapping to the next line. When
    /// wrapping to the next line, this value is reset back to 0.
    ///
    /// See also [`Padding`].
    pub(crate) biggest: f32,

    /// Used for additional checks that enclosing box match opening ones
    pub(crate) box_type: CxBoxType,

    /// Available width for the content of the box, starting from box origin, minus right padding.
    /// This is different from [`CxLayoutBox::width`] which is box outer width.
    /// For example, for Width::Compute boxes width would be [`f32::NAN`] as this needs to be computed,
    /// but available_width is defined until the bounds of parent.
    /// This is capped at 0 if the content already overflows the bounds.
    pub(crate) available_width: f32,

    /// Available height for the content of the box, starting from box origin, minus bottom padding.
    /// This is different from [`CxLayoutBox::height`] which is box outer height.
    /// For example, for Height::Compute boxes height would be [`f32::NAN`] as this needs to be computed,
    /// but available_height is defined until the bounds of parent/
    /// This is capped at 0 if the content already overflows the bounds.
    pub(crate) available_height: f32,
}

impl CxLayoutBox {
    /// Returns how much available_width is "left" for current box,
    /// i.e. distance from current box x position until the right bound
    pub(crate) fn get_width_left(&self) -> f32 {
        (self.origin.x + self.available_width - self.pos.x).max(0.)
    }

    /// Returns how much available_height is "left" for current box
    /// i.e. distance from current box y position until the bottom bound
    pub(crate) fn get_height_left(&self) -> f32 {
        (self.origin.y + self.available_height - self.pos.y).max(0.)
    }
}

impl Cx {
    /// Begin a new [`CxLayoutBox`] with a given [`Layout`]. This new [`CxLayoutBox`] will be added to the
    /// [`Cx::layout_boxes`] stack.
    pub(crate) fn begin_typed_box(&mut self, box_type: CxBoxType, layout: Layout) {
        if !self.in_redraw_cycle {
            panic!("calling begin_typed_box outside of redraw cycle is not possible!");
        }
        if layout.direction == Direction::Down && layout.line_wrap != LineWrap::None {
            panic!("Direction down with line wrapping is not supported");
        }

        // fetch origin and size from parent
        let (mut origin, mut abs_size) = if let Some(parent) = self.layout_boxes.last() {
            (Vec2 { x: parent.pos.x, y: parent.pos.y }, parent.abs_size)
        } else {
            assert!(layout.absolute);
            assert!(layout.abs_size.is_some());
            (Vec2 { x: 0., y: 0. }, Vec2::default())
        };

        // see if layout overrode size
        if let Some(layout_abs_size) = layout.abs_size {
            abs_size = layout_abs_size;
        }

        let width;
        let height;
        if layout.absolute {
            // absolute overrides origin to start from (0, 0)
            origin = vec2(0.0, 0.0);
            // absolute overrides the computation of width/height to use the parent absolute
            width = self.eval_absolute_width(&layout.layout_size.width, abs_size.x);
            height = self.eval_absolute_height(&layout.layout_size.height, abs_size.y);
        } else {
            width = self.eval_width(&layout.layout_size.width);
            height = self.eval_height(&layout.layout_size.height);
        }

        let pos = Vec2 { x: origin.x + layout.padding.l, y: origin.y + layout.padding.t };

        let available_width =
            (self.eval_available_width(&layout.layout_size.width, layout.absolute, abs_size) - layout.padding.r).max(0.);
        let available_height =
            (self.eval_available_height(&layout.layout_size.height, layout.absolute, abs_size) - layout.padding.b).max(0.);

        // By induction property this values should never be NaN
        assert!(!available_width.is_nan());
        assert!(!available_height.is_nan());

        let layout_box = CxLayoutBox {
            align_list_x_start_index: self.layout_box_align_list.len(),
            align_list_y_start_index: self.layout_box_align_list.len(),
            origin,
            pos,
            layout,
            biggest: 0.0,
            bound_right_bottom: Vec2 { x: std::f32::NEG_INFINITY, y: std::f32::NEG_INFINITY },
            width,
            height,
            abs_size,
            box_type,
            available_height,
            available_width,
        };

        self.layout_boxes.push(layout_box);
    }

    /// Pop the current [`CxLayoutBox`] from the [`Cx::layout_boxes`] stack, returning a [`Rect`] that the box walked
    /// during its lifetime. The parent [`CxLayoutBox`] will be made to walk this [`Rect`].
    pub(crate) fn end_typed_box(&mut self, box_type: CxBoxType) -> Rect {
        self.assert_last_box_type_matches(box_type);
        self.end_last_box_unchecked()
    }

    pub(crate) fn assert_last_box_type_matches(&self, box_type: CxBoxType) {
        let layout_box = self.layout_boxes.last().unwrap();
        if layout_box.box_type != box_type {
            panic!("Closing box type doesn't match! Expected: {:?}, found: {:?}", box_type, layout_box.box_type);
        }
    }

    /// Similar to [`Cx::end_typed_box`], but doesn't do any matching checks on the box. Use at your own risk!
    fn end_last_box_unchecked(&mut self) -> Rect {
        let old = self.layout_boxes.pop().unwrap();
        let w = if old.width.is_nan() {
            // when nesting Fill box inside Compute the former would have nan width
            if old.layout.layout_size.width == Width::Fill {
                // use all available width + padding
                Width::Fix(old.available_width + old.layout.padding.r)
            } else if old.bound_right_bottom.x == std::f32::NEG_INFINITY {
                // nothing happened, use padding
                Width::Fix(old.layout.padding.l + old.layout.padding.r)
            } else {
                // use the bounding box
                Width::Fix(max_zero_keep_nan(old.bound_right_bottom.x - old.origin.x + old.layout.padding.r))
            }
        } else {
            Width::Fix(old.width)
        };

        let h = if old.height.is_nan() {
            // when nesting Fill box inside Compute the former would have nan height
            if old.layout.layout_size.height == Height::Fill {
                // use all available height + padding
                Height::Fix(old.available_height + old.layout.padding.b)
            } else if old.bound_right_bottom.y == std::f32::NEG_INFINITY {
                // nothing happened use the padding
                Height::Fix(old.layout.padding.t + old.layout.padding.b)
            } else {
                // use the bounding box
                Height::Fix(max_zero_keep_nan(old.bound_right_bottom.y - old.origin.y + old.layout.padding.b))
            }
        } else {
            Height::Fix(old.height)
        };

        let rect = {
            // when a box is absolutely positioned don't walk the parent
            if old.layout.absolute {
                let w = if let Width::Fix(vw) = w { vw } else { 0. };
                let h = if let Height::Fix(vh) = h { vh } else { 0. };
                Rect { pos: vec2(0., 0.), size: vec2(w, h) }
            } else {
                self.move_box_with_old(LayoutSize { width: w, height: h }, Some(&old))
            }
        };
        self.debug_logs.push(DebugLog::EndBox { rect });
        rect
    }

    /// Move the box with the given [`LayoutSize`]
    ///
    /// Returns a [`Rect`] containing the area that the box moved
    ///
    /// TODO(JP): This `old_box` stuff is a bit awkward and only used for the
    /// alignment stuff at the end. We can probably structure this in a nicer way.
    pub(crate) fn move_box_with_old(&mut self, layout_size: LayoutSize, old_box: Option<&CxLayoutBox>) -> Rect {
        let mut align_dx = 0.0;
        let mut align_dy = 0.0;

        // TODO(JP): This seems a bit weird: you can technically pass in Width::Compute, which would
        // return a NaN for `w`, but that doesn't make much sense when you explicitly do a walk.
        // It looks like it's assumed that that never gets passed in here, but it would be better to
        // verify that.
        // NOTE(Dmitry): now this methods will panic when receiving Compute sizes.
        // We can probably express this better in type system, but this is good enough for now.
        let w = self.eval_walking_width(&layout_size.width);
        let h = self.eval_walking_height(&layout_size.height);

        let ret = if let Some(layout_box) = self.layout_boxes.last_mut() {
            let old_pos = match layout_box.layout.direction {
                Direction::Right => {
                    match layout_box.layout.line_wrap {
                        LineWrap::Overflow => {
                            if (layout_box.pos.x + w) > (layout_box.origin.x + layout_box.available_width) + 0.01 {
                                // what is the move delta.
                                let old_x = layout_box.pos.x;
                                let old_y = layout_box.pos.y;
                                layout_box.pos.x = layout_box.origin.x + layout_box.layout.padding.l;
                                layout_box.pos.y += layout_box.biggest;
                                layout_box.biggest = 0.0;
                                align_dx = layout_box.pos.x - old_x;
                                align_dy = layout_box.pos.y - old_y;
                            }
                        }
                        LineWrap::None => {}
                    }

                    let old_pos = layout_box.pos;
                    // walk it normally
                    layout_box.pos.x += w;

                    // keep track of biggest item in the line
                    layout_box.biggest = layout_box.biggest.max(h);
                    old_pos
                }
                Direction::Down => {
                    let old_pos = layout_box.pos;
                    // walk it normally
                    layout_box.pos.y += h;

                    // keep track of biggest item in the line
                    layout_box.biggest = layout_box.biggest.max(w);
                    old_pos
                }
            };

            // update bounds
            let new_bound = old_pos + vec2(w, h);
            layout_box.bound_right_bottom = layout_box.bound_right_bottom.max(&new_bound);

            Rect { pos: old_pos, size: vec2(w, h) }
        } else {
            Rect { pos: vec2(0.0, 0.0), size: vec2(w, h) }
        };

        if align_dx != 0.0 {
            if let Some(old_box) = old_box {
                self.move_by_x(align_dx, old_box.align_list_x_start_index);
            }
        };
        if align_dy != 0.0 {
            if let Some(old_box) = old_box {
                self.move_by_y(align_dy, old_box.align_list_y_start_index);
            }
        };

        ret
    }

    /// Actually perform a horizontal movement of items in [`Cx::layout_box_align_list`], but only for positive dx
    pub(crate) fn do_align_x(&mut self, dx: f32, align_start: usize) {
        if dx < 0. {
            // do only forward moving alignment
            // backwards alignment could happen if the size of content became larger than the container
            // in which case the alignment is not well defined
            return;
        }
        self.move_by_x(dx, align_start)
    }

    /// Actually perform a horizontal movement of items in [`Cx::layout_box_align_list`].
    /// Unlike "do_align_x" negative moves can happen here because of wrapping behavior.
    ///
    /// TODO(JP): Should we move some of this stuff to [`Area`], where we already seem to do a bunch
    /// of rectangle and position calculations?
    fn move_by_x(&mut self, dx: f32, align_start: usize) {
        let dx = (dx * self.current_dpi_factor).floor() / self.current_dpi_factor;
        for i in align_start..self.layout_box_align_list.len() {
            let align_item = &self.layout_box_align_list[i];
            match align_item {
                Area::InstanceRange(inst) => {
                    let cxview = &mut self.views[inst.view_id];
                    let draw_call = &mut cxview.draw_calls[inst.draw_call_id];
                    let sh = &self.shaders[draw_call.shader_id];
                    for i in 0..inst.instance_count {
                        if let Some(rect_pos) = sh.mapping.rect_instance_props.rect_pos {
                            draw_call.instances[inst.instance_offset + rect_pos + i * sh.mapping.instance_props.total_slots] +=
                                dx;
                        }
                    }
                }
                Area::View(view_area) => {
                    let cxview = &mut self.views[view_area.view_id];
                    cxview.rect.pos.x += dx;
                }
                // TODO(JP): Would be nice to implement this for [`Align::View`], which would
                // probably require some offset field on [`CxView`] that gets used during rendering.
                _ => unreachable!(),
            }
        }
    }

    /// Actually perform a vertical movement of items in [`Cx::layout_box_align_list`], but only for positive dy
    pub(crate) fn do_align_y(&mut self, dy: f32, align_start: usize) {
        if dy < 0. {
            // do only forward moving alignment
            // backwards alignment could happen if the size of content became larger than the container
            // in which case the alignment is not well defined
            return;
        }
        self.move_by_y(dy, align_start);
    }

    /// Actually perform a vertical movement of items in [`Cx::layout_box_align_list`].
    /// Unlike "do_align_y" negative moves can happen here because of wrapping behavior.
    ///
    /// TODO(JP): Should we move some of this stuff to [`Area`], where we already seem to do a bunch
    /// of rectangle and position calculations?
    fn move_by_y(&mut self, dy: f32, align_start: usize) {
        let dy = (dy * self.current_dpi_factor).floor() / self.current_dpi_factor;
        for i in align_start..self.layout_box_align_list.len() {
            let align_item = &self.layout_box_align_list[i];
            match align_item {
                Area::InstanceRange(inst) => {
                    let cxview = &mut self.views[inst.view_id];
                    let draw_call = &mut cxview.draw_calls[inst.draw_call_id];
                    let sh = &self.shaders[draw_call.shader_id];
                    for i in 0..inst.instance_count {
                        if let Some(rect_pos) = sh.mapping.rect_instance_props.rect_pos {
                            draw_call.instances
                                [inst.instance_offset + rect_pos + 1 + i * sh.mapping.instance_props.total_slots] += dy;
                        }
                    }
                }
                Area::View(view_area) => {
                    let cxview = &mut self.views[view_area.view_id];
                    cxview.rect.pos.y += dy;
                }
                // TODO(JP): Would be nice to implement this for `Align::View`, which would
                // probably require some offset field on `CxView` that gets used during rendering.
                _ => unreachable!(),
            }
        }
    }

    /// Returns how many pixels we should move over based on the [`AlignX`] ratio
    /// (which is between 0 and 1). We do this by looking at the bound
    /// ([`CxLayoutBox::bound_right_bottom`]) to see how much we have actually drawn, and how
    /// subtract that from the width of this box. That "remaining width" is
    /// then multiplied with the ratio. If there is no inherent width then this
    /// will return 0.
    pub(crate) fn compute_align_box_x(layout_box: &CxLayoutBox, align: AlignX) -> f32 {
        let AlignX(fx) = align;
        if fx > 0.0 {
            // TODO(Dmitry): check if we need use padding here
            let dx = fx
                * ((layout_box.available_width - (layout_box.layout.padding.l + layout_box.layout.padding.r))
                    - (layout_box.bound_right_bottom.x - (layout_box.origin.x + layout_box.layout.padding.l)));
            if dx.is_nan() {
                return 0.0;
            }
            dx
        } else {
            0.
        }
    }

    /// Returns how many pixels we should move over based on the [`AlignY`] ratio
    /// (which is between 0 and 1). We do this by looking at the bound
    /// ([`CxLayoutBox::bound_right_bottom`]) to see how much we have actually drawn, and how
    /// subtract that from the height of this box. That "remaining height" is
    /// then multiplied with the ratio. If there is no inherent height then this
    /// will return 0.
    pub(crate) fn compute_align_box_y(layout_box: &CxLayoutBox, align: AlignY) -> f32 {
        let AlignY(fy) = align;
        if fy > 0.0 {
            // TODO(Dmitry): check if we need use padding here
            let dy = fy
                * ((layout_box.available_height - (layout_box.layout.padding.t + layout_box.layout.padding.b))
                    - (layout_box.bound_right_bottom.y - (layout_box.origin.y + layout_box.layout.padding.t)));
            if dy.is_nan() {
                return 0.0;
            }
            dy
        } else {
            0.
        }
    }

    // TODO(Dmitry): simplify all the following eval functions
    fn eval_width(&self, width: &Width) -> f32 {
        match width {
            Width::Compute => std::f32::NAN,
            Width::Fix(v) => v.max(0.),
            Width::Fill => self.get_width_left(),
            Width::FillUntil(v) => self.get_width_left().min(*v),
        }
    }

    fn eval_absolute_width(&self, width: &Width, abs_size: f32) -> f32 {
        match width {
            Width::Compute => std::f32::NAN,
            Width::Fix(v) => max_zero_keep_nan(*v),
            Width::Fill => max_zero_keep_nan(abs_size),
            Width::FillUntil(v) => min_keep_nan(*v, abs_size),
        }
    }

    fn eval_walking_width(&self, width: &Width) -> f32 {
        match width {
            Width::Compute => panic!("Walking with Width:Compute is not supported"),
            Width::Fix(v) => v.max(0.),
            Width::Fill => self.get_width_left(),
            Width::FillUntil(v) => self.get_width_left().min(*v),
        }
    }

    fn eval_available_width(&self, width: &Width, absolute: bool, abs_size: Vec2) -> f32 {
        if absolute {
            return abs_size.x;
        }

        // Non-absolute layouts will always have parents
        let parent = self.layout_boxes.last().unwrap();
        match width {
            Width::Fix(v) => *v,
            Width::FillUntil(v) => parent.get_width_left().min(*v),
            Width::Compute | Width::Fill => parent.get_width_left(),
        }
    }

    fn eval_height(&self, height: &Height) -> f32 {
        match height {
            Height::Compute => std::f32::NAN,
            Height::Fix(v) => v.max(0.),
            Height::Fill => self.get_height_left(),
            Height::FillUntil(v) => self.get_height_left().min(*v),
        }
    }

    fn eval_absolute_height(&self, height: &Height, abs_size: f32) -> f32 {
        match height {
            Height::Compute => std::f32::NAN,
            Height::Fix(v) => v.max(0.),
            Height::Fill => max_zero_keep_nan(abs_size),
            Height::FillUntil(v) => min_keep_nan(*v, abs_size),
        }
    }

    fn eval_walking_height(&self, height: &Height) -> f32 {
        match height {
            Height::Compute => panic!("Walking with Height:Compute is not supported"),
            Height::Fix(v) => v.max(0.),
            Height::Fill => self.get_height_left(),
            Height::FillUntil(v) => self.get_height_left().min(*v),
        }
    }

    fn eval_available_height(&self, height: &Height, absolute: bool, abs_size: Vec2) -> f32 {
        if absolute {
            return abs_size.y;
        }
        // Non-absolute layouts will always have parents
        let parent = self.layout_boxes.last().unwrap();
        match height {
            Height::Fix(v) => *v,
            Height::FillUntil(v) => parent.get_height_left().min(*v),
            Height::Compute | Height::Fill => parent.get_height_left(),
        }
    }

    /// Add an `Area::InstanceRange` to the [`Cx::layout_box_align_list`], so that it will get aligned,
    /// e.g. when you call [`Cx::end_typed_box`].
    pub(crate) fn add_to_box_align_list(&mut self, area: Area) {
        match area {
            Area::InstanceRange(_) => self.layout_box_align_list.push(area),
            _ => panic!("Only Area::InstanceRange can be aligned currently"),
        }
    }
}

pub(crate) fn max_zero_keep_nan(v: f32) -> f32 {
    if v.is_nan() {
        v
    } else {
        f32::max(v, 0.0)
    }
}

pub(crate) fn min_keep_nan(a: f32, b: f32) -> f32 {
    if a.is_nan() || b.is_nan() {
        f32::NAN
    } else {
        f32::min(a, b)
    }
}