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> {

View File

@@ -85,7 +85,7 @@ pub enum Align {
}
impl Align {
pub const fn anchor(&self) -> Vec2 {
pub const fn rel(&self) -> Vec2 {
match self {
Self::TopLeft => vec2(0.0, 0.0),
Self::Top => vec2(0.5, 0.0),

View File

@@ -131,7 +131,9 @@ impl<'a> PainterCtx<'a> {
if active.region == region {
return;
} else if active.region.size() == region.size() {
self.mov(id, region);
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
let active = self.remove(id).unwrap();
@@ -185,22 +187,21 @@ impl<'a> PainterCtx<'a> {
self.active.insert(id, instance);
}
fn mov(&mut self, id: Id, to: UiRegion) {
fn mov(&mut self, id: Id, from: UiRegion, to: UiRegion) {
let active = self.active.get_mut(&id).unwrap();
// children will not be changed, so this technically should not be needed
// probably need unsafe
let from = active.region;
for h in &active.primitives {
let region = self.layers[h.layer].primitives.region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = to;
active.region = active.region.outside(&from).within(&to);
for m in self.modules.iter_mut() {
m.on_move(active);
}
let children = active.children.clone();
for child in children {
self.mov(child, to);
self.mov(child, from, to);
}
}

View File

@@ -11,23 +11,20 @@ pub struct UiVec2 {
}
impl UiVec2 {
pub const ZERO: Self = Self {
rel: Vec2::ZERO,
abs: Vec2::ZERO,
};
/// expands this position into a sized region centered at self
pub fn expand(&self, size: impl Into<Vec2>) -> UiRegion {
let size = size.into();
UiRegion {
top_left: self.shifted(-size / 2.0),
bot_right: self.shifted(size / 2.0),
top_left: self.offset(-size / 2.0),
bot_right: self.offset(size / 2.0),
}
}
pub const fn anchor(anchor: Vec2) -> Self {
Self::rel(anchor)
}
pub const fn offset(offset: Vec2) -> Self {
Self::abs(offset)
}
pub const fn abs(abs: Vec2) -> Self {
Self {
rel: Vec2::ZERO,
@@ -42,11 +39,12 @@ impl UiVec2 {
}
}
pub const fn shift(&mut self, offset: impl const Into<Vec2>) {
self.abs += offset.into();
pub const fn shift(&mut self, offset: impl const Into<UiVec2>) {
let offset = offset.into();
*self += offset;
}
pub const fn shifted(mut self, offset: Vec2) -> Self {
pub const fn offset(mut self, offset: impl const Into<UiVec2>) -> Self {
self.shift(offset);
self
}
@@ -118,7 +116,7 @@ impl_op!(UiVec2 Sub sub; rel abs);
impl const From<Align> for UiVec2 {
fn from(align: Align) -> Self {
Self::anchor(align.anchor())
Self::rel(align.rel())
}
}
@@ -128,6 +126,12 @@ impl Align {
}
}
impl const From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self {
Self::abs(abs)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct UiScalar {
pub rel: f32,
@@ -198,10 +202,10 @@ impl UiRegion {
bot_right: Align::BotRight.into(),
}
}
pub fn anchor(anchor: Vec2) -> Self {
pub fn rel(anchor: Vec2) -> Self {
Self {
top_left: UiVec2::anchor(anchor),
bot_right: UiVec2::anchor(anchor),
top_left: UiVec2::rel(anchor),
bot_right: UiVec2::rel(anchor),
}
}
pub fn within(&self, parent: &Self) -> Self {
@@ -229,13 +233,13 @@ impl UiRegion {
std::mem::swap(&mut self.top_left, &mut self.bot_right);
}
pub fn shift(&mut self, offset: impl Into<Vec2>) {
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
let offset = offset.into();
self.top_left.shift(offset);
self.bot_right.shift(offset);
}
pub fn shifted(mut self, offset: impl Into<Vec2>) -> Self {
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
self.shift(offset);
self
}
@@ -265,9 +269,9 @@ impl UiRegion {
pub fn from_size_align(size: Vec2, align: Align) -> Self {
let mut top_left = UiVec2::from(align);
top_left.abs -= size * align.anchor();
top_left.abs -= size * align.rel();
let mut bot_right = UiVec2::from(align);
bot_right.abs += size * (Vec2::ONE - align.anchor());
bot_right.abs += size * (Vec2::ONE - align.rel());
Self {
top_left,
bot_right,
@@ -276,11 +280,11 @@ impl UiRegion {
pub fn from_ui_size_align(size: UiVec2, align: Align) -> Self {
let mut top_left = UiVec2::from(align);
top_left.abs -= size.abs * align.anchor();
top_left.rel -= size.rel * align.anchor();
top_left.abs -= size.abs * align.rel();
top_left.rel -= size.rel * align.rel();
let mut bot_right = UiVec2::from(align);
bot_right.abs += size.abs * (Vec2::ONE - align.anchor());
bot_right.rel += size.rel * (Vec2::ONE - align.anchor());
bot_right.abs += size.abs * (Vec2::ONE - align.rel());
bot_right.rel += size.rel * (Vec2::ONE - align.rel());
Self {
top_left,
bot_right,

View File

@@ -3,7 +3,6 @@
#![feature(const_trait_impl)]
#![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(trait_alias)]
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(const_cmp)]

View File

@@ -3,7 +3,7 @@ use ui::{
layout::Vec2,
};
use winit::{
event::{MouseButton, WindowEvent},
event::{MouseButton, MouseScrollDelta, WindowEvent},
keyboard::{Key, NamedKey},
};
@@ -32,6 +32,13 @@ impl Input {
_ => (),
}
}
WindowEvent::MouseWheel { delta, .. } => {
let delta = match *delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
self.cursor.scroll_delta = delta;
}
WindowEvent::CursorLeft { .. } => {
self.cursor.exists = false;
self.modifiers.clear();
@@ -56,7 +63,7 @@ impl Input {
}
pub fn end_frame(&mut self) {
self.cursor.buttons.end_frame();
self.cursor.end_frame();
}
}

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use app::App;
use arboard::Clipboard;
use cosmic_text::Family;
use render::Renderer;
use ui::prelude::*;
@@ -22,6 +23,7 @@ pub struct Client {
ui: Ui,
info: WidgetId<Text>,
focus: Option<WidgetId<TextEdit>>,
clipboard: Clipboard,
}
#[derive(Eq, PartialEq, Hash, Clone)]
@@ -127,6 +129,15 @@ impl Client {
.add_static(&mut ui);
let texts = Span::empty(Dir::DOWN).add_static(&mut ui);
let msg_area = (
Rect::new(Color::SKY),
texts
.offset(UiVec2::ZERO)
.edit_on(Sense::Scroll, |w, data| {
w.amt += UiVec2::abs(data.scroll_delta * 50.0);
}),
)
.stack();
let add_text = text_edit("add")
.text_align(Align::Left)
.size(30)
@@ -151,7 +162,7 @@ impl Client {
})
.add(&mut ui);
let text_edit_scroll = (
(Rect::new(Color::SKY), texts).stack(),
msg_area,
(
Rect::new(Color::WHITE.darker(0.9)),
(
@@ -217,6 +228,7 @@ impl Client {
ui,
info,
focus: None,
clipboard: Clipboard::new().unwrap(),
}
}
@@ -249,14 +261,20 @@ impl Client {
if let Some(sel) = &self.focus
&& event.state.is_pressed()
{
match self.ui.text(sel).apply_event(&event, &self.input.modifiers) {
let mut text = self.ui.text(sel);
match text.apply_event(&event, &self.input.modifiers) {
TextInputResult::Unfocus => {
self.focus = None;
}
TextInputResult::Submit => {
self.run_event(&sel.clone(), Submit, ());
}
_ => (),
TextInputResult::Paste => {
if let Ok(t) = self.clipboard.get_text() {
text.insert(&t);
}
}
TextInputResult::Unused | TextInputResult::Used => (),
}
}
}