287 lines
9.2 KiB
Rust
287 lines
9.2 KiB
Rust
//! Per-occurrence scroll state and a single geometry/clip projection shared by
|
|
//! native paint, source paint and input. No business object is copied.
|
|
use crate::{
|
|
document::Frame,
|
|
geometry::Point,
|
|
layout::{self, Size},
|
|
mount::{Mount, MountTable},
|
|
ui_bridge::{UiKind, UiNode},
|
|
};
|
|
use anyhow::{Result, bail};
|
|
use endor_core::{NativeContext, ObjectRef, Value, Vm, VmError, WeakObjectRef};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub enum Axis {
|
|
#[default]
|
|
Both,
|
|
Vertical,
|
|
Horizontal,
|
|
}
|
|
impl Axis {
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Self::Both => "both",
|
|
Self::Vertical => "vertical",
|
|
Self::Horizontal => "horizontal",
|
|
}
|
|
}
|
|
pub fn parse(name: &str) -> Result<Self, VmError> {
|
|
match name {
|
|
"both" => Ok(Self::Both),
|
|
"vertical" => Ok(Self::Vertical),
|
|
"horizontal" => Ok(Self::Horizontal),
|
|
_ => Err(VmError::InvalidArgument(
|
|
"ScrollBlock.axis expects both, vertical or horizontal",
|
|
)),
|
|
}
|
|
}
|
|
pub fn horizontal(self) -> bool {
|
|
self != Self::Vertical
|
|
}
|
|
pub fn vertical(self) -> bool {
|
|
self != Self::Horizontal
|
|
}
|
|
}
|
|
|
|
fn intersection(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
|
let x = a[0].max(b[0]);
|
|
let y = a[1].max(b[1]);
|
|
[
|
|
x,
|
|
y,
|
|
(a[0] + a[2]).min(b[0] + b[2]).max(x) - x,
|
|
(a[1] + a[3]).min(b[1] + b[3]).max(y) - y,
|
|
]
|
|
}
|
|
pub fn project(vm: &mut Vm, table: &MountTable, root: &UiNode, frame: &mut Frame) -> Result<()> {
|
|
let mut nodes = HashMap::new();
|
|
let mut work = vec![root];
|
|
while let Some(node) = work.pop() {
|
|
nodes.insert(node.block_id.as_str(), node);
|
|
work.extend(node.children().iter().map(|node| node.as_ref()));
|
|
}
|
|
let indices: HashMap<_, _> = frame
|
|
.mounts
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, m)| (m.id, i))
|
|
.collect();
|
|
let scrolling: Vec<_> = frame
|
|
.mounts
|
|
.iter()
|
|
.map(|m| matches!(nodes[m.block_id.as_str()].kind, UiKind::Scroll { .. }))
|
|
.collect();
|
|
let mut extent: Vec<_> = frame
|
|
.mounts
|
|
.iter()
|
|
.map(|m| {
|
|
let padding = match nodes[m.block_id.as_str()].kind {
|
|
UiKind::Scroll { padding, .. } => padding,
|
|
_ => 0.0,
|
|
};
|
|
(m.x + m.width - padding, m.y + m.height - padding)
|
|
})
|
|
.collect();
|
|
for i in (0..frame.mounts.len()).rev() {
|
|
let m = &frame.mounts[i];
|
|
if let Some(parent) = m.parent {
|
|
let parent = indices[&parent];
|
|
let e = if scrolling[i] {
|
|
(m.x + m.width, m.y + m.height)
|
|
} else {
|
|
extent[i]
|
|
};
|
|
extent[parent].0 = extent[parent].0.max(e.0);
|
|
extent[parent].1 = extent[parent].1.max(e.1);
|
|
}
|
|
}
|
|
let mut contexts = HashMap::<u64, (f32, f32, Option<[f32; 4]>)>::new();
|
|
frame.commands.clear();
|
|
frame.command_clips.clear();
|
|
for (i, m) in frame.mounts.iter_mut().enumerate() {
|
|
let (dx, dy, clip) = m
|
|
.parent
|
|
.and_then(|id| contexts.get(&id).copied())
|
|
.unwrap_or_default();
|
|
let padding = match nodes[m.block_id.as_str()].kind {
|
|
UiKind::Scroll { padding, .. } => padding,
|
|
_ => 0.0,
|
|
};
|
|
let axis = match nodes[m.block_id.as_str()].kind {
|
|
UiKind::Scroll { axis, .. } => axis,
|
|
_ => Axis::Both,
|
|
};
|
|
let limits = [
|
|
if axis.horizontal() {
|
|
(extent[i].0 + padding - m.x - m.width).max(0.0)
|
|
} else {
|
|
0.0
|
|
},
|
|
if axis.vertical() {
|
|
(extent[i].1 + padding - m.y - m.height).max(0.0)
|
|
} else {
|
|
0.0
|
|
},
|
|
];
|
|
m.x -= dx;
|
|
m.y -= dy;
|
|
m.clip = clip;
|
|
let mut child = (dx, dy, clip);
|
|
if scrolling[i] {
|
|
let weak = table
|
|
.active
|
|
.get(&m.id)
|
|
.ok_or_else(|| anyhow::anyhow!("missing scroll Mount"))?;
|
|
let owner = vm
|
|
.upgrade(*weak)?
|
|
.ok_or_else(|| anyhow::anyhow!("expired scroll Mount"))?;
|
|
let offset = vm
|
|
.payload::<Mount>(owner, "Mount")
|
|
.map(|mount| mount.scroll_offset);
|
|
vm.release(owner)?;
|
|
let offset = offset?;
|
|
let x = offset[0].clamp(0.0, limits[0]);
|
|
let y = offset[1].clamp(0.0, limits[1]);
|
|
m.scroll = Some([x, y, limits[0], limits[1]]);
|
|
let viewport = [m.x, m.y, m.width, m.height];
|
|
child = (
|
|
dx + x,
|
|
dy + y,
|
|
Some(
|
|
clip.map(|clip| intersection(clip, viewport))
|
|
.unwrap_or(viewport),
|
|
),
|
|
);
|
|
}
|
|
contexts.insert(m.id, child);
|
|
if let Some(command) = layout::paint_native(
|
|
nodes[m.block_id.as_str()],
|
|
m.x,
|
|
m.y,
|
|
Size {
|
|
width: m.width,
|
|
height: m.height,
|
|
},
|
|
) {
|
|
frame.commands.push(command);
|
|
frame.command_clips.push(m.clip);
|
|
}
|
|
}
|
|
if frame.commands.len() > 10_000 {
|
|
bail!("scroll paint command budget exceeded");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn coordinate(value: &Value) -> Result<f32, VmError> {
|
|
let Value::Number(value) = value else {
|
|
return Err(VmError::InvalidArgument(
|
|
"scroll coordinates must be numbers",
|
|
));
|
|
};
|
|
let n = value
|
|
.to_string()
|
|
.parse::<f32>()
|
|
.map_err(|_| VmError::InvalidArgument("invalid scroll coordinate"))?;
|
|
if !n.is_finite() || n.abs() > 1_000_000.0 {
|
|
return Err(VmError::InvalidArgument("scroll coordinate exceeds limits"));
|
|
}
|
|
Ok(n)
|
|
}
|
|
pub fn set(
|
|
cx: &mut NativeContext<'_>,
|
|
owner: ObjectRef,
|
|
desktop: WeakObjectRef,
|
|
point: [f32; 2],
|
|
relative: bool,
|
|
) -> Result<(), VmError> {
|
|
let mut mount = cx.store().payload::<Mount>(owner, "Mount")?.clone();
|
|
if !mount.mounted || mount.layout.is_some() {
|
|
return Err(VmError::InvalidArgument(
|
|
"scroll requires committed attached Mount",
|
|
));
|
|
}
|
|
let bounds = mount
|
|
.geometry
|
|
.scroll
|
|
.ok_or(VmError::InvalidArgument("Mount is not scrollable"))?;
|
|
let next = if relative {
|
|
[
|
|
mount.scroll_offset[0] + point[0],
|
|
mount.scroll_offset[1] + point[1],
|
|
]
|
|
} else {
|
|
point
|
|
};
|
|
mount.scroll_offset = [next[0].clamp(0.0, bounds[2]), next[1].clamp(0.0, bounds[3])];
|
|
let desktop = cx
|
|
.upgrade(desktop)?
|
|
.ok_or(VmError::InvalidArgument("Mount window expired"))?;
|
|
// Check the normal mutation barrier even when the clamped result is equal.
|
|
cx.replace_payload(owner, mount)?;
|
|
cx.set_attr(desktop, "scrollRequested", Value::Bool(true))
|
|
}
|
|
pub fn into_view(
|
|
cx: &mut NativeContext<'_>,
|
|
owner: ObjectRef,
|
|
desktop: WeakObjectRef,
|
|
) -> Result<(), VmError> {
|
|
let mount = cx.store().payload::<Mount>(owner, "Mount")?.clone();
|
|
if !mount.mounted || mount.layout.is_some() {
|
|
return Err(VmError::InvalidArgument(
|
|
"scrollIntoView requires committed attached Mount",
|
|
));
|
|
}
|
|
let mut rect = [
|
|
mount.geometry.x,
|
|
mount.geometry.y,
|
|
mount.geometry.width,
|
|
mount.geometry.height,
|
|
];
|
|
let mut parent = mount.parent;
|
|
while let Some(weak) = parent {
|
|
let Some(owner) = cx.upgrade(weak)? else {
|
|
break;
|
|
};
|
|
let mount = cx.store().payload::<Mount>(owner, "Mount")?.clone();
|
|
parent = mount.parent;
|
|
if let Some(offset) = mount.geometry.scroll {
|
|
let delta = |start: f32, length: f32, viewport: f32, size: f32| {
|
|
if start < viewport {
|
|
start - viewport
|
|
} else {
|
|
(start + length - viewport - size).max(0.0)
|
|
}
|
|
};
|
|
let dx = delta(rect[0], rect[2], mount.geometry.x, mount.geometry.width);
|
|
let dy = delta(rect[1], rect[3], mount.geometry.y, mount.geometry.height);
|
|
let x = (offset[0] + dx).clamp(0.0, offset[2]);
|
|
let y = (offset[1] + dy).clamp(0.0, offset[3]);
|
|
rect[0] -= x - offset[0];
|
|
rect[1] -= y - offset[1];
|
|
if x != offset[0] || y != offset[1] {
|
|
set(cx, owner, desktop, [x, y], false)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
pub fn wheel_target(frame: &Frame, point: Point, delta: [f32; 2]) -> Option<u64> {
|
|
let indices: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect();
|
|
let mut current = frame
|
|
.mounts
|
|
.iter()
|
|
.rev()
|
|
.find(|mount| mount.contains(point));
|
|
while let Some(mount) = current {
|
|
if mount.scroll.is_some_and(|s| {
|
|
(s[0] + delta[0]).clamp(0.0, s[2]) != s[0] || (s[1] + delta[1]).clamp(0.0, s[3]) != s[1]
|
|
}) {
|
|
return Some(mount.id);
|
|
}
|
|
current = mount.parent.and_then(|id| indices.get(&id).copied());
|
|
}
|
|
None
|
|
}
|