`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
377 lines
11 KiB
Rust
377 lines
11 KiB
Rust
use crate::prelude::*;
|
|
use std::{
|
|
ops::{BitOr, Deref, DerefMut},
|
|
rc::Rc,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum CursorButton {
|
|
Left,
|
|
Right,
|
|
Middle,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum CursorSense {
|
|
PressStart(CursorButton),
|
|
Pressing(CursorButton),
|
|
PressEnd(CursorButton),
|
|
HoverStart,
|
|
Hovering,
|
|
HoverEnd,
|
|
Scroll,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CursorSenses(Vec<CursorSense>);
|
|
|
|
impl Event for CursorSenses {
|
|
type Data<'a> = CursorData<'a>;
|
|
type Global = Hovered;
|
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
|
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
|
let mut data = data.clone();
|
|
data.sense = sense;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// A press or a scroll is used up by whatever answered it, so it stops
|
|
/// there. Hovering is not: a cursor resting somewhere goes on resting.
|
|
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
|
!data.sense.position_only()
|
|
}
|
|
}
|
|
|
|
/// Who the cursor was inside, before and after an input. The difference is
|
|
/// whose hover has ended -- including a widget a higher layer has covered,
|
|
/// which the walk stops before reaching.
|
|
///
|
|
/// Two buffers that swap rather than one rebuilt, so an input allocates
|
|
/// nothing once they have grown.
|
|
#[derive(Default)]
|
|
pub struct Hovered {
|
|
was: Vec<WidgetId>,
|
|
now: Vec<WidgetId>,
|
|
}
|
|
|
|
impl CursorSense {
|
|
pub fn click() -> Self {
|
|
Self::PressStart(CursorButton::Left)
|
|
}
|
|
pub fn click_or_drag() -> CursorSenses {
|
|
Self::click() | Self::Pressing(CursorButton::Left)
|
|
}
|
|
pub fn unclick() -> Self {
|
|
Self::PressEnd(CursorButton::Left)
|
|
}
|
|
pub fn is_dragging(&self) -> bool {
|
|
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
|
}
|
|
|
|
/// False if the sense is a button or a scroll, true if it is only about
|
|
/// where the cursor is.
|
|
fn position_only(&self) -> bool {
|
|
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct CursorState {
|
|
pub pos: Vec2,
|
|
pub exists: bool,
|
|
pub buttons: CursorButtons,
|
|
pub scroll_delta: Vec2,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct CursorButtons {
|
|
pub left: ActivationState,
|
|
pub middle: ActivationState,
|
|
pub right: ActivationState,
|
|
}
|
|
|
|
impl CursorButtons {
|
|
pub fn select(&self, button: &CursorButton) -> &ActivationState {
|
|
match button {
|
|
CursorButton::Left => &self.left,
|
|
CursorButton::Right => &self.right,
|
|
CursorButton::Middle => &self.middle,
|
|
}
|
|
}
|
|
|
|
pub fn end_frame(&mut self) {
|
|
self.left.end_frame();
|
|
self.middle.end_frame();
|
|
self.right.end_frame();
|
|
}
|
|
|
|
pub fn iter(&self) -> impl Iterator<Item = (CursorButton, &ActivationState)> {
|
|
[
|
|
CursorButton::Left,
|
|
CursorButton::Middle,
|
|
CursorButton::Right,
|
|
]
|
|
.into_iter()
|
|
.map(|b| (b, self.select(&b)))
|
|
}
|
|
}
|
|
|
|
impl CursorState {
|
|
/// True if the cursor is only reporting where it is: no button and no
|
|
/// scroll this frame.
|
|
pub fn position_only(&self) -> bool {
|
|
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
|
|
}
|
|
|
|
pub fn end_frame(&mut self) {
|
|
self.buttons.end_frame();
|
|
self.scroll_delta = Vec2::ZERO;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
|
pub enum ActivationState {
|
|
Start,
|
|
On,
|
|
End,
|
|
#[default]
|
|
Off,
|
|
}
|
|
|
|
/// this and other similar stuff has a generic
|
|
/// because I kind of want to make CursorModule generic
|
|
/// or basically have some way to have custom senses
|
|
/// that depend on active widget positions
|
|
/// but I'm not sure how or if worth it
|
|
pub struct Sensor<Ctx: HasEvents, Data> {
|
|
pub senses: CursorSenses,
|
|
pub f: Rc<dyn EventFn<Ctx, Data>>,
|
|
}
|
|
|
|
pub type SenseShape = UiRegion;
|
|
|
|
#[derive(Clone)]
|
|
pub struct CursorData<'a> {
|
|
/// where this widget was hit
|
|
pub pos: Vec2,
|
|
pub size: Vec2,
|
|
pub scroll_delta: Vec2,
|
|
pub hover: ActivationState,
|
|
pub cursor: CursorState,
|
|
/// the first sense that triggered this
|
|
pub sense: CursorSense,
|
|
pub render: &'a UiRenderState,
|
|
}
|
|
|
|
pub trait SensorUi {
|
|
fn run_sensors<Rsc: HasEvents>(
|
|
&self,
|
|
rsc: &mut Rsc,
|
|
state: &mut Rsc::State,
|
|
cursor: CursorState,
|
|
);
|
|
}
|
|
|
|
impl SensorUi for UiRenderState {
|
|
fn run_sensors<Rsc: HasEvents>(
|
|
&self,
|
|
rsc: &mut Rsc,
|
|
state: &mut Rsc::State,
|
|
cursor: CursorState,
|
|
) {
|
|
// in order to remove this take, need to store active list in UiRenderState somehow
|
|
// this would probably be done through a generic parameter that adds yet another rsc /
|
|
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
|
// update it there?
|
|
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
|
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
|
|
hovered.now.clear();
|
|
let position_only = cursor.position_only();
|
|
let region_of = |id| self.window_region(&id);
|
|
|
|
for layer in self.layers.indices().rev() {
|
|
let mut consumed = false;
|
|
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
|
let Some(region) = region_of(id) else {
|
|
continue;
|
|
};
|
|
if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) {
|
|
continue;
|
|
}
|
|
hovered.now.push(id);
|
|
let hover = match hovered.was.contains(&id) {
|
|
true => ActivationState::On,
|
|
false => ActivationState::Start,
|
|
};
|
|
// A press or a scroll stops where something answered it, so a
|
|
// button over a list does not swallow the list's scrolling.
|
|
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
|
|
// A cursor doing neither stops at whatever it is over, so
|
|
// hovering does not reach through.
|
|
consumed |= position_only;
|
|
}
|
|
// Applied after the layer, never during it: senses on one layer do
|
|
// not block each other.
|
|
if consumed {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Whatever the cursor was inside and is not now, whether it left or a
|
|
// layer above took the input before the walk reached it. A widget that
|
|
// stopped being drawn has no region to report and is simply dropped.
|
|
for &id in &hovered.was {
|
|
if !hovered.now.contains(&id)
|
|
&& let Some(region) = region_of(id)
|
|
{
|
|
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
|
|
}
|
|
}
|
|
std::mem::swap(&mut hovered.was, &mut hovered.now);
|
|
|
|
let senses = rsc.events_mut().get_type::<CursorSense>();
|
|
senses.active = active;
|
|
senses.global = hovered;
|
|
}
|
|
}
|
|
|
|
/// Runs one widget's cursor senses, and says whether they used up the input.
|
|
fn deliver<Rsc: HasEvents>(
|
|
render: &UiRenderState,
|
|
rsc: &mut Rsc,
|
|
state: &mut Rsc::State,
|
|
id: WidgetId,
|
|
hover: ActivationState,
|
|
cursor: &CursorState,
|
|
region: PixelRegion,
|
|
) -> bool {
|
|
let data = CursorData {
|
|
pos: cursor.pos - region.top_left.to_f32(),
|
|
size: region.size().to_f32(),
|
|
scroll_delta: cursor.scroll_delta,
|
|
hover,
|
|
cursor: cursor.clone(),
|
|
// this does not have any meaning;
|
|
// might wanna set up Event to have a prepare stage
|
|
sense: CursorSense::Hovering,
|
|
render,
|
|
};
|
|
rsc.run_event::<CursorSense>(id, data, state)
|
|
}
|
|
|
|
pub fn should_run(
|
|
senses: &CursorSenses,
|
|
cursor: &CursorState,
|
|
hover: ActivationState,
|
|
) -> Option<CursorSense> {
|
|
for sense in senses.iter() {
|
|
// A widget the cursor is no longer inside senses only its position:
|
|
// the press that ended its hover landed on something else.
|
|
if !hover.is_on() && !sense.position_only() {
|
|
continue;
|
|
}
|
|
if match sense {
|
|
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
|
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
|
CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(),
|
|
CursorSense::HoverStart => hover.is_start(),
|
|
CursorSense::Hovering => hover.is_on(),
|
|
CursorSense::HoverEnd => hover.is_end(),
|
|
CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
|
} {
|
|
return Some(*sense);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
impl ActivationState {
|
|
pub fn is_start(&self) -> bool {
|
|
*self == Self::Start
|
|
}
|
|
pub fn is_on(&self) -> bool {
|
|
*self == Self::Start || *self == Self::On
|
|
}
|
|
pub fn is_end(&self) -> bool {
|
|
*self == Self::End
|
|
}
|
|
pub fn is_off(&self) -> bool {
|
|
*self == Self::End || *self == Self::Off
|
|
}
|
|
pub fn update(&mut self, on: bool) {
|
|
*self = match *self {
|
|
Self::Start => match on {
|
|
true => Self::On,
|
|
false => Self::End,
|
|
},
|
|
Self::On => match on {
|
|
true => Self::On,
|
|
false => Self::End,
|
|
},
|
|
Self::End => match on {
|
|
true => Self::Start,
|
|
false => Self::Off,
|
|
},
|
|
Self::Off => match on {
|
|
true => Self::Start,
|
|
false => Self::Off,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn end_frame(&mut self) {
|
|
match self {
|
|
Self::Start => *self = Self::On,
|
|
Self::End => *self = Self::Off,
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EventLike for CursorSense {
|
|
type Event = CursorSenses;
|
|
fn into_event(self) -> Self::Event {
|
|
self.into()
|
|
}
|
|
}
|
|
|
|
impl Deref for CursorSenses {
|
|
type Target = Vec<CursorSense>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl DerefMut for CursorSenses {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
impl From<CursorSense> for CursorSenses {
|
|
fn from(val: CursorSense) -> Self {
|
|
CursorSenses(vec![val])
|
|
}
|
|
}
|
|
|
|
impl BitOr for CursorSense {
|
|
type Output = CursorSenses;
|
|
|
|
fn bitor(self, rhs: Self) -> Self::Output {
|
|
CursorSenses(vec![self, rhs])
|
|
}
|
|
}
|
|
|
|
impl BitOr<CursorSense> for CursorSenses {
|
|
type Output = Self;
|
|
|
|
fn bitor(mut self, rhs: CursorSense) -> Self::Output {
|
|
self.0.push(rhs);
|
|
self
|
|
}
|
|
}
|