wrflib 0.0.3

🐢⚡ Wrflib is a fast, cross-platform (web+native), GPU-based application framework, written in Rust.
Documentation
// Copyright (c) 2021-present, Cruise LLC
//
// This source code is licensed under the Apache License, Version 2.0,
// found in the LICENSE-APACHE file in the root directory of this source tree.
// You may not use this file except in compliance with the License.

//! The main primitives for rendering to the screen.
//!
//! A draw tree has two kinds of nodes: [`View`] and [`DrawCall`]. It might look like this:
//! * `View` - root
//!   * `DrawCall` - rendering some text
//!   * `DrawCall` - rendering some buttons
//!   * `View` - scrollable container
//!     * `DrawCall` - rendering some more buttons
//!     * `DrawCall` - rendering some more text
//!   * `DrawCall` - maybe even more buttons
//!
//! [`DrawCall`]s contain the actual data that needs to be drawn to the screen.
//!
//! [`View`]s are groups of draw calls, have some special features, such as
//! scrolling/clipping.
//!
//! Note that one level higher, we have a hierarchy of [`Pass`]es.

use crate::*;

use crate::Debugger;

/// A group of [`DrawCall`]s.
///
/// This is really a pointer to a [`CxView`] (indexed in [`Cx::views`] using [`View::view_id`]),
/// so you can find more information there.
///
/// A [`View`] has a few special features:
/// * It has its own [`Rect`], within which everything is clipped (see also [`DrawUniforms`]).
///   This typically gets set by the return value of [`Cx::end_typed_box`] for the [`CxLayoutBox`] that
///   is associated with the [`View`]. TODO(JP): Look into decoupling [`CxLayoutBox`] from [`View`].
/// * It can scroll (but does not have to; again see also [`DrawUniforms`]).
/// * It has its own set of [`DrawCall`]s, which are isolated from the [`DrawCall`]s of the
///   parent [`View`].
///
/// See also [`ViewArea`], which is an [`Area`] pointer to a [`View`].
#[derive(Clone, Default)]
pub struct View {
    /// The index of the corresponding [`CxView`] in [`Cx::views`].
    pub view_id: Option<usize>,
    /// The most recent [`Cx::redraw_id`] that this [`View`] was drawn with.
    pub(crate) redraw_id: u64,
    /// Whether this [`View`] is an overlay/popup, which means all [`DrawCall`]s underneath it
    /// will get rendered last.
    pub(crate) is_overlay: bool,

    debugger: Debugger,
}

impl View {
    /// Creates a new, empty [`View`].

    /// See [`View::is_overlay`].
    #[must_use]
    pub fn with_is_overlay(self, is_overlay: bool) -> Self {
        Self { is_overlay, ..self }
    }

    /// Register the [`View`] in the draw tree.
    ///
    /// This also creates a new [`CxLayoutBox`] with the [`LayoutSize`] that is passed in.
    /// Note that you should not create a [`View`] just in order to get a new
    /// [`CxLayoutBox`], since creating a [`View`] is relatively expensive -- no
    /// [`DrawCall`]s inside this [`View`] will get merged with ones outside of
    /// it, so adding too many [`View`]s will create too many individual calls to
    /// the GPU.
    ///
    /// TODO(JP): Perhaps we should decouple [`CxLayoutBox`] and [`View`] altogether?
    pub fn begin_view(&mut self, cx: &mut Cx, layout_size: LayoutSize) {
        self.begin_view_with_layout(cx, Layout { direction: Direction::Down, layout_size, ..Layout::default() });
    }

