Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/yakui-core/src/geometry/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,14 @@ impl Rect {

/// Tells whether two rectangles intersect.
///
/// If the rectangles touch but do not overlap, they are considered **not
/// intersecting**.
/// If the rectangles touch but do not overlap, they are still considered **intersecting**.
#[inline]
pub fn intersects(&self, other: &Self) -> bool {
let self_max = self.max();
let other_max = other.max();

let x_intersect = self.pos.x < other_max.x && self_max.x > other.pos.x;
let y_intersect = self.pos.y < other_max.y && self_max.y > other.pos.y;
let x_intersect = self.pos.x <= other_max.x && self_max.x >= other.pos.x;
let y_intersect = self.pos.y <= other_max.y && self_max.y >= other.pos.y;

x_intersect && y_intersect
}
Expand Down
7 changes: 3 additions & 4 deletions crates/yakui-core/src/geometry/urect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,14 @@ impl URect {

/// Tells whether two rectangles intersect.
///
/// If the rectangles touch but do not overlap, they are considered **not
/// intersecting**.
/// If the rectangles touch but do not overlap, they are still considered **intersecting**.
#[inline]
pub fn intersects(&self, other: &Self) -> bool {
let self_max = self.max();
let other_max = other.max();

let x_intersect = self.pos.x < other_max.x && self_max.x > other.pos.x;
let y_intersect = self.pos.y < other_max.y && self_max.y > other.pos.y;
let x_intersect = self.pos.x <= other_max.x && self_max.x >= other.pos.x;
let y_intersect = self.pos.y <= other_max.y && self_max.y >= other.pos.y;

x_intersect && y_intersect
}
Expand Down
9 changes: 1 addition & 8 deletions crates/yakui-core/src/input/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,6 @@ impl InputState {

/// Signal that the mouse has moved.
fn mouse_moved(&self, dom: &Dom, layout: &LayoutDom, pos: Option<Vec2>) {
let pos = pos.map(|pos| pos - layout.unscaled_viewport().pos());

{
let mut mouse = self.mouse.borrow_mut();
mouse.position = pos;
Expand Down Expand Up @@ -553,12 +551,7 @@ fn hit_test(_dom: &Dom, layout: &LayoutDom, coords: Vec2, output: &mut Vec<Widge
continue;
};

let mut rect = layout_node.rect;
let mut node = layout_node;
while let Some(parent) = node.clipped_by {
node = layout.get(parent).unwrap();
rect = rect.constrain(node.rect);
}
let rect = layout_node.clip.constrain(layout_node.rect);

if rect.contains_point(coords) {
output.push(id);
Expand Down
126 changes: 126 additions & 0 deletions crates/yakui-core/src/layout/clipping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use glam::Vec2;

use crate::geometry::Rect;

#[derive(Debug, Clone, Copy)]
pub(crate) struct ClipResolutionArgs {
pub parent_clip: Rect,
pub parent_rect: Rect,
pub layout_rect: Rect,
pub viewport: Rect,
pub offset: Vec2,
}

/// Defines abstract sources of [`Rect`] for clipping resolution.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AbstractClipRect {
/// Represents the parent widget's clipping rect.
ParentClip,
/// Represents the parent widget's layout rect.
ParentRect,
/// Represents the current widget's layout rect.
LayoutRect,
/// Represents the entire viewport rect.
Viewport,
/// The provided rect.
Value(Rect),
}

impl AbstractClipRect {
/// Turns the [`AbstractClipRect`] into a concrete [`Rect`].
pub(crate) fn to_rect(
self,
ClipResolutionArgs {
parent_clip,
parent_rect,
layout_rect,
viewport,
offset,
}: ClipResolutionArgs,
) -> Rect {
let mut rect = match self {
AbstractClipRect::LayoutRect => layout_rect,
AbstractClipRect::Value(rect) => rect,
// don't offset these
AbstractClipRect::ParentClip => return parent_clip,
AbstractClipRect::ParentRect => return parent_rect,
AbstractClipRect::Viewport => return viewport,
};

rect.set_pos(rect.pos() + offset);

rect
}
}

/// Defines the clipping logic of clipping resolution.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ClipLogic {
/// No clipping logic is performed. The widget will simply reuse the parent's clipping rect.
#[default]
Pass,
/// The clipping rect will be the [`constrained`][Rect::constrain] result of the two rects.
Constrain(AbstractClipRect, AbstractClipRect),
/// If the widget is out of the [`parent`][ClipLogic::Contain::parent]'s bounds, try to push it back in first.
/// Optionally add an offset to the widget first.
Contain {
/// The rect of the supposed widget in question.
it: AbstractClipRect,
/// The rect of the supposed widget's parent.
parent: AbstractClipRect,
/// An optional pixel offset to apply to [`it`][ClipLogic::Contain::it] before containing it in the [`parent`][ClipLogic::Contain::parent]'s bounds.
offset: Vec2,
},
/// The clipping rect will be overridden to be the provided rect.
Override(AbstractClipRect),
}

impl ClipLogic {
/// Resolves the *layout rect* with the provided [`ClipLogic`].
///
/// Should be calculated before doing clipping rect resolution.
pub(crate) fn resolve_layout(
self,
args @ ClipResolutionArgs { viewport, .. }: ClipResolutionArgs,
) -> Rect {
match self {
ClipLogic::Contain { it, parent, offset } => {
let it = it.to_rect(ClipResolutionArgs {
offset: args.offset + offset,
..args
});

// implicitly also constrain to viewport
let parent = parent.to_rect(args).constrain(viewport);

let ratio = Vec2::max(it.size() / parent.size() - Vec2::ONE, Vec2::ONE);

let offset_min = Vec2::max(parent.pos() - it.pos(), Vec2::ZERO) * ratio;
let offset_max = Vec2::max(it.max() - parent.max(), Vec2::ZERO) * ratio;

Rect::from_pos_size(it.pos() + (-offset_max + offset_min), it.size())
}
_ => AbstractClipRect::LayoutRect.to_rect(args),
}
}

/// Resolves the *clipping rect* with the provided [`ClipLogic`].
pub(crate) fn resolve_clip(
self,
args @ ClipResolutionArgs { parent_clip, .. }: ClipResolutionArgs,
) -> Rect {
match self {
ClipLogic::Pass => parent_clip,
ClipLogic::Constrain(a, b)
| ClipLogic::Contain {
it: a, parent: b, ..
} => {
let a = a.to_rect(args);
let b = b.to_rect(args);

a.constrain(b)
}
ClipLogic::Override(rect) => rect.to_rect(args),
}
}
}
114 changes: 86 additions & 28 deletions crates/yakui-core/src/layout/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Defines yakui's layout protocol and Layout DOM.

mod clipping;

pub use self::clipping::*;

use std::collections::VecDeque;

use glam::Vec2;
Expand All @@ -18,12 +22,13 @@ use crate::widget::LayoutContext;
#[derive(Debug)]
pub struct LayoutDom {
nodes: Arena<LayoutDomNode>,
clip_stack: Vec<WidgetId>,

unscaled_viewport: Rect,
scale_factor: f32,

pub(crate) interest_mouse: MouseInterest,

clip_logic_overrides: Arena<ClipLogic>,
}

/// A node in a [`LayoutDom`].
Expand All @@ -32,16 +37,13 @@ pub struct LayoutDomNode {
/// The bounding rectangle of the node in logical pixels.
pub rect: Rect,

/// This node will clip its descendants to its bounding rectangle.
pub clipping_enabled: bool,
/// The clipping rectangle of the node in logical pixels.
pub clip: Rect,

/// This node is the beginning of a new layer, and all of its descendants
/// should be hit tested and painted with higher priority.
pub new_layer: bool,

/// This node is clipped to the region defined by the given node.
pub clipped_by: Option<WidgetId>,

/// What events the widget reported interest in.
pub event_interest: EventInterest,
}
Expand All @@ -51,12 +53,12 @@ impl LayoutDom {
pub fn new() -> Self {
Self {
nodes: Arena::new(),
clip_stack: Vec::new(),

unscaled_viewport: Rect::ONE,
scale_factor: 1.0,

interest_mouse: MouseInterest::new(),
clip_logic_overrides: Arena::new(),
}
}

Expand Down Expand Up @@ -119,13 +121,14 @@ impl LayoutDom {
profiling::scope!("LayoutDom::calculate_all");
log::debug!("LayoutDom::calculate_all()");

self.clip_stack.clear();
self.interest_mouse.clear();
self.clip_logic_overrides.clear();

let constraints = Constraints::tight(self.viewport().size());

self.calculate(dom, input, paint, dom.root(), constraints);
self.resolve_positions(dom);
self.resolve_clipping(dom);
}

/// Calculate the layout of a specific widget.
Expand Down Expand Up @@ -170,40 +173,37 @@ impl LayoutDom {
self.interest_mouse.pop_layer();
}

// If the widget called enable_clipping() during layout, it will be on
// top of the clip stack at this point.
let clipping_enabled = self.clip_stack.last() == Some(&id);

// If this node enabled clipping, the next node under that is the node
// that clips this one.
let clipped_by = if clipping_enabled {
self.clip_stack.iter().nth_back(2).copied()
} else {
self.clip_stack.last().copied()
};

self.nodes.insert_at(
id.index(),
LayoutDomNode {
rect: Rect::from_pos_size(Vec2::ZERO, size),
clipping_enabled,
clip: Rect::ZERO,
new_layer,
clipped_by,
event_interest,
},
);

if clipping_enabled {
self.clip_stack.pop();
}

dom.exit(id);
size
}

/// Sets the clipping logic for the currently active widget.
pub fn set_clip_logic(&mut self, dom: &Dom, logic: ClipLogic) {
self.clip_logic_overrides
.insert_at(dom.current().index(), logic);
}

/// Enables clipping for the currently active widget.
pub fn enable_clipping(&mut self, dom: &Dom) {
self.clip_stack.push(dom.current());
self.set_clip_logic(
dom,
ClipLogic::Constrain(AbstractClipRect::ParentClip, AbstractClipRect::LayoutRect),
);
}

/// Escapes clipping from the current clipping rect for the currently active widget.
pub fn escape_clipping(&mut self, dom: &Dom) {
self.set_clip_logic(dom, ClipLogic::Override(AbstractClipRect::Viewport));
}

/// Put this widget and its children into a new layer.
Expand All @@ -221,7 +221,7 @@ impl LayoutDom {
fn resolve_positions(&mut self, dom: &Dom) {
let mut queue = VecDeque::new();

queue.push_back((dom.root(), Vec2::ZERO));
queue.push_back((dom.root(), self.viewport().pos()));

while let Some((id, parent_pos)) = queue.pop_front() {
if let Some(layout_node) = self.nodes.get_mut(id.index()) {
Expand All @@ -234,4 +234,62 @@ impl LayoutDom {
}
}
}

fn resolve_clipping(&mut self, dom: &Dom) {
let viewport = self.viewport();

let mut queue = VecDeque::new();

queue.push_back((dom.root(), viewport, viewport, Vec2::ZERO));

while let Some((id, parent_clip, parent_rect, mut offset)) = queue.pop_front() {
let Some(layout_node) = self.nodes.get_mut(id.index()) else {
continue;
};
let node = dom.get(id).unwrap();

let logic = self
.clip_logic_overrides
.get(id.index())
.copied()
.unwrap_or(ClipLogic::Pass);

let mut layout_rect = layout_node.rect;

// We need to do this before the clipping rect resolution.
// We might change the layout rect here, and that might change what the clipping should be.
let new_layout_rect = logic.resolve_layout(ClipResolutionArgs {
parent_clip,
parent_rect,
layout_rect,
viewport,
offset,
});

// Calculate the new offset with the new layout rect,
// and resize our `layout_rect`, but keep its position the same.
// The position will be correct in clip resolution because we'll use the new offset.
offset = new_layout_rect.pos() - layout_node.rect.pos();
layout_rect.set_size(new_layout_rect.size());

// We don't want to use `new_layout_rect` here!
// Instead use the new offset to ensure the calculations are correct with stuff like `AbstractClipRect::Value`.
let new_clip_rect = logic.resolve_clip(ClipResolutionArgs {
parent_clip,
parent_rect,
layout_rect,
viewport,
offset,
});

layout_node.rect = new_layout_rect;
layout_node.clip = new_clip_rect;

queue.extend(
node.children
.iter()
.map(|&id| (id, layout_node.clip, layout_node.rect, offset)),
);
}
}
}
Loading
Loading