From 242c3b992e2972a461fa9dbef007c959990e00ae Mon Sep 17 00:00:00 2001 From: Shadow Cat Date: Thu, 11 Sep 2025 00:59:26 -0400 Subject: [PATCH] IDC FINALLY OH MY GOD (I think like ctx + resize propagation + some other stuff) --- src/core/image.rs | 2 +- src/core/sense.rs | 28 ++++---- src/core/text.rs | 98 ++++++++++++++++++++++++- src/core/trait_fns.rs | 42 +++++------ src/layout/id.rs | 37 +++++----- src/layout/painter.rs | 75 ++++++++++++++++--- src/layout/sense.rs | 79 ++++++++++---------- src/layout/text.rs | 24 ++++--- src/layout/ui.rs | 112 ++++++++++++----------------- src/layout/widget.rs | 68 ++++++------------ src/render/mod.rs | 2 +- src/testing/mod.rs | 147 ++++++++++++++++++++++++-------------- src/testing/render/mod.rs | 2 +- src/util/borrow.rs | 35 +++++++++ src/util/mod.rs | 6 +- 15 files changed, 476 insertions(+), 281 deletions(-) create mode 100644 src/util/borrow.rs diff --git a/src/core/image.rs b/src/core/image.rs index 5df0991..00ea973 100644 --- a/src/core/image.rs +++ b/src/core/image.rs @@ -15,7 +15,7 @@ impl Widget for Image { } } -pub fn image(image: impl LoadableImage) -> impl WidgetFn { +pub fn image(image: impl LoadableImage) -> impl WidgetFn { let image = image.get_image().expect("Failed to load image"); move |ui| Image { handle: ui.add_texture(image), diff --git a/src/core/sense.rs b/src/core/sense.rs index 462a0f0..1c1ca8e 100644 --- a/src/core/sense.rs +++ b/src/core/sense.rs @@ -1,25 +1,26 @@ use crate::prelude::*; -pub trait Sensable { - fn on(self, sense: Senses, f: impl SenseFn) -> impl WidgetIdFn; +pub trait Sensable { + fn on(self, sense: Senses, f: impl SenseFn) -> impl WidgetIdFn; fn id_on( self, senses: Senses, - f: impl FnMut(&WidgetId, &mut Ui, SenseCtx) + 'static, - ) -> impl WidgetIdFn + f: impl FnMut(&WidgetId, &mut Ctx, SenseCtx) + 'static, + ) -> impl WidgetIdFn where W: Widget; fn edit_on( self, senses: Senses, f: impl FnMut(&mut W, SenseCtx) + 'static, - ) -> impl WidgetIdFn + ) -> impl WidgetIdFn where - W: Widget; + W: Widget, + Ctx: UiCtx; } -impl, Tag> Sensable for W { - fn on(self, senses: Senses, f: impl SenseFn) -> impl WidgetIdFn { +impl, Ctx, Tag> Sensable for W { + fn on(self, senses: Senses, f: impl SenseFn) -> impl WidgetIdFn { move |ui| { let id = self.add(ui); ui.add_sensor( @@ -35,24 +36,25 @@ impl, Tag> Sensable for W { fn id_on( self, senses: Senses, - mut f: impl FnMut(&WidgetId, &mut Ui, SenseCtx) + 'static, - ) -> impl WidgetIdFn + mut f: impl FnMut(&WidgetId, &mut Ctx, SenseCtx) + 'static, + ) -> impl WidgetIdFn where W::Widget: Widget, { self.with_id(move |ui, id| { let id2 = id.clone(); - ui.add(id.on(senses, move |ui, pos| f(&id2, ui, pos))) + id.on(senses, move |ctx, pos| f(&id2, ctx, pos)).add(ui) }) } fn edit_on( self, senses: Senses, mut f: impl FnMut(&mut W::Widget, SenseCtx) + 'static, - ) -> impl WidgetIdFn + ) -> impl WidgetIdFn where W::Widget: Widget, + Ctx: UiCtx, { - self.id_on(senses, move |id, ui, pos| f(&mut ui[id], pos)) + self.id_on(senses, move |id, ctx, pos| f(&mut ctx.ui()[id], pos)) } } diff --git a/src/core/text.rs b/src/core/text.rs index a93e5ee..a6b9be5 100644 --- a/src/core/text.rs +++ b/src/core/text.rs @@ -5,6 +5,7 @@ use crate::prelude::*; pub struct Text { pub content: String, pub attrs: TextAttrs, + /// inner alignment of text region (within where its drawn) pub align: Align, buf: TextBuffer, cursor: Cursor, @@ -40,17 +41,109 @@ impl Text { size: Vec2::ZERO, } } + pub fn select(&mut self, pos: Vec2, size: Vec2) { let pos = pos - self.region().top_left.to_size(size); let Some(cursor) = self.buf.hit(pos.x, pos.y) else { return; }; self.cursor = Cursor::Select { - line: cursor.line, - idx: cursor.index, + line: cursor.line as isize, + col: cursor.index as isize, }; } + pub fn deselect(&mut self) { + self.cursor = Cursor::None; + } + + pub fn insert(&mut self, text: &str) { + let i = self.update_cursor(); + self.content.insert_str(i, text); + + match &mut self.cursor { + Cursor::None => (), + Cursor::Select { line, col } => { + *col += 1; + } + } + } + + pub fn backspace(&mut self) { + if let Some(i) = self.update_cursor().checked_sub(1) { + self.content.remove(i); + match &mut self.cursor { + Cursor::None => (), + Cursor::Select { line, col } => { + *col -= 1; + } + } + } + } + + pub fn delete(&mut self) { + let i = self.update_cursor(); + if i != self.content.len() { + self.content.remove(i); + } + } + + pub fn move_cursor(&mut self, dir: Dir) { + if let Cursor::Select { line, col } = &mut self.cursor { + match dir { + Dir::LEFT => *col -= 1, + Dir::RIGHT => *col += 1, + Dir::UP => *line -= 1, + Dir::DOWN => *line += 1, + } + } + } + + pub fn update_cursor(&mut self) -> usize { + match &mut self.cursor { + Cursor::None => 0, + Cursor::Select { line, col } => { + if *col < 0 { + *line -= 1; + } + if *line < 0 { + *line = 0; + *col = 0; + } + let mut idx = self.content.len(); + let mut l = 0; + let mut c = 0; + let mut cur_len = 0; + for (i, ch) in self.content.chars().enumerate() { + if ch == '\n' { + l += 1; + c = 0; + } else { + if l == *line { + cur_len = i as isize + 1; + if c == *col { + idx = i; + } + } + c += 1; + } + } + if *col < 0 { + *col = cur_len; + } + if *col > cur_len { + *col = 0; + *line += 1; + } + if *line > l { + *line = l; + *col = cur_len; + } + idx + } + } + } + pub fn region(&self) -> UiRegion { UiRegion::from_size_align(self.size, self.align) } @@ -58,6 +151,7 @@ impl Text { impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { + self.update_cursor(); let (handle, offset) = painter.render_text(&mut self.buf, &self.content, &self.attrs, &self.cursor); let dims = handle.size(); diff --git a/src/core/trait_fns.rs b/src/core/trait_fns.rs index 40fd29d..dd75ab1 100644 --- a/src/core/trait_fns.rs +++ b/src/core/trait_fns.rs @@ -1,34 +1,34 @@ use super::*; use crate::prelude::*; -pub trait CoreWidget { - fn pad(self, padding: impl Into) -> impl WidgetFn; - fn align(self, align: Align) -> impl WidgetFn; - fn center(self) -> impl WidgetFn; - fn label(self, label: impl Into) -> impl WidgetIdFn; - fn size(self, size: impl Into) -> impl WidgetFn; +pub trait CoreWidget { + fn pad(self, padding: impl Into) -> impl WidgetFn; + fn align(self, align: Align) -> impl WidgetFn; + fn center(self) -> impl WidgetFn; + fn label(self, label: impl Into) -> impl WidgetIdFn; + fn size(self, size: impl Into) -> impl WidgetFn; } -impl, Tag> CoreWidget for W { - fn pad(self, padding: impl Into) -> impl WidgetFn { +impl, Ctx, Tag> CoreWidget for W { + fn pad(self, padding: impl Into) -> impl WidgetFn { |ui| Padded { padding: padding.into(), - inner: self.add(ui).erase_type(), + inner: self.add(ui).any(), } } - fn align(self, align: Align) -> impl WidgetFn { + fn align(self, align: Align) -> impl WidgetFn { move |ui| Aligned { - inner: self.add(ui).erase_type(), + inner: self.add(ui).any(), align, } } - fn center(self) -> impl WidgetFn { + fn center(self) -> impl WidgetFn { self.align(Align::Center) } - fn label(self, label: impl Into) -> impl WidgetIdFn { + fn label(self, label: impl Into) -> impl WidgetIdFn { |ui| { let id = self.add(ui); ui.set_label(&id, label.into()); @@ -36,28 +36,28 @@ impl, Tag> CoreWidget for W { } } - fn size(self, size: impl Into) -> impl WidgetFn { + fn size(self, size: impl Into) -> impl WidgetFn { move |ui| Sized { - inner: self.add(ui).erase_type(), + inner: self.add(ui).any(), size: size.into(), } } } -pub trait CoreWidgetArr { - fn span(self, dir: Dir, lengths: impl IntoSpanLens) -> impl WidgetFn; - fn stack(self) -> impl WidgetFn; +pub trait CoreWidgetArr { + fn span(self, dir: Dir, lengths: impl IntoSpanLens) -> impl WidgetFn; + fn stack(self) -> impl WidgetFn; } -impl, Tag> CoreWidgetArr for Wa { - fn span(self, dir: Dir, lengths: impl IntoSpanLens) -> impl WidgetFn { +impl, Ctx, Tag> CoreWidgetArr for Wa { + fn span(self, dir: Dir, lengths: impl IntoSpanLens) -> impl WidgetFn { let lengths = lengths.into_lens(); move |ui| Span { children: self.ui(ui).arr.into_iter().zip(lengths).collect(), dir, } } - fn stack(self) -> impl WidgetFn { + fn stack(self) -> impl WidgetFn { move |ui| Stack { children: self.ui(ui).arr.to_vec(), } diff --git a/src/layout/id.rs b/src/layout/id.rs index 0bc779c..640f8eb 100644 --- a/src/layout/id.rs +++ b/src/layout/id.rs @@ -78,7 +78,7 @@ impl WidgetId { } } - pub fn erase_type(self) -> WidgetId { + pub fn any(self) -> WidgetId { self.cast_type() } @@ -127,14 +127,14 @@ impl Drop for WidgetId { pub struct IdTag; pub struct IdFnTag; -pub trait WidgetIdFn: FnOnce(&mut Ui) -> WidgetId {} -impl WidgetId> WidgetIdFn for F {} +pub trait WidgetIdFn: FnOnce(&mut Ui) -> WidgetId {} +impl) -> WidgetId, Ctx> WidgetIdFn for F {} /// TODO: does this ever make sense to use? it allows for invalid ids -pub trait Idable { +pub trait Idable { type Widget: Widget; - fn set(self, ui: &mut Ui, id: &WidgetId); - fn id(self, id: &WidgetId) -> impl WidgetIdFn + fn set(self, ui: &mut Ui, id: &WidgetId); + fn id(self, id: &WidgetId) -> impl WidgetIdFn where Self: Sized, { @@ -144,7 +144,7 @@ pub trait Idable { id } } - fn id_static(self, id: StaticWidgetId) -> impl WidgetIdFn + fn id_static(self, id: StaticWidgetId) -> impl WidgetIdFn where Self: Sized, { @@ -156,33 +156,33 @@ pub trait Idable { } } -impl Idable for W { +impl Idable for W { type Widget = W; - fn set(self, ui: &mut Ui, id: &WidgetId) { + fn set(self, ui: &mut Ui, id: &WidgetId) { ui.set(id, self); } } -impl W, W: Widget> Idable for F { +impl) -> W, W: Widget, Ctx> Idable for F { type Widget = W; - fn set(self, ui: &mut Ui, id: &WidgetId) { + fn set(self, ui: &mut Ui, id: &WidgetId) { let w = self(ui); ui.set(id, w); } } -impl WidgetLike for WidgetId { +impl WidgetLike for WidgetId { type Widget = W; - fn add(self, _: &mut Ui) -> WidgetId { + fn add(self, _: &mut Ui) -> WidgetId { self } } -impl WidgetId> WidgetLike for F { +impl) -> WidgetId, Ctx> WidgetLike for F { type Widget = W; - fn add(self, ui: &mut Ui) -> WidgetId { + fn add(self, ui: &mut Ui) -> WidgetId { self(ui) } } @@ -191,11 +191,14 @@ impl StaticWidgetId { pub fn to_id(&self, send: &Sender) -> WidgetId { WidgetId::new(self.id.id(), self.ty, send.clone(), true) } + pub fn any(self) -> StaticWidgetId { + unsafe { std::mem::transmute(self) } + } } -impl WidgetLike for StaticWidgetId { +impl WidgetLike for StaticWidgetId { type Widget = W; - fn add(self, ui: &mut Ui) -> WidgetId { + fn add(self, ui: &mut Ui) -> WidgetId { self.id(&ui.send) } } diff --git a/src/layout/painter.rs b/src/layout/painter.rs index 78b2d77..b6ba534 100644 --- a/src/layout/painter.rs +++ b/src/layout/painter.rs @@ -2,8 +2,8 @@ use std::ops::Range; use crate::{ layout::{ - Active, Cursor, SensorMap, SizeCtx, TextAttrs, TextBuffer, TextData, TextOffset, - TextureHandle, Textures, UiRegion, Vec2, WidgetId, Widgets, + Active, Cursor, TextAttrs, TextBuffer, TextData, TextOffset, TextureHandle, Textures, + UiRegion, Vec2, WidgetId, Widgets, }, render::{Primitive, PrimitiveHandle, Primitives}, util::{HashSet, Id}, @@ -15,12 +15,12 @@ pub struct Painter<'a, 'c> { textures: Vec, primitives: Vec, children: Vec, + sized_children: HashSet, id: Id, } pub struct PainterCtx<'a> { pub widgets: &'a Widgets, - pub sensor_map: &'a SensorMap, pub active: &'a mut Active, pub primitives: &'a mut Primitives, pub textures: &'a mut Textures, @@ -36,6 +36,7 @@ pub struct WidgetInstance { pub textures: Vec, pub primitives: Vec, pub children: Vec, + pub resize: Option, pub span: Range, } @@ -43,7 +44,6 @@ impl<'a> PainterCtx<'a> { pub fn new( widgets: &'a Widgets, primitives: &'a mut Primitives, - sensor_map: &'a SensorMap, active: &'a mut Active, textures: &'a mut Textures, text: &'a mut TextData, @@ -51,7 +51,6 @@ impl<'a> PainterCtx<'a> { ) -> Self { Self { widgets, - sensor_map, active, primitives, textures, @@ -63,11 +62,24 @@ impl<'a> PainterCtx<'a> { pub fn redraw(&mut self, id: &Id) { self.drawing.clear(); + let Some(active) = self.active.widgets.get(id) else { + return; + }; + + if let Some(rid) = &active.resize { + self.redraw(&rid.duplicate()); + if self.drawing.contains(id) { + return; + } + } + let Some(active) = self.remove(id) else { return; }; self.primitives.set_pos(active.span.start); self.draw_inner(id, active.region, active.parent(), Some(active.children)); + self.active.widgets.get_mut(id).unwrap().resize = active.resize; + let delta = self.primitives.apply(active.span.clone()); if delta != 0 && let Some(parent) = active.parent @@ -90,13 +102,20 @@ impl<'a> PainterCtx<'a> { parent: Option, old_children: Option>, ) { + // I have no idea if these checks work lol + // the idea is u can't redraw stuff u already drew, + // and if parent is different then there's another copy with a different parent + // but this has a very weird issue where you can't move widgets unless u remove first + // so swapping is impossible rn I think? + // there's definitely better solutions like a counter (>1 = panic) but don't care rn if self.drawing.contains(id) { - panic!("Cannot draw the same widget twice"); + panic!("Cannot draw the same widget twice (1)"); } let mut old_children = old_children.unwrap_or_default(); + let mut resize = None; if let Some(active) = self.active.widgets.get(id) { if active.parent != parent { - panic!("Cannot draw the same widget twice"); + panic!("Cannot draw the same widget twice (2)"); } if active.region == region { self.primitives.skip(&active.span); @@ -106,6 +125,7 @@ impl<'a> PainterCtx<'a> { // else if active.region.size() == region.size() { move } let active = self.remove(id).unwrap(); old_children = active.children; + resize = active.resize; } let mut painter = Painter { @@ -115,6 +135,7 @@ impl<'a> PainterCtx<'a> { primitives: Vec::new(), ctx: self, children: Vec::new(), + sized_children: HashSet::new(), }; // draw widgets @@ -122,6 +143,8 @@ impl<'a> PainterCtx<'a> { painter.ctx.widgets.get_dyn_dynamic(id).draw(&mut painter); let end_i = painter.ctx.primitives.cur_pos(); + let sized_children = painter.sized_children; + // add to active let instance = WidgetInstance { id: id.duplicate(), @@ -131,14 +154,20 @@ impl<'a> PainterCtx<'a> { span: start_i..end_i, primitives: painter.primitives, children: painter.children, + resize, }; + for cid in sized_children { + if let Some(w) = self.active.widgets.get_mut(&cid) { + w.resize = Some(id.duplicate()) + } + } for c in &old_children { if !instance.children.contains(c) { self.remove_rec(c); } } - self.active.add(id, instance, self.sensor_map); + self.active.add(id, instance, self.widgets); } fn remove(&mut self, id: &Id) -> Option { @@ -168,8 +197,6 @@ impl<'a> PainterCtx<'a> { let instance = self.active.widgets.get_mut(&parent).unwrap(); let end = &mut instance.span.end; *end = end.strict_add_signed(delta); - // ids are supposed to be unique so theoretically no cloning is needed - // leaving for now for testing let parent = instance.parent(); for child in instance .children @@ -186,6 +213,7 @@ impl<'a> PainterCtx<'a> { fn shift_child(&mut self, child: &Id, start: usize, delta: isize) { let instance = self.active.widgets.get_mut(child).unwrap(); + // = also prevents the original id from getting shifted if instance.span.start <= start { return; } @@ -263,6 +291,7 @@ impl<'a, 'c> Painter<'a, 'c> { } pub fn size(&mut self, id: &WidgetId) -> Vec2 { + self.sized_children.insert(id.id.duplicate()); self.ctx .widgets .get_dyn_dynamic(&id.id) @@ -275,10 +304,36 @@ impl<'a, 'c> Painter<'a, 'c> { text: self.ctx.text, textures: self.ctx.textures, widgets: self.ctx.widgets, + checked: &mut self.sized_children, } } } +pub struct SizeCtx<'a> { + pub size: Vec2, + pub text: &'a mut TextData, + pub textures: &'a mut Textures, + widgets: &'a Widgets, + checked: &'a mut HashSet, +} + +impl SizeCtx<'_> { + pub fn size(&mut self, id: &WidgetId) -> Vec2 { + self.checked.insert(id.id.duplicate()); + self.widgets.get_dyn_dynamic(&id.id).get_size(self) + } + pub fn draw_text( + &mut self, + buffer: &mut TextBuffer, + content: &str, + attrs: &TextAttrs, + cursor: &Cursor, + ) -> (TextureHandle, TextOffset) { + self.text + .draw(buffer, content, attrs, cursor, self.textures) + } +} + impl WidgetInstance { pub fn parent(&self) -> Option { self.parent.as_ref().map(|p| p.duplicate()) diff --git a/src/layout/sense.rs b/src/layout/sense.rs index 5fbe3a3..2f8eb5b 100644 --- a/src/layout/sense.rs +++ b/src/layout/sense.rs @@ -1,7 +1,5 @@ -use std::any::Any; - use crate::{ - layout::{Ui, UiRegion, Vec2, WidgetId}, + layout::{Ui, UiRegion, Vec2}, util::{HashMap, Id, bitflags}, }; @@ -30,19 +28,18 @@ pub enum ActivationState { Off, } -pub struct Sensor { +pub struct Sensor { pub senses: Senses, - pub f: Box, + pub f: Box>, } -pub type SensorMap = HashMap; +pub type SensorMap = HashMap>; pub type ActiveSensors = HashMap; pub type SenseShape = UiRegion; -#[derive(Default)] -pub struct SensorGroup { +pub struct SensorGroup { pub hover: ActivationState, pub cursor: ActivationState, - pub sensors: Vec, + pub sensors: Vec>, } pub struct SenseCtx { @@ -50,41 +47,37 @@ pub struct SenseCtx { pub size: Vec2, } -pub trait SenseFn: FnMut(&mut Ui, SenseCtx) + 'static {} -impl SenseFn for F {} +pub trait SenseFn: FnMut(&mut Ctx, SenseCtx) + 'static {} +impl SenseFn for F {} -impl Ui { - pub fn add_sensor(&mut self, id: &WidgetId, f: Sensor) { - self.sensor_map - .entry(id.id.duplicate()) - .or_default() - .sensors - .push(f); - } +pub trait UiCtx { + fn ui(&mut self) -> &mut Ui + where + Self: Sized; +} - pub fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) { - let active = std::mem::take(&mut self.active.sensors); - let mut map = std::mem::take(&mut self.sensor_map); - for (id, shape) in active.iter() { - let group = &mut map.get_mut(id).unwrap(); - let region = shape.to_screen(window_size); - let in_shape = cursor.exists && region.contains(cursor.pos); - group.hover.update(in_shape); - group.cursor.update(cursor.pressed && in_shape); +pub fn run_sensors(ctx: &mut Ctx, cursor: &CursorState, window_size: Vec2) { + let active = std::mem::take(&mut ctx.ui().active.sensors); + let mut map = std::mem::take(&mut ctx.ui().sensor_map); + for (id, shape) in active.iter() { + let group = &mut map.get_mut(id).unwrap(); + let region = shape.to_screen(window_size); + let in_shape = cursor.exists && region.contains(cursor.pos); + group.hover.update(in_shape); + group.cursor.update(cursor.pressed && in_shape); - for sensor in &mut group.sensors { - if should_run(sensor.senses, group.cursor, group.hover) { - let ctx = SenseCtx { - cursor: cursor.pos - region.top_left, - size: region.bot_right - region.top_left, - }; - (sensor.f)(self, ctx); - } + for sensor in &mut group.sensors { + if should_run(sensor.senses, group.cursor, group.hover) { + let sctx = SenseCtx { + cursor: cursor.pos - region.top_left, + size: region.bot_right - region.top_left, + }; + (sensor.f)(ctx, sctx); } } - self.sensor_map = map; - self.active.sensors = active; } + ctx.ui().sensor_map = map; + ctx.ui().active.sensors = active; } pub fn should_run(senses: Senses, cursor: ActivationState, hover: ActivationState) -> bool { @@ -138,3 +131,13 @@ impl ActivationState { } } } + +impl Default for SensorGroup { + fn default() -> Self { + Self { + hover: Default::default(), + cursor: Default::default(), + sensors: Default::default(), + } + } +} diff --git a/src/layout/text.rs b/src/layout/text.rs index a9d2b01..c27a45a 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -33,8 +33,8 @@ pub enum Cursor { #[default] None, Select { - line: usize, - idx: usize, + line: isize, + col: isize, }, } @@ -78,15 +78,14 @@ impl TextData { let mut max_y = 0; let c = attrs.color; let mut max_width = 0.0f32; - let mut i = 0; let mut cursor_x = 0; for (run_i, run) in buffer.layout_runs().enumerate() { if let Cursor::Select { line, .. } = cursor - && *line == run_i + && *line == run_i as isize { cursor_x = run.line_w as i32; } - for glyph in run.glyphs.iter() { + for (i, glyph) in run.glyphs.iter().enumerate() { let physical_glyph = glyph.physical((0., 0.), 1.0); let glyph_color = match glyph.color_opt { @@ -94,13 +93,12 @@ impl TextData { None => cosmic_text::Color::rgba(c.r, c.g, c.b, c.a), }; - if let Cursor::Select { idx, line } = cursor - && *line == run_i - && *idx == i + if let Cursor::Select { col: idx, line } = cursor + && *line == run_i as isize + && *idx == i as isize { cursor_x = physical_glyph.x; } - i += 1; self.swash_cache.with_pixels( &mut self.font_system, @@ -133,19 +131,23 @@ impl TextData { image.put_pixel(x, y, color); } if let &Cursor::Select { line, .. } = cursor { + let x = (cursor_x - min_x) as u32; for y in 0..attrs.line_height as u32 { // no clue if this is good or bad for non integer values // depends on how the layouting is actually done - let x = (cursor_x - min_x) as u32; let y = (y as f32 + attrs.line_height * line as f32 - min_y as f32) as u32; image.put_pixel(x, y, Rgba(c.as_arr())); } } + let mut lines = buffer.lines.len(); + if content.ends_with('\n') { + lines += 1; + } let offset = TextOffset { top_left: Vec2::new(min_x as f32, min_y as f32), bot_right: Vec2::new( max_width - max_x as f32, - attrs.line_height * buffer.lines.len() as f32 - max_y as f32, + attrs.line_height * lines as f32 - max_y as f32, ), }; (textures.add(image), offset) diff --git a/src/layout/ui.rs b/src/layout/ui.rs index d6d0ece..8aba521 100644 --- a/src/layout/ui.rs +++ b/src/layout/ui.rs @@ -2,21 +2,21 @@ use image::DynamicImage; use crate::{ layout::{ - ActiveSensors, PainterCtx, SensorMap, StaticWidgetId, TextData, TextureHandle, Textures, - Vec2, Widget, WidgetId, WidgetInstance, WidgetLike, + ActiveSensors, PainterCtx, Sensor, SensorMap, StaticWidgetId, TextData, TextureHandle, + Textures, Vec2, Widget, WidgetId, WidgetInstance, WidgetLike, }, render::Primitives, - util::{HashMap, Id, IdTracker}, + util::{DynBorrower, HashMap, HashSet, Id, IdTracker}, }; use std::{ any::{Any, TypeId}, - ops::{Deref, DerefMut, Index, IndexMut}, + ops::{Index, IndexMut}, sync::mpsc::{Receiver, Sender, channel}, }; -pub struct Ui { +pub struct Ui { root: Option, - widgets: Widgets, + pub(super) widgets: Widgets, updates: Vec, recv: Receiver, pub(super) send: Sender, @@ -24,11 +24,11 @@ pub struct Ui { // TODO: make these non pub(crate) pub(crate) primitives: Primitives, pub(crate) textures: Textures, - pub(crate) text: TextData, + text: TextData, full_redraw: bool, pub(super) active: Active, - pub(super) sensor_map: SensorMap, + pub(super) sensor_map: SensorMap, } #[derive(Default)] @@ -38,19 +38,23 @@ pub struct Widgets { } pub struct WidgetData { - widget: Box, - borrowed: bool, - label: String, + pub widget: Box, + pub label: String, + pub sized_children: HashSet, + /// dynamic borrow checking + pub borrowed: bool, + /// whether this widget has any sensors + pub sensor: bool, } -impl Ui { - pub fn add(&mut self, w: impl WidgetLike) -> WidgetId { +impl Ui { + pub fn add(&mut self, w: impl WidgetLike) -> WidgetId { w.add(self) } pub fn add_static( &mut self, - w: impl WidgetLike, + w: impl WidgetLike, ) -> StaticWidgetId { let id = w.add(self); id.into_static() @@ -58,7 +62,7 @@ impl Ui { /// useful for debugging pub fn set_label(&mut self, id: &WidgetId, label: String) { - *self.widgets.label_mut(id).unwrap() = label; + self.widgets.data_mut(&id.id).unwrap().label = label; } pub fn add_widget(&mut self, w: W) -> WidgetId { @@ -75,8 +79,8 @@ impl Ui { self.widgets.insert(id.id.duplicate(), w); } - pub fn set_root(&mut self, w: impl WidgetLike) { - self.root = Some(w.add(self).erase_type()); + pub fn set_root(&mut self, w: impl WidgetLike) { + self.root = Some(w.add(self).any()); self.full_redraw = true; } @@ -121,7 +125,6 @@ impl Ui { let mut ctx = PainterCtx::new( &self.widgets, &mut self.primitives, - &self.sensor_map, &mut self.active, &mut self.textures, &mut self.text, @@ -145,7 +148,6 @@ impl Ui { let mut ctx = PainterCtx::new( &self.widgets, &mut self.primitives, - &self.sensor_map, &mut self.active, &mut self.textures, &mut self.text, @@ -160,6 +162,7 @@ impl Ui { /// free any resources that don't have references anymore fn free(&mut self) { for id in self.recv.try_iter() { + self.sensor_map.remove(&id); self.widgets.delete(id); } self.textures.free(); @@ -176,9 +179,18 @@ impl Ui { pub fn active_widgets(&self) -> usize { self.active.widgets.len() } + + pub fn add_sensor(&mut self, id: &WidgetId, f: Sensor) { + self.sensor_map + .entry(id.id.duplicate()) + .or_default() + .sensors + .push(f); + self.widgets.data_mut(&id.id).unwrap().sensor = true; + } } -impl Index<&WidgetId> for Ui { +impl Index<&WidgetId> for Ui { type Output = W; fn index(&self, id: &WidgetId) -> &Self::Output { @@ -186,14 +198,14 @@ impl Index<&WidgetId> for Ui { } } -impl IndexMut<&WidgetId> for Ui { +impl IndexMut<&WidgetId> for Ui { fn index_mut(&mut self, id: &WidgetId) -> &mut Self::Output { self.updates.push(id.id.duplicate()); self.get_mut(id).unwrap() } } -impl Index> for Ui { +impl Index> for Ui { type Output = W; fn index(&self, id: StaticWidgetId) -> &Self::Output { @@ -201,7 +213,7 @@ impl Index> for Ui { } } -impl IndexMut> for Ui { +impl IndexMut> for Ui { fn index_mut(&mut self, id: StaticWidgetId) -> &mut Self::Output { self.updates.push(id.id.id()); self.widgets.get_static_mut(&id).unwrap() @@ -233,10 +245,7 @@ impl Widgets { if data.borrowed { panic!("tried to mutably borrow the same widget twice"); } - WidgetWrapper { - widget: data.widget.as_mut(), - borrowed: &mut data.borrowed, - } + WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed) } pub fn get_static(&self, id: &StaticWidgetId) -> Option<&W> { @@ -259,12 +268,12 @@ impl Widgets { self.insert_any(id, Box::new(widget), std::any::type_name::().to_string()); } - pub fn label(&self, id: &WidgetId) -> Option<&String> { - self.map.get(&id.id).map(|d| &d.label) + pub fn data(&self, id: &Id) -> Option<&WidgetData> { + self.map.get(id) } - pub fn label_mut(&mut self, id: &WidgetId) -> Option<&mut String> { - self.map.get_mut(&id.id).map(|d| &mut d.label) + pub fn data_mut(&mut self, id: &Id) -> Option<&mut WidgetData> { + self.map.get_mut(id) } pub fn insert_any(&mut self, id: Id, widget: Box, label: String) { @@ -272,8 +281,10 @@ impl Widgets { id, WidgetData { widget, - borrowed: false, label, + sized_children: HashSet::new(), + borrowed: false, + sensor: false, }, ); } @@ -296,36 +307,7 @@ impl Widgets { } } -pub struct WidgetWrapper<'a> { - widget: &'a mut dyn Widget, - borrowed: &'a mut bool, -} - -impl<'a> WidgetWrapper<'a> { - pub fn new(widget: &'a mut dyn Widget, borrowed: &'a mut bool) -> Self { - Self { widget, borrowed } - } -} - -impl Drop for WidgetWrapper<'_> { - fn drop(&mut self) { - *self.borrowed = false; - } -} - -impl Deref for WidgetWrapper<'_> { - type Target = dyn Widget; - - fn deref(&self) -> &Self::Target { - self.widget - } -} - -impl DerefMut for WidgetWrapper<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.widget - } -} +pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>; impl dyn Widget { pub fn as_any(&self) -> &dyn Any { @@ -337,7 +319,7 @@ impl dyn Widget { } } -impl Default for Ui { +impl Default for Ui { fn default() -> Self { let (send, recv) = channel(); Self { @@ -377,8 +359,8 @@ impl Active { instance } - pub fn add(&mut self, id: &Id, instance: WidgetInstance, sensors_map: &SensorMap) { - if sensors_map.get(id).is_some() { + pub fn add(&mut self, id: &Id, instance: WidgetInstance, widgets: &Widgets) { + if widgets.data(id).is_some_and(|w| w.sensor) { self.sensors.insert(id.duplicate(), instance.region); } self.widgets.insert(id.duplicate(), instance); diff --git a/src/layout/widget.rs b/src/layout/widget.rs index 6131021..9007551 100644 --- a/src/layout/widget.rs +++ b/src/layout/widget.rs @@ -1,7 +1,4 @@ -use crate::layout::{ - Cursor, Painter, StaticWidgetId, TextAttrs, TextBuffer, TextData, TextOffset, TextureHandle, - Textures, Ui, Vec2, WidgetId, WidgetIdFn, Widgets, -}; +use crate::layout::{Painter, SizeCtx, StaticWidgetId, Ui, Vec2, WidgetId, WidgetIdFn}; use std::{any::Any, marker::PhantomData}; @@ -12,39 +9,16 @@ pub trait Widget: Any { } } -pub struct SizeCtx<'a> { - pub size: Vec2, - pub text: &'a mut TextData, - pub textures: &'a mut Textures, - pub(super) widgets: &'a Widgets, -} - -impl SizeCtx<'_> { - pub fn size(&mut self, id: &WidgetId) -> Vec2 { - self.widgets.get_dyn_dynamic(&id.id).get_size(self) - } - pub fn draw_text( - &mut self, - buffer: &mut TextBuffer, - content: &str, - attrs: &TextAttrs, - cursor: &Cursor, - ) -> (TextureHandle, TextOffset) { - self.text - .draw(buffer, content, attrs, cursor, self.textures) - } -} - pub struct WidgetTag; pub struct FnTag; -pub trait WidgetLike { +pub trait WidgetLike { type Widget: 'static; - fn add(self, ui: &mut Ui) -> WidgetId; + fn add(self, ui: &mut Ui) -> WidgetId; fn with_id( self, - f: impl FnOnce(&mut Ui, WidgetId) -> WidgetId, - ) -> impl WidgetIdFn + f: impl FnOnce(&mut Ui, WidgetId) -> WidgetId, + ) -> impl WidgetIdFn where Self: Sized, { @@ -53,7 +27,7 @@ pub trait WidgetLike { f(ui, id) } } - fn add_static(self, ui: &mut Ui) -> StaticWidgetId + fn add_static(self, ui: &mut Ui) -> StaticWidgetId where Self: Sized, { @@ -64,19 +38,19 @@ pub trait WidgetLike { /// A function that returns a widget given a UI. /// Useful for defining trait functions on widgets that create a parent widget so that the children /// don't need to be IDs yet -pub trait WidgetFn: FnOnce(&mut Ui) -> W {} -impl W> WidgetFn for F {} +pub trait WidgetFn: FnOnce(&mut Ui) -> W {} +impl) -> W, Ctx> WidgetFn for F {} -impl W> WidgetLike for F { +impl) -> W, Ctx> WidgetLike for F { type Widget = W; - fn add(self, ui: &mut Ui) -> WidgetId { + fn add(self, ui: &mut Ui) -> WidgetId { self(ui).add(ui) } } -impl WidgetLike for W { +impl WidgetLike for W { type Widget = W; - fn add(self, ui: &mut Ui) -> WidgetId { + fn add(self, ui: &mut Ui) -> WidgetId { ui.add_widget(self) } } @@ -96,22 +70,22 @@ impl WidgetArr { } pub struct ArrTag; -pub trait WidgetArrLike { +pub trait WidgetArrLike { type Ws; - fn ui(self, ui: &mut Ui) -> WidgetArr; + fn ui(self, ui: &mut Ui) -> WidgetArr; } -impl WidgetArrLike for WidgetArr { +impl WidgetArrLike for WidgetArr { type Ws = Ws; - fn ui(self, _: &mut Ui) -> WidgetArr { + fn ui(self, _: &mut Ui) -> WidgetArr { self } } -impl> WidgetArrLike<1, WidgetTag> for W { +impl, Ctx> WidgetArrLike<1, Ctx, WidgetTag> for W { type Ws = (W::Widget,); - fn ui(self, ui: &mut Ui) -> WidgetArr<1, (W::Widget,)> { - WidgetArr::new([self.add(ui).erase_type()]) + fn ui(self, ui: &mut Ui) -> WidgetArr<1, (W::Widget,)> { + WidgetArr::new([self.add(ui).any()]) } } @@ -121,9 +95,9 @@ macro_rules! impl_widget_arr { impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*); }; ($n:expr;$($W:ident)*;$($Tag:ident)*) => { - impl<$($W: WidgetLike<$Tag>,$Tag,)*> WidgetArrLike<$n, ($($Tag,)*)> for ($($W,)*) { + impl,$Tag,)*> WidgetArrLike<$n, Ctx, ($($Tag,)*)> for ($($W,)*) { type Ws = ($($W::Widget,)*); - fn ui(self, ui: &mut Ui) -> WidgetArr<$n, ($($W::Widget,)*)> { + fn ui(self, ui: &mut Ui) -> WidgetArr<$n, ($($W::Widget,)*)> { #[allow(non_snake_case)] let ($($W,)*) = self; WidgetArr::new( diff --git a/src/render/mod.rs b/src/render/mod.rs index b67110a..9167002 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -48,7 +48,7 @@ impl UiRenderer { pass.draw(0..4, 0..self.instance.len() as u32); } - pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) { + pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) { if ui.primitives.updated { self.instance .update(device, queue, ui.primitives.instances()); diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 6cb6b1f..74517c5 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -5,7 +5,12 @@ use cosmic_text::Family; use render::Renderer; use senses::*; use ui::prelude::*; -use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window}; +use winit::{ + event::WindowEvent, + event_loop::ActiveEventLoop, + keyboard::{Key, NamedKey}, + window::Window, +}; use crate::testing::input::Input; @@ -20,7 +25,7 @@ pub fn main() { pub struct Client { renderer: Renderer, input: Input, - ui: Ui, + ui: Ui, info: WidgetId, selected: Option>, } @@ -71,22 +76,31 @@ impl Client { let span_add = Span::empty(Dir::RIGHT).add_static(&mut ui); - let main = pad_test.pad(10).add_static(&mut ui); + let add_button = rect(Color::LIME) + .radius(30) + .on(PRESS_START, move |ctx: &mut Client, _| { + let child = ctx + .ui + .add(image(include_bytes!("assets/sungals.png")).center()) + .any(); + ctx.ui[span_add].children.push((child, sized())); + }) + .size(150) + .align(Align::BotRight); - let switch_button = |color, to, label| { - let rect = rect(color) - .id_on(PRESS_START, move |id, ui, _| { - ui[main].inner.set_static(to); - ui[id].color = color.add_rgb(-0.2); - }) - .edit_on(HOVER_START | PRESS_END, move |r, _| { - r.color = color.add_rgb(0.4); - }) - .edit_on(HOVER_END, move |r, _| { - r.color = color; - }); - (rect, text(label).font_size(30)).stack() - }; + let del_button = rect(Color::RED) + .radius(30) + .on(PRESS_START, move |ctx: &mut Client, _| { + ctx.ui[span_add].children.pop(); + }) + .size(150) + .align(Align::BotLeft); + + let span_add_test = (span_add, add_button, del_button) + .stack() + .add_static(&mut ui); + + let main = pad_test.pad(10).add_static(&mut ui); let btext = |content| text(content).font_size(30); @@ -113,56 +127,51 @@ impl Client { let texts = Span::empty(Dir::DOWN).add(&mut ui); let text_edit_scroll = ( texts, - text("add").font_size(30).edit_on(PRESS_START, |text, ctx| { - text.select(ctx.cursor, ctx.size); - }), + text("add") + .font_size(30) + .id_on(PRESS_START, |id, client: &mut Client, ctx| { + client.ui[id].select(ctx.cursor, ctx.size); + client.selected = Some(id.clone()); + }) + .pad(30), ) - .span(Dir::DOWN, [ratio(1), fixed(40)]) + .span(Dir::DOWN, [ratio(1), sized()]) .add_static(&mut ui); + let switch_button = |color, to, label| { + let rect = rect(color) + .id_on(PRESS_START, move |id, ctx: &mut Client, _| { + ctx.ui[main].inner.set_static(to); + ctx.ui[id].color = color.add_rgb(-0.2); + }) + .edit_on(HOVER_START | PRESS_END, move |r, _| { + r.color = color.add_rgb(0.4); + }) + .edit_on(HOVER_END, move |r, _| { + r.color = color; + }); + (rect, text(label).font_size(30)).stack() + }; + let tabs = ( - switch_button(Color::RED, pad_test, "pad"), - switch_button(Color::GREEN, span_test, "span"), - switch_button(Color::BLUE, span_add, "image span"), - switch_button(Color::MAGENTA, text_test, "text layout"), + switch_button(Color::RED, pad_test.any(), "pad"), + switch_button(Color::GREEN, span_test.any(), "span"), + switch_button(Color::BLUE, span_add_test.any(), "image span"), + switch_button(Color::MAGENTA, text_test.any(), "text layout"), switch_button( Color::YELLOW.mul_rgb(0.5), - text_edit_scroll, + text_edit_scroll.any(), "text edit scroll", ), ) .span(Dir::RIGHT, ratio(1)); - let add_button = rect(Color::LIME) - .radius(30) - .on(PRESS_START, move |ui, _| { - let child = ui - .add(image(include_bytes!("assets/sungals.png")).center()) - .erase_type(); - ui[span_add].children.push((child, sized())); - }) - .size(150) - .align(Align::BotRight); - - let del_button = rect(Color::RED) - .radius(30) - .on(PRESS_START, move |ui, _| { - ui[span_add].children.pop(); - }) - .size(150) - .align(Align::BotLeft); - let info = text("").add(&mut ui); let info_sect = info.clone().pad(10).align(Align::BotLeft); ui.set_root( ( tabs.label("tabs"), - ( - main, - add_button.label("add button"), - del_button.label("del button"), - info_sect.label("info sect"), - ) + (main, info_sect.label("info sect")) .stack() .label("main stack"), ) @@ -183,7 +192,7 @@ impl Client { self.input.event(&event); let cursor_state = self.cursor_state(); let window_size = self.window_size(); - self.ui.run_sensors(&cursor_state, window_size); + run_sensors(self, &cursor_state, window_size); match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::RedrawRequested => { @@ -195,6 +204,34 @@ impl Client { self.ui.resize((size.width, size.height)); self.renderer.resize(&size) } + WindowEvent::KeyboardInput { event, .. } => { + if let Some(sel) = &self.selected + && event.state.is_pressed() + { + let w = &mut self.ui[sel]; + match &event.logical_key { + Key::Named(named) => match named { + NamedKey::Backspace => w.backspace(), + NamedKey::Delete => w.delete(), + NamedKey::Space => w.insert(" "), + NamedKey::Enter => w.insert("\n"), + NamedKey::ArrowRight => w.move_cursor(Dir::RIGHT), + NamedKey::ArrowLeft => w.move_cursor(Dir::LEFT), + NamedKey::ArrowUp => w.move_cursor(Dir::UP), + NamedKey::ArrowDown => w.move_cursor(Dir::DOWN), + NamedKey::Escape => { + w.deselect(); + self.selected = None; + } + _ => (), + }, + Key::Character(text) => { + w.insert(text); + } + _ => (), + } + } + } _ => (), } let new = format!( @@ -211,3 +248,9 @@ impl Client { } } } + +impl UiCtx for Client { + fn ui(&mut self) -> &mut Ui { + &mut self.ui + } +} diff --git a/src/testing/render/mod.rs b/src/testing/render/mod.rs index d57b4ad..adb4353 100644 --- a/src/testing/render/mod.rs +++ b/src/testing/render/mod.rs @@ -21,7 +21,7 @@ pub struct Renderer { } impl Renderer { - pub fn update(&mut self, updates: &mut Ui) { + pub fn update(&mut self, updates: &mut Ui) { self.ui.update(&self.device, &self.queue, updates); } diff --git a/src/util/borrow.rs b/src/util/borrow.rs new file mode 100644 index 0000000..bc8e51a --- /dev/null +++ b/src/util/borrow.rs @@ -0,0 +1,35 @@ +use std::ops::{Deref, DerefMut}; + +pub struct DynBorrower<'a, T: ?Sized> { + data: &'a mut T, + borrowed: &'a mut bool, +} + +impl<'a, T: ?Sized> DynBorrower<'a, T> { + pub fn new(data: &'a mut T, borrowed: &'a mut bool) -> Self { + if *borrowed { + panic!("tried to mutably borrow the same thing twice"); + } + Self { data, borrowed } + } +} + +impl Drop for DynBorrower<'_, T> { + fn drop(&mut self) { + *self.borrowed = false; + } +} + +impl Deref for DynBorrower<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl DerefMut for DynBorrower<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.data + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 04c8895..df7090f 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,12 +1,14 @@ +mod bitflags; +mod borrow; mod id; mod math; mod refcount; -mod bitflags; +pub(crate) use bitflags::*; +pub(crate) use borrow::*; pub(crate) use id::*; pub(crate) use math::*; pub(crate) use refcount::*; -pub(crate) use bitflags::*; pub type HashMap = std::collections::HashMap; pub type HashSet = std::collections::HashSet;