    fn begin_view_with_layout(&mut self, cx: &mut Cx, layout: Layout) {
        if !cx.in_redraw_cycle {
            panic!("calling begin_view outside of redraw cycle is not possible!");
        }
        assert!(cx.shader_group_instance_offsets.is_empty(), "Can't use begin_view inside a shader group");

        // check if we have a pass id parent
        let pass_id = *cx.pass_stack.last().expect("No pass found when begin_view");

        if self.view_id.is_none() {
            // we need a draw_list_id
            self.view_id = Some(cx.views.len());
            cx.views.push(CxView::default());
            let cxview = &mut cx.views[self.view_id.unwrap()];
            cxview.redraw_id = cx.redraw_id;
            cxview.pass_id = pass_id;
        }

        let view_id = self.view_id.unwrap();

        let (override_layout, is_root_for_pass) = if cx.passes[pass_id].main_view_id.is_none() {
            // we are the first view on a window
            let cxpass = &mut cx.passes[pass_id];
            cxpass.main_view_id = Some(view_id);
            // we should take the window geometry and abs position as our box layout
            (Layout { absolute: true, abs_size: Some(cxpass.pass_size), ..layout }, true)
        } else {
            (layout, false)
        };

        let cxpass = &mut cx.passes[pass_id];
        // find the parent draw list id
        let parent_view_id = if self.is_overlay {
            if cxpass.main_view_id.is_none() {
                panic!("Cannot make overlay inside window without root view")
            };

            cxpass.main_view_id.unwrap()
        } else if is_root_for_pass {
            view_id
        } else if let Some(last_view_id) = cx.view_stack.last() {
            *last_view_id
        } else {
            // we have no parent
            view_id
        };

        // push ourselves up the parent draw_stack
        if view_id != parent_view_id {
            // we need a new draw
            let parent_cxview = &mut cx.views[parent_view_id];

            let id = parent_cxview.draw_calls_len;
            parent_cxview.draw_calls_len += 1;

            // see if we need to add a new one
            if parent_cxview.draw_calls_len > parent_cxview.draw_calls.len() {
                parent_cxview.draw_calls.push({
                    DrawCall {
                        view_id: parent_view_id,
                        draw_call_id: parent_cxview.draw_calls.len(),
                        redraw_id: cx.redraw_id,
                        sub_view_id: view_id,
                        ..Default::default()
                    }
                })
            } else {
                // or reuse a sub list node
                let draw = &mut parent_cxview.draw_calls[id];
                draw.sub_view_id = view_id;
                draw.redraw_id = cx.redraw_id;
            }
        }

        // TODO(JP): Do we want to keep this? We don't really use this for anything except as a
        // convenience. I talked with Rik about redrawing of [`View`]s, and one idea was to always
        // fully invalidate the closest [`View`] parent that did not have a [`Layout`] with
        // [`Width::Compute`] or [`Height::Compute`], but that seems to fragile to me. It would be
        // better to check if a [`CxView::rect`] actually changed and in that case trigger a redraw
        // or even a panic (with some way of manually overriding the panic). So anyway, I think we
        // should strive to remove this after all.
        cx.begin_typed_box(CxBoxType::View, override_layout);

        // prepare drawlist for drawing
        let cxview = &mut cx.views[view_id];

        // TODO(JP): We don't seem to currently support moving a `View` to a different pass. Do we
        // want to?
        assert_eq!(cxview.pass_id, pass_id);

        // update drawlist ids
        self.redraw_id = cx.redraw_id;
        cxview.redraw_id = cx.redraw_id;
        cxview.draw_calls_len = 0;

        cx.view_stack.push(view_id);

        if is_root_for_pass {
            cx.passes[pass_id].paint_dirty = true;
        }
    }

    fn is_main_view(view_id: usize, cx: &mut Cx) -> bool {
        if let Some(window_id) = cx.window_stack.last() {
            if let Some(main_pass_id) = cx.windows[*window_id].main_pass_id {
                let pass_id = cx.views[view_id].pass_id;
                let cxpass = &cx.passes[pass_id];
                if let Some(main_view_id) = cxpass.main_view_id {
                    if main_view_id == view_id && main_pass_id == pass_id {
                        // we are the main view of a main pass of a window
                        return true;
                    }
                }
            }
        }
        false
    }

    /// End the [`View`], by ending the [`CxLayoutBox`]. Returns a [`ViewArea`] that
    /// you can hold onto.
    ///
    /// Should only be called if [`View::begin_view`] returned [`Result::Ok`].
    ///
    /// TODO(JP): Is the [`ViewArea`] redundant, since it basically contains the
    /// same information as the [`View`] itself?
    pub fn end_view(&mut self, cx: &mut Cx) -> Area {
        assert!(cx.shader_group_instance_offsets.is_empty(), "Can't use end_view inside a shader group");

        let view_id = self.view_id.unwrap();

        if cx.debug_flags.enable_layout_debugger && View::is_main_view(view_id, cx) {
            self.debugger.draw(cx);
        }

        let view_area = Area::View(ViewArea { view_id, redraw_id: cx.redraw_id });
        // Make sure that ViewArea would also be aligned when underlying calls getting moved
        cx.layout_box_align_list.push(view_area);

        let rect = cx.end_typed_box(CxBoxType::View);
        cx.views[view_id].rect = rect;
        cx.view_stack.pop();
        view_area
    }

