add offset / scrolling + clipboard support

This commit is contained in:
2025-09-27 21:13:00 -04:00
parent 95f049acb4
commit 5445008528
16 changed files with 386 additions and 99 deletions

View File

View File

@@ -1,7 +1,9 @@
mod align;
mod image;
mod offset;
mod pad;
mod rect;
mod scroll;
mod sense;
mod sized;
mod span;
@@ -12,8 +14,10 @@ mod trait_fns;
pub use align::*;
pub use image::*;
pub use offset::*;
pub use pad::*;
pub use rect::*;
pub use scroll::*;
pub use sense::*;
pub use sized::*;
pub use span::*;

17
src/core/offset.rs Normal file
View File

@@ -0,0 +1,17 @@
use crate::prelude::*;
pub struct Offset {
pub inner: WidgetId,
pub amt: UiVec2,
}
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) {
let region = UiRegion::full().offset(self.amt);
painter.widget_within(&self.inner, region);
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 {
ctx.size(&self.inner)
}
}

View File

@@ -31,10 +31,10 @@ impl Widget for Padded {
}
pub struct Padding {
left: f32,
right: f32,
top: f32,
bottom: f32,
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl Padding {

19
src/core/scroll.rs Normal file
View File

@@ -0,0 +1,19 @@
use crate::prelude::*;
pub struct Scroll {
pub inner: Offset,
pub size: UiVec2,
}
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) {
self.inner.draw(painter);
}
fn desired_size(&mut self, _: &mut SizeCtx) -> UiVec2 {
self.size
}
}
pub struct ScrollModule {
}

View File