    /// Get the [`Rect`] that the [`CxLayoutBox`] associated with the [`View`]
    /// returned.
    ///
    /// TODO(JP): Should we return an [`Option<Rect>`] instead of just
    /// returning a zero-sized [`Rect`] when the [`View`] has never been
    /// drawn yet?
    ///
    /// TODO(JP): Doesn't check if the [`View::redraw_id`] is still up to
    /// date, so we might be returning an outdated [`Rect`] here.
    pub fn get_rect(&self, cx: &Cx) -> Rect {
        if let Some(view_id) = self.view_id {
            let cxview = &cx.views[view_id];
            return cxview.rect;
        }
        Rect::default()
    }

    /// Returns an [`Area::View`] for this [`View`], or [`Area::Empty`] if the
    /// [`View`] hasn't been instantiated yet.
    pub fn area(&self) -> Area {
        if let Some(view_id) = self.view_id {
            Area::View(ViewArea { view_id, redraw_id: self.redraw_id })
        } else {
            Area::Empty
        }
    }

    /// Get the current [`CxView::unsnapped_scroll`] if the [`View`] has been
    /// instantiated.
    pub fn get_scroll_pos(&self, cx: &Cx) -> Vec2 {
        if let Some(view_id) = self.view_id {
            let cxview = &cx.views[view_id];
            cxview.unsnapped_scroll
        } else {
            Vec2::default()
        }
    }
}

impl Cx {
    /// Returns an existing [`DrawCall`] based on the given [`Shader`], or
    /// creates a new one if none can be found in the current [`CxView`].
    ///
    /// Reuses an existing [`DrawCall`] if [`CxView::draw_calls_len`] is less than
    /// [`CxView::draw_calls.len()`], so we can reuse existing GPU resources.
    ///
    /// TODO(JP): It's unclear to me if the reusing of GPU resources in this way
    /// is beneficial. And if it is, if it should instead be done in the
    /// platform-specific code instead.
    fn create_draw_call(&mut self, shader_id: usize, props: DrawCallProps) -> &mut DrawCall {
        assert!(self.in_redraw_cycle, "Must be in redraw cycle to append to draw calls");

        let sh = &self.shaders[shader_id];

        let current_view_id = *self.view_stack.last().unwrap();
        let cxview = &mut self.views[current_view_id];
        let draw_call_id = cxview.draw_calls_len;

        // Find a draw call to append to.
        if props.is_batchable() {
            let shader_group_size = self.shader_group_instance_offsets.len();
            if shader_group_size > 0 {
                // If we're in a shader group then the given shader must be part of the group, so we just
                // search within the group.
                assert!(cxview.draw_calls_len >= shader_group_size);
                for index in cxview.draw_calls_len - shader_group_size..cxview.draw_calls_len {
                    if cxview.draw_calls[index].shader_id == shader_id {
                        return &mut cxview.draw_calls[index];
                    }
                }
                panic!("Trying to use Shader within a shader group that isn't part of the group");
            } else {
                // Do the most basic of [`DrawCall`] batching, by checking if the very last [`DrawCall`] matches
                // the shader that we're drawing, and if so, appending to that.
                if cxview.draw_calls_len > 0 && !self.debug_flags.disable_draw_call_batching {
                    let dc = &mut cxview.draw_calls[cxview.draw_calls_len - 1];
                    if dc.props.is_batchable() && dc.sub_view_id == 0 && dc.shader_id == shader_id {
                        return &mut cxview.draw_calls[cxview.draw_calls_len - 1];
                    }
                }
            }
        }

        // add one
        cxview.draw_calls_len += 1;

        // see if we need to add a new one
        if draw_call_id >= cxview.draw_calls.len() {
            cxview.draw_calls.push(DrawCall {
                props,
                draw_call_id,
                view_id: current_view_id,
                redraw_id: self.redraw_id,
                sub_view_id: 0,
                shader_id,
                instances: Vec::new(),
                draw_uniforms: DrawUniforms::default(),
                user_uniforms: {
                    let mut f = Vec::new();
                    f.resize(sh.mapping.user_uniform_props.total_slots, 0.0);
                    f
                },
                textures_2d: {
                    let mut f = Vec::new();
                    f.resize(sh.mapping.textures.len(), 0);
                    f
                },
                //current_instance_offset: 0,
                instance_dirty: true,
                uniforms_dirty: true,
                platform: CxPlatformDrawCall::default(),
            });
            let dc = &mut cxview.draw_calls[draw_call_id];
            return dc;
        }
        // reuse an older one, keeping all GPU resources attached
        let dc = &mut cxview.draw_calls[draw_call_id];
        dc.shader_id = shader_id;
        dc.props = props;
        dc.sub_view_id = 0; // make sure its recognised as a draw call
                            // truncate buffers and set update frame
        dc.redraw_id = self.redraw_id;
        dc.instances.truncate(0);
        dc.user_uniforms.truncate(0);
        dc.user_uniforms.resize(sh.mapping.user_uniform_props.total_slots, 0.0);
        dc.textures_2d.truncate(0);
        dc.textures_2d.resize(sh.mapping.textures.len(), 0);
        dc.instance_dirty = true;
        dc.uniforms_dirty = true;
        dc
    }

    /// Add a slice of instances to [`DrawCall::instances`]. See [`Cx::add_instances`].
    fn add_instances_internal<T: Sized>(&mut self, shader: &'static Shader, data: &[T], props: DrawCallProps) -> Area {
        if data.is_empty() {
            // This is important, because otherwise you can call this function with empty data in order to force
            // a particular ordering of `DrawCall`s, and then depend on batching of `DrawCall`s. That should
            // generally be avoided -- we might change how `DrawCall` batching works in the future.
            return Area::Empty;
        }
        let shader_id = self.get_shader_id(shader);
        let total_instance_slots = self.shaders[shader_id].mapping.instance_props.total_slots;
        assert_eq!(total_instance_slots * std::mem::size_of::<f32>(), std::mem::size_of::<T>());
        let dc = self.create_draw_call(shader_id, props);
        let ia = InstanceRangeArea {
            view_id: dc.view_id,
            draw_call_id: dc.draw_call_id,
            instance_count: data.len(),
            instance_offset: dc.instances.len(),
            redraw_id: dc.redraw_id,
        };
        dc.instances.extend_from_slice(data.as_f32_slice());
        let area = Area::InstanceRange(ia);
        self.add_to_box_align_list(area);
        area
    }

    /// Add a slice of instances to [`DrawCall::instances`].
    ///
    /// Supports appending any data that has the correct size.
    ///
    /// You should assume that any call to this creates a new [`DrawCall`], even though in reality we might
    /// batch certain [`DrawCall`]s. For more information about [`DrawCall`] batching, see [`Shader`].
    ///
    /// Uses [`Cx::create_draw_call`] under the hood to find the [`DrawCall`]
    /// to add to.
    pub fn add_instances<T: Sized>(&mut self, shader: &'static Shader, data: &[T]) -> Area {
        self.add_instances_internal(shader, data, DrawCallProps::default())
    }

    /// Add a slice of instances while specifying a custom Geometry
    pub fn add_mesh_instances<T: Sized>(&mut self, shader: &'static Shader, data: &[T], gpu_geometry: GpuGeometry) -> Area {
        assert!(self.shader_group_instance_offsets.is_empty(), "Can't add mesh instances when in a shader group");

        self.add_instances_internal(shader, data, DrawCallProps { gpu_geometry: Some(gpu_geometry), ..Default::default() })
    }

    /// By default, [`DrawCall`] gets horizontal and vertical scrolling applied to
    /// its uniforms, but you can disable that by calling this method. This only
    /// applies to scrolling from its direct parent [`View`].
    ///
    /// This always creates a new [`DrawCall`]; no batching ever happens when using
    /// sticky scrolling.
    ///
    /// TODO(JP): The fact that this only applies to the direct parent [`View`]
    /// makes it so you can't just arbitrarily wrap [`DrawCall`]s inside [`View`]s,
    /// which is somewhat unexpected. It might be better to have this apply to
    /// the nearest `wrflib_widget::ScrollView` instead?
    ///
    /// TODO(JP): Do we need to track as fields on [`DrawCall`]? The same behavior
    /// can also be accomplished by overriding the [`Shader`]'s `scroll`
    /// function, by doing `draw_scroll - draw_local_scroll`. It's not as
    /// convenient, but then again, it might not be used very often, and it would
    /// encourage people to do more stuff in shaders.
    pub fn add_instances_with_scroll_sticky<T: Sized>(
        &mut self,
        shader: &'static Shader,
        data: &[T],
        horizontal: bool,
        vertical: bool,
    ) -> Area {
        assert!(self.shader_group_instance_offsets.is_empty(), "Can't add instances with scroll sticky when in a shader group");

        self.add_instances_internal(
            shader,
            data,
            DrawCallProps { scroll_sticky_horizontal: horizontal, scroll_sticky_vertical: vertical, ..Default::default() },
        )
    }