@@ -29,6 +29,7 @@ pub enum Sense {
HoverStart,
Hovering,
HoverEnd,
Scroll,
}
pub struct Senses(Vec<Sense>);
@@ -47,6 +48,7 @@ pub struct CursorState {
pub pos: Vec2,
pub exists: bool,
pub buttons: CursorButtons,
pub scroll_delta: Vec2,
}
#[derive(Default, Clone)]
@@ -72,6 +74,13 @@ impl CursorButtons {
}
}
impl CursorState {
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,
@@ -81,29 +90,27 @@ pub enum ActivationState {
Off,
}
pub struct Sensor<Ctx> {
pub struct Sensor<Ctx, Data> {
pub senses: Senses,
pub f: Rc<dyn SenseFn<Ctx>>,
pub f: Rc<dyn EventFn<Ctx, Data>>,
}
pub type SensorMap<Ctx> = HashMap<Id, SensorGroup<Ctx>>;
pub type SensorMap<Ctx, Data> = HashMap<Id, SensorGroup<Ctx, Data>>;
pub type SenseShape = UiRegion;
pub struct SensorGroup<Ctx> {
pub struct SensorGroup<Ctx, Data> {
pub hover: ActivationState,
pub sensors: Vec<Sensor<Ctx>>,
pub sensors: Vec<Sensor<Ctx, Data>>,
}
#[derive(Clone)]
pub struct SenseData {
pub struct CursorData {
pub cursor: Vec2,
pub size: Vec2,
pub scroll_delta: Vec2,
}
pub trait SenseFn<Ctx>: Fn(&mut Ctx, SenseData) + 'static {}
impl<F: Fn(&mut Ctx, SenseData) + 'static, Ctx> SenseFn<Ctx> for F {}
pub struct SensorModule<Ctx> {
map: SensorMap<Ctx>,
map: SensorMap<Ctx, CursorData>,
active: HashMap<usize, HashMap<Id, SenseShape>>,
}
@@ -125,6 +132,9 @@ impl<Ctx: 'static> UiModule for SensorModule<Ctx> {
fn on_remove(&mut self, id: &Id) {
self.map.remove(id);
for layer in self.active.values_mut() {
layer.remove(id);
}
}
fn on_move(&mut self, inst: &WidgetInstance) {
@@ -165,7 +175,7 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
let Some(list) = module.active.get_mut(&i) else {
continue;
};
let mut ran = false;
let mut sensed = false;
for (id, shape) in list.iter() {
let group = module.map.get_mut(id).unwrap();
let region = shape.to_screen(window_size);
@@ -174,19 +184,20 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
if group.hover == ActivationState::Off {
continue;
}
sensed = true;
for sensor in &mut group.sensors {
if should_run(&sensor.senses, &cursor.buttons, group.hover) {
ran = true;
let sctx = SenseData {
if should_run(&sensor.senses, cursor, group.hover) {
let data = CursorData {
cursor: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
};
(sensor.f)(ctx, sctx);
(sensor.f)(ctx, data);
}
}
}
if ran {
if sensed {
break;
}
}
@@ -198,15 +209,16 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
}
}
pub fn should_run(senses: &Senses, cursor: &CursorButtons, hover: ActivationState) -> bool {
pub fn should_run(senses: &Senses, cursor: &CursorState, hover: ActivationState) -> bool {
for sense in senses.iter() {
if match sense {
Sense::PressStart(button) => cursor.select(button).is_start(),
Sense::Pressing(button) => cursor.select(button).is_on(),
Sense::PressEnd(button) => cursor.select(button).is_end(),
Sense::PressStart(button) => cursor.buttons.select(button).is_start(),
Sense::Pressing(button) => cursor.buttons.select(button).is_on(),
Sense::PressEnd(button) => cursor.buttons.select(button).is_end(),
Sense::HoverStart => hover.is_start(),
Sense::Hovering => hover.is_on(),
Sense::HoverEnd => hover.is_end(),
Sense::Scroll => cursor.scroll_delta != Vec2::ZERO,
} {
return true;
}
@@ -259,12 +271,12 @@ impl ActivationState {
impl Event for Senses {
type Module<Ctx: 'static> = SensorModule<Ctx>;
type Data = SenseData;
type Data = CursorData;
}
impl Event for Sense {
type Module<Ctx: 'static> = SensorModule<Ctx>;
type Data = SenseData;
type Data = CursorData;
}
impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> EventModule<E, Ctx>
@@ -292,7 +304,7 @@ impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> Even
}
})
.collect();
Some(move |ctx: &mut Ctx, data: SenseData| {
Some(move |ctx: &mut Ctx, data: CursorData| {
for f in &fs {
f(ctx, data.clone());
}
@@ -303,7 +315,7 @@ impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> Even
}
}
impl<Ctx> Default for SensorGroup<Ctx> {
impl<Ctx, Data> Default for SensorGroup<Ctx, Data> {
fn default() -> Self {
Self {
hover: Default::default(),

View File

@@ -43,7 +43,7 @@ impl Widget for TextEdit {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
UiRegion::from_size_align(size, Align::TopLeft)
.shifted(offset)
.offset(offset)
.within(&region),
);
} else {
@@ -247,7 +247,13 @@ impl<'a> TextEditCtx<'a> {
}
_ => return TextInputResult::Unused,
},
Key::Character(text) => self.insert(text),
Key::Character(text) => {
if modifiers.control && text == "v" {
return TextInputResult::Paste;
} else {
self.insert(text)
}
}
_ => return TextInputResult::Unused,
}
TextInputResult::Used
@@ -272,6 +278,7 @@ pub enum TextInputResult {
Unused,
Unfocus,
Submit,
Paste,
}
impl TextInputResult {

View File

@@ -7,6 +7,7 @@ pub trait CoreWidget<W, Tag> {
fn center(self) -> impl WidgetFn<Aligned>;
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<W>;
fn sized(self, size: impl Into<Vec2>) -> impl WidgetFn<Sized>;
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset>;
}
impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
@@ -42,6 +43,13 @@ impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
size: size.into(),
}
}
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset> {
move |ui| Offset {
inner: self.add(ui).any(),
amt: amt.into(),
}
}
}
pub trait CoreWidgetArr<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> {