    /// Start a "shader group", which is a group of [`Shader`]s that will always be drawn in
    /// the same order.
    ///
    /// When drawing the same "shader group" multiple times in a row, the existing [`DrawCall`]s
    /// will be reused (batched) instead of new ones being created.
    ///
    /// For example, calling `cx.begin_shader_group(&[&FOO_SHADER, &BAR_SHADER]);` will guarantee
    /// that for that shader group exactly two [`DrawCall`]s will be created, with `FOO_SHADER`
    /// being always drawn first.
    pub fn begin_shader_group(&mut self, shaders_ordered: &[&'static Shader]) {
        assert!(self.in_redraw_cycle, "Must be in redraw cycle to call begin_shader_group");
        assert!(self.shader_group_instance_offsets.is_empty(), "Nested shader groups are not supported (yet)");

        let shader_ids: Vec<usize> = shaders_ordered.iter().map(|&shader| self.get_shader_id(shader)).collect();

        // Make sure shaders are unique.
        debug_assert!(
            {
                let mut unique_shader_ids = shader_ids.clone();
                unique_shader_ids.sort_unstable();
                unique_shader_ids.dedup();
                shader_ids.len() == unique_shader_ids.len()
            },
            "Can't use shader more than once in shader group"
        );

        let shader_group_size = shaders_ordered.len();
        let current_view_id = *self.view_stack.last().unwrap();
        let cxview = &self.views[current_view_id];

        // We have to hold the following invariant: if shader_group_instance_offsets is not empty, then the last
        // set of `DrawCall`s in the current `CxView` have to match exactly the `shaders_ordered`. If that
        // invariant already holds (e.g. if the exact same shader group was previously used) then we can skip
        // creating new `DrawCall`s.
        if self.debug_flags.disable_draw_call_batching
            || cxview.draw_calls_len < shader_group_size
            || shader_ids.iter().enumerate().any(|(index, &shader_id)| {
                let dc = &cxview.draw_calls[cxview.draw_calls_len - shader_group_size + index];
                dc.shader_id != shader_id || dc.sub_view_id != 0
            })
        {
            for shader_id in shader_ids {
                self.create_draw_call(shader_id, DrawCallProps::default());
            }
        }

        // Since `shader_group_instance_offsets` is empty (see assertion above) we can just extend with an iterator.
        let cxview = &self.views[current_view_id];
        self.shader_group_instance_offsets.extend(
            (cxview.draw_calls_len - shader_group_size..cxview.draw_calls_len)
                .map(|index| cxview.draw_calls[index].instances.len()),
        );
    }

    /// End a "shader group". See [`Cx::begin_shader_group`].
    pub fn end_shader_group(&mut self) {
        assert!(!self.shader_group_instance_offsets.is_empty(), "Call begin_shader_group before end_shader_group");
        self.shader_group_instance_offsets.clear();
    }

    /// Sets the horizontal scroll position for a [`View`]/[`CxView`].
    pub fn set_view_scroll_x(&mut self, view_id: usize, scroll_pos: f32) {
        let fac = self.get_delegated_dpi_factor(self.views[view_id].pass_id);
        let cxview = &mut self.views[view_id];
        cxview.unsnapped_scroll.x = scroll_pos;
        let snapped = scroll_pos - scroll_pos % (1.0 / fac);
        if cxview.snapped_scroll.x != snapped {
            cxview.snapped_scroll.x = snapped;
            self.passes[cxview.pass_id].paint_dirty = true;
        }
    }

    /// Sets the vertical scroll position for a [`View`]/[`CxView`].
    pub fn set_view_scroll_y(&mut self, view_id: usize, scroll_pos: f32) {
        let fac = self.get_delegated_dpi_factor(self.views[view_id].pass_id);
        let cxview = &mut self.views[view_id];
        cxview.unsnapped_scroll.y = scroll_pos;
        let snapped = scroll_pos - scroll_pos % (1.0 / fac);
        if cxview.snapped_scroll.y != snapped {
            cxview.snapped_scroll.y = snapped;
            self.passes[cxview.pass_id].paint_dirty = true;
        }
    }
}

/// Hardcoded set of uniforms that are present on every [`DrawCall`].
///
/// TODO(JP): Should we just use [`Vec4`]s and [`Vec2`] here instead of individual
/// [`f32`]s?
#[derive(Default, Clone)]
#[repr(C, align(8))]
pub(crate) struct DrawUniforms {
    /// Clip region top left x-position.
    draw_clip_x1: f32,
    /// Clip region top left y-position.
    draw_clip_y1: f32,
    /// Clip region bottom right x-position.
    draw_clip_x2: f32,
    /// Clip region bottom right y-position.
    draw_clip_y2: f32,
    /// The total horizontal scroll offset, including all its parents.
    pub(crate) draw_scroll_x: f32,
    /// The total vertical scroll offset, including all its parents.
    pub(crate) draw_scroll_y: f32,
    /// The horizontal scroll offset of just the containing [`View`].
    pub(crate) draw_local_scroll_x: f32,
    /// The vertical scroll offset of just the containing [`View`].
    pub(crate) draw_local_scroll_y: f32,
    /// A small increment that you can add to the z-axis of your vertices, which is based on the
    /// position of the [`DrawCall`] in the draw tree.
    ///
    /// TODO(JP): Not entirely sure why we need this, given that we're already drawing everything
    /// in order?
    draw_zbias: f32,
}

impl DrawUniforms {
    /// Get as a raw `[f32]` slice.
    pub fn as_slice(&self) -> &[f32; std::mem::size_of::<DrawUniforms>()] {
        unsafe { std::mem::transmute(self) }
    }
}

/// Some user-defined props to initialize a [`DrawCall`] with.
#[derive(Default)]
pub(crate) struct DrawCallProps {
    /// The base [`Geometry`] object that will be used for generating the initial
    /// vertex locations for every instance, such as a rectangle or cube.
    /// This is currently only used when specifying custom meshes.
    pub(crate) gpu_geometry: Option<GpuGeometry>,
    /// See [`Cx::add_instances_with_scroll_sticky`].
    scroll_sticky_vertical: bool,
    /// See [`Cx::add_instances_with_scroll_sticky`].
    scroll_sticky_horizontal: bool,
}
impl DrawCallProps {
    /// Whether the draw call can be batched, or if a new one should be created.
    fn is_batchable(&self) -> bool {
        self.gpu_geometry.is_none() && !self.scroll_sticky_horizontal && !self.scroll_sticky_vertical
    }
}

/// This represents an actual call to the GPU, _or_ it can represent a
/// sub-[`View`], in case [`DrawCall::sub_view_id`] is set. Note that all of this behaves
/// completely differently if [`DrawCall::sub_view_id`] is set; all regular drawing fields
/// are ignored in that case!
///
/// TODO(JP): this sub-[`View`] behavior is confusing, and we should instead
/// split this out to something like [`enum DrawTreeItem { DrawCall(DrawCall),
/// NestedView(usize) }`] or so.
///
/// That said, for a regular [`DrawCall`], this contains all the information that
/// you need to make a draw call on the GPU: the [`Shader`], [`DrawCall::instances`],
/// [`DrawCall::draw_uniforms`], and so on.
///
/// It is always kept in [`CxView::draw_calls`], and as said, is part of a tree
/// structure, called the "draw tree". To print a textual representation of the
/// draw tree, use [`Cx::debug_flags_mut`].
#[derive(Default)]
pub struct DrawCall {
    /// The index of this [`DrawCall`] within its parent [`CxView::draw_calls`].
    pub(crate) draw_call_id: usize,
    /// The parent [`CxView`]/[`View`] that this [`DrawCall`] is a part of.
    pub(crate) view_id: usize,
    /// The [`Cx::redraw_id`] of the last time this [`DrawCall`] was accessed.
    pub(crate) redraw_id: u64,
    /// If not 0, this [`DrawCall`] actually represents a nested sub-[`View`].
    /// See [`DrawCall`] for a TODO on fixing this, because this is confusing!
    pub(crate) sub_view_id: usize,
    /// The actual [`Shader`] to use when drawing.
    pub(crate) shader_id: usize,
    /// The instance buffer that will be sent directly to the GPU.
    pub(crate) instances: Vec<f32>,
    /// Buffer of user-defined uniforms (in addition to the [`draw_uniforms`
    /// below.)
    pub(crate) user_uniforms: Vec<f32>,
    /// Buffer of texture IDs.
    pub(crate) textures_2d: Vec<u32>,
    /// Whether or not the draw call has been accessed since the last paint.
    /// Should currently always be the same as [`DrawCall::uniforms_dirty`] below.
    pub(crate) instance_dirty: bool,
    /// Whether or not the draw call has been accessed since the last paint.
    /// Should currently always be the same as [`DrawCall::instance_dirty`] above.
    pub(crate) uniforms_dirty: bool,
    /// Hardcoded set of uniforms that are present on every [`DrawCall`].
    pub(crate) draw_uniforms: DrawUniforms,
    /// Platform-specific data for use during painting.
    pub(crate) platform: CxPlatformDrawCall,
    pub(crate) props: DrawCallProps,
}

impl DrawCall {
    /// Set the scroll uniforms in [`DrawCall::draw_uniforms`], as computed when
    /// walking the draw tree during painting.
    pub(crate) fn set_local_scroll(&mut self, scroll: Vec2, local_scroll: Vec2) {
        self.draw_uniforms.draw_scroll_x = scroll.x;
        if !self.props.scroll_sticky_horizontal {
            self.draw_uniforms.draw_scroll_x += local_scroll.x;
        }
        self.draw_uniforms.draw_scroll_y = scroll.y;
        if !self.props.scroll_sticky_vertical {
            self.draw_uniforms.draw_scroll_y += local_scroll.y;
        }
        self.draw_uniforms.draw_local_scroll_x = local_scroll.x;
        self.draw_uniforms.draw_local_scroll_y = local_scroll.y;
    }

    /// Set the zbias in [`DrawCall::draw_uniforms`], as computed when
    /// walking the draw tree during painting.
    pub(crate) fn set_zbias(&mut self, zbias: f32) {
        self.draw_uniforms.draw_zbias = zbias;
    }

    /// Set the clip dimensions in [`DrawCall::draw_uniforms`], as computed when
    /// walking the draw tree during painting.
    pub(crate) fn set_clip(&mut self, clip: (Vec2, Vec2)) {
        self.draw_uniforms.draw_clip_x1 = clip.0.x;
        self.draw_uniforms.draw_clip_y1 = clip.0.y;
        self.draw_uniforms.draw_clip_x2 = clip.1.x;
        self.draw_uniforms.draw_clip_y2 = clip.1.y;
    }

    /// Get the actual position on the screen given the scroll and clip uniforms
    /// in [`DrawCall::draw_uniforms`].
    pub(crate) fn clip_and_scroll_rect(&self, x: f32, y: f32, w: f32, h: f32) -> Rect {
        let mut x1 = x - self.draw_uniforms.draw_scroll_x;
        let mut y1 = y - self.draw_uniforms.draw_scroll_y;
        let mut x2 = x1 + w;
        let mut y2 = y1 + h;
        x1 = self.draw_uniforms.draw_clip_x1.max(x1).min(self.draw_uniforms.draw_clip_x2);
        y1 = self.draw_uniforms.draw_clip_y1.max(y1).min(self.draw_uniforms.draw_clip_y2);
        x2 = self.draw_uniforms.draw_clip_x1.max(x2).min(self.draw_uniforms.draw_clip_x2);
        y2 = self.draw_uniforms.draw_clip_y1.max(y2).min(self.draw_uniforms.draw_clip_y2);
        Rect { pos: vec2(x1, y1), size: vec2(x2 - x1, y2 - y1) }
    }
}

/// Uniforms that can be set on the [`View`] that wraps a [`DrawCall`].
///
/// TODO(JP): Currently empty, but I can see this be potentially useful, so I left
/// the code around. Might want to either make use of this directly, or expose it
/// as something users can configure, or just remove altogether.
///  - This could potentially be used for adding transformations of many instances,
///    for example translating or rotating, similarly to ThreeJS's Group abstraction.
#[derive(Default, Clone)]
#[repr(C)]
pub struct ViewUniforms {}

impl ViewUniforms {
    pub fn as_slice(&self) -> &[f32; std::mem::size_of::<ViewUniforms>()] {
        unsafe { std::mem::transmute(self) }
    }
}

/// An actual instantiation of a [`View`]. It's a node in the draw tree with
/// children, which can be either [`DrawCall`]s or other [`View`]s.
///
/// Child [`View`]s are represented by [`DrawCall`]s that have [`DrawCall::sub_view_id`] set.
///
/// TODO(JP): this sub-[`View`] behavior is confusing, and we should instead
/// split out [`DrawCall`] into something like [`enum DrawTreeItem { DrawCall(DrawCall),
/// NestedView(usize) }`] or so.
///
/// See also [`View`] and [`ViewArea`].
#[derive(Default)]
pub struct CxView {
    /// The actual children, which are always [`DrawCall`] objects, but those can
    /// represent either actual draw calls or child [`CxView`]s (see [`DrawCall`] and
    /// [`CxView`] for more documentation).
    pub(crate) draw_calls: Vec<DrawCall>,
    /// The [`Rect`] of the [`CxLayoutBox`] that we created in [`View::begin_view`].
    ///
    /// TODO(JP): We want to decouple [`CxLayoutBox`] more from [`CxView`], so we have to
    /// figure out what to do with this. For [`View`]s that are actively used for
    /// scrolling and clipping, having this `rect` makes sense, but maybe not for
    /// other uses?
    pub(crate) rect: Rect,
    /// The [`Cx::redraw_id`] of the last time this [`CxView`] was drawn. Can be used
    /// to see if an [`ViewArea`] pointer is still valid.
    ///
    /// TODO(JP): There is no way to tell if a [`CxView`] is still part of the draw tree,
    /// since merely comparing [`CxView::redraw_id`] and [`Cx::redraw_id`] is not
    /// enough, since those can also be different if the [`CxView`] was simply not
    /// marked for redrawing recently. It would be good to have some way to clean up
    /// old [`CxView`]s.
    pub(crate) redraw_id: u64,
    /// The [`Pass`]/[`CxPass`] that this is part of.
    ///
    /// TODO(JP): What happens if you change this after instantiating a [`CxView`]?
    /// Does that even work? Should it be supported?
    pub(crate) pass_id: usize,
    /// The actual number of fields in [`CxView::draw_calls`] that we use, so we can keep
    /// GPU resources associated with each [`DrawCall`] associated even when not in use.
    ///
    /// TODO(JP): Is this actually useful? Is caching of resources like that worth it, or
    /// should we do it on a per-platform basis, and only where it's really necessary?
    pub(crate) draw_calls_len: usize,
    /// The cumulative scroll offset from all of the parents. Gets set during painting.
    pub(crate) parent_scroll: Vec2,
    /// See [`ViewUniforms`].
    pub(crate) view_uniforms: ViewUniforms,
    /// The actual scroll position, including fractional offsets.
    pub(crate) unsnapped_scroll: Vec2,
    /// The scroll position that gets snapped to actual pixel values (taking into account
    /// the device pixel ratio; called `dpi_factor` internally).
    pub(crate) snapped_scroll: Vec2,

    /// Platform-specific fields. Currently only used on Windows.
    #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
    pub(crate) platform: CxPlatformView,
}

impl CxView {
    /// Returns the intersection of clip coordinates and [`CxView::rect`], taking
    /// into account [`CxView::parent_scroll`].
    ///
    /// TODO(JP): Should this instead take and return a [`Rect`]?
    pub(crate) fn intersect_clip(&self, clip: (Vec2, Vec2)) -> (Vec2, Vec2) {
        let min_x = self.rect.pos.x - self.parent_scroll.x;
        let min_y = self.rect.pos.y - self.parent_scroll.y;
        let max_x = self.rect.pos.x + self.rect.size.x - self.parent_scroll.x;
        let max_y = self.rect.pos.y + self.rect.size.y - self.parent_scroll.y;

        (Vec2 { x: min_x.max(clip.0.x), y: min_y.max(clip.0.y) }, Vec2 { x: max_x.min(clip.1.x), y: max_y.min(clip.1.y) })
    }
}