Compare commits

..
Author SHA1 Message Date
iris fa771a99eb update stuff 2026-08-03 20:33:59 -04:00
iris e817bc83af const trait keyword order 2026-06-23 21:30:09 -04:00
iris a648c62aa2 update 2026-04-15 20:31:52 -04:00
iris c118bb446b some potentially nice trait stuff 2026-03-15 21:07:06 -04:00
iris 1102dc7338 work 2026-02-26 19:18:27 -05:00
iris 1aadef0e7e fix Draw (redraw) 2026-02-21 00:19:39 -05:00
iris 426ff0adfc oop 2026-02-18 16:49:59 -05:00
iris dab6cf298a Merge branch 'work' of git.arirex.me:shadowcat/iris into work 2026-02-17 18:14:38 -05:00
iris 38d896d44d selector 2026-02-17 18:14:19 -05:00
43 changed files with 1716 additions and 2002 deletions

No files matched your search

Generated
+732 -911
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -8,7 +8,8 @@ edition.workspace = true
[dependencies] [dependencies]
iris-core = { workspace = true } iris-core = { workspace = true }
iris-macro = { workspace = true } iris-macro = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
unicode-segmentation = { workspace = true }
winit = { workspace = true } winit = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] } arboard = { workspace = true, features = ["wayland-data-control"] }
pollster = { workspace = true } pollster = { workspace = true }
@@ -27,13 +28,13 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[workspace.dependencies] [workspace.dependencies]
pollster = "0.4.0" pollster = "1.0.1"
winit = "0.30.12" winit = "0.30.12"
wgpu = "28.0.0" wgpu = "30.0.0"
bytemuck = "1.23.1" bytemuck = "1.23.1"
image = "0.25.6" image = "0.25.6"
parley = "0.11.1" cosmic-text = "0.16.0"
swash = "0.2.10" unicode-segmentation = "1.12.0"
fxhash = "0.2.1" fxhash = "0.2.1"
arboard = "3.6.1" arboard = "3.6.1"
iris-core = { path = "core" } iris-core = { path = "core" }
+2 -2
View File
@@ -4,9 +4,9 @@ version.workspace = true
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]
winit = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
image = { workspace = true } image = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true } fxhash = { workspace = true }
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike}; use crate::{UiRsc, WidgetIdFn, WidgetLike, WeakWidget};
pub trait WidgetAttr<Rsc, W: ?Sized> { pub trait WidgetAttr<Rsc, W: ?Sized> {
type Input; type Input;
+1
View File
@@ -5,6 +5,7 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_destruct)] #![feature(const_destruct)]
#![feature(portable_simd)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
-6
View File
@@ -10,12 +10,6 @@ pub struct Color<T> {
pub a: T, pub a: T,
} }
impl<T: ColorNum> Default for Color<T> {
fn default() -> Self {
Self::BLACK
}
}
impl<T: ColorNum> Color<T> { impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN); pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX); pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
+140 -199
View File
@@ -1,63 +1,60 @@
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2}; use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
use parley::{ use cosmic_text::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, SwashContent,
};
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
}; };
use image::{DynamicImage, GenericImageView, RgbaImage};
use std::simd::{Simd, num::SimdUint};
/// TODO: properly wrap this
pub mod text_lib {
pub use cosmic_text::*;
}
pub struct TextData { pub struct TextData {
pub font_cx: FontContext, pub font_system: FontSystem,
pub layout_cx: LayoutContext<UiColor>, pub swash_cache: SwashCache,
scale_cx: ScaleContext, glyph_cache: Vec<(Placement, CacheKey, Color)>,
pub atlas: GlyphAtlas,
} }
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
font_cx: FontContext::new(), font_system: FontSystem::new(),
layout_cx: LayoutContext::new(), swash_cache: SwashCache::new(),
scale_cx: ScaleContext::new(), glyph_cache: Default::default(),
atlas: GlyphAtlas::default(),
} }
} }
} }
#[derive(Clone, PartialEq)] #[derive(Clone, Copy)]
pub enum Family {
SansSerif,
Serif,
Monospace,
Named(String),
}
impl Family {
fn family(&self) -> FontFamily<'_> {
let name = match self {
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
};
FontFamily::Single(name)
}
}
#[derive(Clone, PartialEq)]
pub struct TextAttrs { pub struct TextAttrs {
pub color: UiColor, pub color: UiColor,
pub font_size: f32, pub font_size: f32,
pub line_height: f32, pub line_height: f32,
pub family: Family, pub family: Family<'static>,
pub wrap: bool, pub wrap: bool,
/// inner alignment of text region (within where it's drawn)
pub align: RegionAlign, pub align: RegionAlign,
} }
pub const LINE_HEIGHT_MULT: f32 = 1.1; impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics_and_size(
font_system,
Metrics::new(self.font_size, self.line_height),
width,
None,
);
let attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs);
for line in &mut buf.lines {
line.set_attrs_list(list.clone());
}
}
}
pub type TextBuffer = Buffer;
impl Default for TextAttrs { impl Default for TextAttrs {
fn default() -> Self { fn default() -> Self {
@@ -73,178 +70,122 @@ impl Default for TextAttrs {
} }
} }
/// Keeps text and its corresponding layout from getting out of sync. pub const LINE_HEIGHT_MULT: f32 = 1.1;
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
shaped: Option<(TextAttrs, Option<f32>)>,
}
impl TextBuffer {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
layout: Layout::new(),
shaped: None,
}
}
pub fn new_empty() -> Self {
Self::new("")
}
pub fn text(&self) -> &str {
&self.text
}
pub fn layout(&self) -> &Layout<UiColor> {
&self.layout
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if text != self.text {
self.text = text;
self.shaped = None;
}
}
/// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String {
self.shaped = None;
&mut self.text
}
pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height())
}
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
return;
}
let mut builder = data
.layout_cx
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
builder.push_default(StyleProperty::FontSize(attrs.font_size));
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
attrs.line_height,
)));
builder.push_default(StyleProperty::Brush(attrs.color));
builder.build_into(&mut self.layout, &self.text);
self.layout.break_all_lines(width);
self.layout
.align(Alignment::Start, AlignmentOptions::default());
self.shaped = Some((attrs.clone(), width));
}
}
impl TextData { impl TextData {
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> { pub fn draw(
let mut placed = Vec::new(); &mut self,
for line in buffer.layout.lines() { buffer: &mut TextBuffer,
for item in line.items() { attrs: &TextAttrs,
let PositionedLayoutItem::GlyphRun(run) = item else { textures: &mut Textures,
continue; ) -> RenderedText {
}; // TODO: either this or the layout stuff (or both) is super slow,
let font = run.run().font(); // should probably do texture packing and things if possible.
let font_size = run.run().font_size(); // very visible if you add just a couple of wrapping texts and resize window
let coords = run.run().normalized_coords(); // should also be timed to figure out exactly what points need to be sped up
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize) // let mut pixels = HashMap::<_, [u8; 4]>::default();
else { let mut min_x = 0;
continue; let mut min_y = 0;
}; let mut max_x = 0;
let coords_hash = hash_coords(coords); let mut max_y = 0;
// `font.data.id()` rather than the pointer, so the same font let text_color = {
// loaded twice is still one set of entries. let c = attrs.color;
let font_id = font.data.id(); cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
};
let mut max_width = 0.0f32;
let mut height = 0.0;
for glyph in run.positioned_glyphs() { for run in buffer.layout_runs() {
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8; for glyph in run.glyphs.iter() {
let key = GlyphKey { let physical_glyph = glyph.physical((0., 0.), 1.0);
font: font_id,
glyph: glyph.id, let glyph_color = match glyph.color_opt {
size: (font_size * 16.0).round() as u32, Some(some) => some,
subpixel, None => text_color,
coords: coords_hash, };
};
let entry = match self.atlas.get(&key) { if let Some(img) = self
Some(entry) => entry, .swash_cache
None => { .get_image(&mut self.font_system, physical_glyph.cache_key)
let mut scaler = self {
.scale_cx let mut pos = img.placement;
.builder(font_ref) pos.left += physical_glyph.x;
.size(font_size) pos.top = physical_glyph.y + run.line_y as i32 - pos.top;
.hint(true) min_x = min_x.min(pos.left);
.normalized_coords(coords) min_y = min_y.min(pos.top);
.build(); max_x = max_x.max(pos.left + pos.width as i32);
let image = Render::new(&[ max_y = max_y.max(pos.top + pos.height as i32);
Source::ColorOutline(0), self.glyph_cache
Source::ColorBitmap(StrikeWith::BestFit), .push((pos, physical_glyph.cache_key, glyph_color));
Source::Outline, }
]) }
.format(Format::Alpha) max_width = max_width.max(run.line_w);
.offset(Vector::new(subpixel as f32 / 4.0, 0.0)) height += run.line_height;
.render(&mut scaler, glyph.id as u16); }
match image { let img_width = (max_x - min_x + 1) as u32;
Some(image) => self.atlas.insert(key, &image, textures), let img_height = (max_y - min_y + 1) as u32;
None => { let mut image = RgbaImage::new(img_width, img_height);
self.atlas.insert_empty(key);
None for (pos, key, color) in self.glyph_cache.drain(..) {
} let img = self
} .swash_cache
} .get_image(&mut self.font_system, key)
}; .as_ref()
let Some(entry) = entry else { continue }; .unwrap();
placed.push(PlacedGlyph { let mut merge = |i, color: [u8; 4]| {
entry, let i = i as i32;
offset: Vec2::new( let x = (i % pos.width as i32 + pos.left - min_x) as u32;
glyph.x.floor() + entry.left as f32, let y = (i / pos.width as i32 + pos.top - min_y) as u32;
glyph.y.floor() - entry.top as f32, let pixel = &mut image[(x, y)].0;
), // TODO: no clue if proper alpha blending should be done
}); *pixel = Simd::from(color).saturating_add(Simd::from(*pixel)).into();
};
match img.content {
SwashContent::Mask => {
for (i, a) in img.data.iter().enumerate() {
let mut color = color.as_rgba();
color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8;
merge(i, color);
}
}
SwashContent::SubpixelMask => todo!("subpixel mask text rendering"),
SwashContent::Color => {
let (colors, _) = img.data.as_chunks::<4>();
for (i, color) in colors.iter().enumerate() {
merge(i, *color);
}
} }
} }
} }
placed
}
}
fn hash_coords(coords: &[i16]) -> u64 { let max_dim = 8192;
let mut h: u64 = 0xcbf2_9ce4_8422_2325; if image.width() > max_dim || image.height() > max_dim {
for c in coords { let width = image.width().min(max_dim);
h ^= *c as u16 as u64; let height = image.height().min(max_dim);
h = h.wrapping_mul(0x1000_0000_01b3); eprintln!(
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
image.dimensions(),
(width, height)
);
image = image.view(0, 0, width, height).to_image();
}
RenderedText {
handle: textures.add(image),
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height),
}
} }
h
} }
#[derive(Clone)] #[derive(Clone)]
pub struct RenderedText { pub struct RenderedText {
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>, pub handle: TextureHandle,
pub top_left_offset: Vec2,
pub size: Vec2, pub size: Vec2,
pub color: UiColor,
} }
impl TextData { pub trait HasTextures {
pub fn render( fn add_texture(&mut self, image: DynamicImage) -> TextureHandle;
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
textures: &mut Textures,
) -> RenderedText {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures);
RenderedText {
glyphs: std::sync::Arc::new(glyphs),
size: buffer.size(),
color: attrs.color,
}
}
} }
-26
View File
@@ -29,24 +29,14 @@ pub struct Textures {
pub enum TextureUpdate<'a> { pub enum TextureUpdate<'a> {
Push(&'a DynamicImage), Push(&'a DynamicImage),
Set(u32, &'a DynamicImage), Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32), Free(u32),
PushFree, PushFree,
SetFree, SetFree,
} }
#[derive(Debug, Clone, Copy)]
pub struct PatchRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
enum Update { enum Update {
Push(u32), Push(u32),
Set(u32), Set(u32),
Patch(u32, PatchRect),
Free(u32), Free(u32),
} }
@@ -91,18 +81,6 @@ impl Textures {
} }
} }
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.inner.view_idx as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates
.push(Update::Patch(handle.inner.view_idx, rect));
}
pub fn free(&mut self) { pub fn free(&mut self) {
for idx in self.recv.try_iter() { for idx in self.recv.try_iter() {
self.images[idx as usize] = None; self.images[idx as usize] = None;
@@ -121,10 +99,6 @@ impl Textures {
.as_ref() .as_ref()
.map(|img| TextureUpdate::Set(i, img)) .map(|img| TextureUpdate::Set(i, img))
.unwrap_or(TextureUpdate::SetFree), .unwrap_or(TextureUpdate::SetFree),
Update::Patch(i, rect) => self.images[i as usize]
.as_ref()
.map(|img| TextureUpdate::Patch(i, rect, img))
.unwrap_or(TextureUpdate::SetFree),
Update::Free(i) => TextureUpdate::Free(i), Update::Free(i) => TextureUpdate::Free(i),
}) })
} }
-200
View File
@@ -1,200 +0,0 @@
use crate::{
PatchRect, TextureHandle, Textures,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
const PAD: u32 = 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font: u64,
pub glyph: u32,
/// Font size in 1/16 px, so sizes that round to the same pixels share a
/// raster instead of filling the atlas with near-duplicates.
pub size: u32,
/// Horizontal subpixel phase, in 1/4 px.
pub subpixel: u8,
/// Hash of the variation coordinates; a variable font at two weights is two
/// different sets of pixels from one glyph id.
pub coords: u64,
}
#[derive(Clone, Copy)]
pub struct GlyphEntry {
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
/// Offset from the glyph's pen position to the top-left of its pixels.
pub left: i32,
pub top: i32,
pub width: u32,
pub height: u32,
pub is_color: bool,
pub view_idx: u32,
pub sampler_idx: u32,
}
struct Page {
handle: TextureHandle,
x: u32,
y: u32,
shelf_height: u32,
}
#[derive(Default)]
pub struct GlyphAtlas {
pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
}
impl GlyphAtlas {
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.entries.get(key).copied()
}
pub fn insert(
&mut self,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
self.entries.insert(key, None);
return None;
}
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
self.entries.insert(key, None);
return None;
}
let (page_idx, x, y) = self.allocate(w, h, textures);
let page = &self.pages[page_idx];
let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
write_glyph(rgba, image, x, y);
let handle = page.handle.clone();
let rect = PatchRect {
x,
y,
width: w,
height: h,
};
textures.patch(&handle, rect);
let page = &self.pages[page_idx];
let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry {
uv_min: [x as f32 * scale, y as f32 * scale],
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
left: image.placement.left,
top: image.placement.top,
width: w,
height: h,
is_color: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx,
sampler_idx: page.handle.primitive().sampler_idx,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
let need_w = w + PAD;
let need_h = h + PAD;
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
let page = &mut self.pages[i];
if page.x + need_w > PAGE {
page.y += page.shelf_height;
page.x = PAD;
page.shelf_height = 0;
}
let (x, y) = (page.x, page.y);
page.x += need_w;
page.shelf_height = page.shelf_height.max(need_h);
return (i, x, y);
}
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page {
handle,
x: PAD + w + PAD,
y: PAD,
shelf_height: h + PAD,
});
(self.pages.len() - 1, PAD, PAD)
}
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn page_count(&self) -> usize {
self.pages.len()
}
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
}
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
}
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
let w = image.placement.width;
let h = image.placement.height;
match image.content {
Content::Mask => {
for row in 0..h {
for col in 0..w {
let a = image.data[(row * w + col) as usize];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
Content::Color => {
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let px = [
image.data[i],
image.data[i + 1],
image.data[i + 2],
image.data[i + 3],
];
page.put_pixel(x + col, y + row, image::Rgba(px));
}
}
}
Content::SubpixelMask => {
// Preserve readable output if the rasterizer returns a subpixel mask.
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let a = image.data[i + 1];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
}
}
#[derive(Clone, Copy)]
pub struct PlacedGlyph {
pub entry: GlyphEntry,
pub offset: Vec2,
}
+8 -13
View File
@@ -3,21 +3,20 @@ use std::num::NonZero;
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::{HashMap, Vec2}, util::HashMap,
}; };
use data::WindowUniform; use data::WindowUniform;
use wgpu::{ use wgpu::{
util::{BufferInitDescriptor, DeviceExt}, util::{BufferInitDescriptor, DeviceExt},
*, *,
}; };
use winit::dpi::PhysicalSize;
mod atlas;
mod data; mod data;
mod primitive; mod primitive;
mod texture; mod texture;
mod util; mod util;
pub use atlas::*;
pub use data::{Mask, MaskIdx}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
@@ -119,11 +118,10 @@ impl UiRenderNode {
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform { let slice = &[WindowUniform {
width: size.x, width: size.width as f32,
height: size.y, height: size.height as f32,
}]; }];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
@@ -139,10 +137,7 @@ impl UiRenderNode {
source: ShaderSource::Wgsl(SHAPE_SHADER.into()), source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
}); });
let window_uniform = WindowUniform { let window_uniform = WindowUniform::default();
width: config.width as f32,
height: config.height as f32,
};
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]), contents: bytemuck::cast_slice(&[window_uniform]),
@@ -193,7 +188,7 @@ impl UiRenderNode {
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"), label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout].map(Some),
immediate_size: 0, immediate_size: 0,
}); });
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
@@ -202,7 +197,7 @@ impl UiRenderNode {
vertex: VertexState { vertex: VertexState {
module: &shader, module: &shader,
entry_point: Some("vs_main"), entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()], buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(), compilation_options: Default::default(),
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
+1 -17
View File
@@ -93,7 +93,7 @@ macro_rules! primitives {
} }
)* )*
}; };
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) }; (@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t:tt) => { 1 }; (@count $t:tt) => { 1 };
} }
@@ -201,7 +201,6 @@ impl PrimitiveHandle {
primitives!( primitives!(
rects: RectPrimitive => 0, rects: RectPrimitive => 0,
textures: TexturePrimitive => 1, textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
); );
#[repr(C)] #[repr(C)]
@@ -231,21 +230,6 @@ pub struct TexturePrimitive {
pub sampler_idx: u32, pub sampler_idx: u32,
} }
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
pub view_idx: u32,
pub sampler_idx: u32,
pub color: Color<u8>,
pub flags: u32,
}
impl GlyphPrimitive {
pub const IS_COLOR: u32 = 1;
}
pub struct PrimitiveVec<T> { pub struct PrimitiveVec<T> {
vec: Vec<T>, vec: Vec<T>,
free: Vec<usize>, free: Vec<usize>,
+5 -29
View File
@@ -1,6 +1,7 @@
enable wgpu_binding_array;
const RECT: u32 = 0u; const RECT: u32 = 0u;
const TEXTURE: u32 = 1u; const TEXTURE: u32 = 1u;
const GLYPH: u32 = 2u;
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@@ -8,8 +9,6 @@ var<uniform> window: WindowUniform;
var<storage> rects: array<Rect>; var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE) @group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>; var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect { struct Rect {
color: u32, color: u32,
@@ -23,15 +22,6 @@ struct TextureInfo {
sampler_idx: u32, sampler_idx: u32,
} }
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
view_idx: u32,
sampler_idx: u32,
color: u32,
flags: u32,
}
struct Mask { struct Mask {
x: UiSpan, x: UiSpan,
y: UiSpan, y: UiSpan,
@@ -77,9 +67,9 @@ struct VertexOutput {
@location(0) top_left: vec2<f32>, @location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>, @location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>, @location(2) uv: vec2<f32>,
@location(3) binding: u32, @location(3) @interpolate(flat) binding: u32,
@location(4) idx: u32, @location(4) @interpolate(flat) idx: u32,
@location(5) mask_idx: u32, @location(5) @interpolate(flat) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
}; };
@@ -137,9 +127,6 @@ fn fs_main(
case TEXTURE: { case TEXTURE: {
color = draw_texture(region, textures[i]); color = draw_texture(region, textures[i]);
} }
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
}
default: { default: {
color = vec4(1.0, 0.0, 1.0, 1.0); color = vec4(1.0, 0.0, 1.0, 1.0);
} }
@@ -163,17 +150,6 @@ fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv); return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
} }
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
let uv = mix(g.uv_min, g.uv_max, region.uv);
let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
if (g.flags & 1u) != 0u {
return texel;
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return color;
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> { fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color); var color = unpack4x8unorm(rect.color);
+13 -75
View File
@@ -1,13 +1,11 @@
use image::{DynamicImage, EncodableLayout, GenericImageView}; use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures}; use crate::{TextureUpdate, Textures};
pub struct GpuTextures { pub struct GpuTextures {
device: Device, device: Device,
queue: Queue, queue: Queue,
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>, views: Vec<TextureView>,
view_count: usize, view_count: usize,
samplers: Vec<Sampler>, samplers: Vec<Sampler>,
@@ -19,95 +17,37 @@ impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool { pub fn update(&mut self, textures: &mut Textures) -> bool {
let mut changed = false; let mut changed = false;
for update in textures.updates() { for update in textures.updates() {
changed = true;
match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(image) => self.push(image),
self.push(image); TextureUpdate::Set(i, image) => self.set(i, image),
changed = true; TextureUpdate::SetFree => self.view_count += 1,
} TextureUpdate::Free(i) => self.free(i),
TextureUpdate::Set(i, image) => { TextureUpdate::PushFree => self.push_free(),
self.set(i, image);
changed = true;
}
TextureUpdate::Patch(i, rect, image) => {
// Patching contents leaves the binding array unchanged.
self.patch(i, rect, image);
}
TextureUpdate::SetFree => {
self.view_count += 1;
changed = true;
}
TextureUpdate::Free(i) => {
self.free(i);
changed = true;
}
TextureUpdate::PushFree => {
self.push_free();
changed = true;
}
} }
} }
changed changed
} }
fn set(&mut self, i: u32, image: &DynamicImage) { fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1; self.view_count += 1;
let (texture, view) = self.create(image); let view = self.create_view(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view; self.views[i as usize] = view;
} }
fn free(&mut self, i: u32) { fn free(&mut self, i: u32) {
self.view_count -= 1; self.view_count -= 1;
self.textures[i as usize] = None;
self.views[i as usize] = self.null_view.clone(); self.views[i as usize] = self.null_view.clone();
} }
fn push(&mut self, image: &DynamicImage) { fn push(&mut self, image: &DynamicImage) {
self.view_count += 1; self.view_count += 1;
let (texture, view) = self.create(image); let view = self.create_view(image);
self.textures.push(Some(texture));
self.views.push(view); self.views.push(view);
} }
fn push_free(&mut self) { fn push_free(&mut self) {
self.view_count += 1; self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone()); self.views.push(self.null_view.clone());
} }
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { fn create_view(&self, image: &DynamicImage) -> TextureView {
let Some(texture) = &self.textures[i as usize] else {
return;
};
if rect.width == 0 || rect.height == 0 {
return;
}
// `write_texture` requires tightly packed rows, unlike the atlas image.
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
aspect: TextureAspect::All,
},
sub.as_bytes(),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width * 4),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
}
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
let image = image.to_rgba8(); let image = image.to_rgba8();
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
@@ -123,14 +63,13 @@ impl GpuTextures {
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm, format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
}, },
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
image.as_bytes(), image.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); texture.create_view(&TextureViewDescriptor::default())
(texture, view)
} }
pub fn new(device: &Device, queue: &Queue) -> Self { pub fn new(device: &Device, queue: &Queue) -> Self {
@@ -138,7 +77,6 @@ impl GpuTextures {
Self { Self {
device: device.clone(), device: device.clone(),
queue: queue.clone(), queue: queue.clone(),
textures: Vec::new(),
views: Vec::new(), views: Vec::new(),
samplers: vec![default_sampler(device)], samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()], no_views: vec![null_view.clone()],
+1 -1
View File
@@ -28,7 +28,7 @@ pub trait UiRsc {
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_remove(&mut self, id: WidgetId) {} fn on_remove(&mut self, id: WidgetId) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_draw(&mut self, active: &ActiveData) {} fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {} fn on_undraw(&mut self, active: &ActiveData) {}
+5 -38
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::Vec2, util::Vec2,
}; };
@@ -90,43 +90,10 @@ impl<'a> Painter<'a> {
self.primitive_at(handle.primitive(), region); self.primitive_at(handle.primitive(), region);
} }
pub fn render_text( /// returns (handle, offset from top left)
&mut self, pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
let ui = self.rsc.ui_mut(); let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width, &mut ui.textures) ui.text.draw(buffer, attrs, &mut ui.textures)
}
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let flags_for = |is_color| {
if is_color {
GlyphPrimitive::IS_COLOR
} else {
0
}
};
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
region.y.end = region.y.start;
let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at(
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
view_idx: glyph.entry.view_idx,
sampler_idx: glyph.entry.sampler_idx,
color: text.color,
flags: flags_for(glyph.entry.is_color),
},
region,
);
}
} }
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
+5 -8
View File
@@ -52,7 +52,7 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.needs_full_redraw(root) { if self.root_changed(root) || self.resized {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
self.resized = false; self.resized = false;
@@ -81,10 +81,12 @@ impl UiRenderState {
old_children: Option<Vec<WidgetId>>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let mut redrawn = old_children.is_some();
let mut old_children = old_children.unwrap_or_default(); let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id) if let Some(active) = self.active.get_mut(&id)
&& !rsc.widgets().needs_redraw.contains(&id) && !rsc.widgets().needs_redraw.contains(&id)
{ {
redrawn = true;
// check to see if we can skip drawing first // check to see if we can skip drawing first
if active.region == region { if active.region == region {
return; return;
@@ -149,7 +151,7 @@ impl UiRenderState {
} }
} }
rsc.on_draw(&active); rsc.on_draw(&active, redrawn);
self.active.insert(id, active); self.active.insert(id, active);
} }
@@ -218,17 +220,12 @@ impl UiRenderState {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.needs_full_redraw(root) || widgets.has_updates() self.root_changed(root) || widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
+2 -7
View File
@@ -76,13 +76,8 @@ impl SizeCtx<'_> {
self.output_size self.output_size
} }
pub fn draw_text( pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
&mut self, self.text.draw(buffer, attrs, self.textures)
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width, self.textures)
} }
pub fn label(&self, id: WidgetId) -> &String { pub fn label(&self, id: WidgetId) -> &String {
+1 -1
View File
@@ -16,7 +16,7 @@ pub use id::*;
pub use math::*; pub use math::*;
pub use refcount::*; pub use refcount::*;
pub use slot::*; pub use slot::*;
pub(crate) use trust::*; pub use trust::*;
pub use typemap::*; pub use typemap::*;
pub use vec2::*; pub use vec2::*;
+3 -3
View File
@@ -1,15 +1,15 @@
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T { pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) } unsafe { std::mem::transmute::<&T, &T>(x) }
} }
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T { pub unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) } unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
} }
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)] #[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T { pub unsafe fn to_mut<T>(x: &T) -> &mut T {
#[allow(mutable_transmutes)] #[allow(mutable_transmutes)]
unsafe { unsafe {
std::mem::transmute::<&T, &mut T>(x) std::mem::transmute::<&T, &mut T>(x)
+8 -1
View File
@@ -34,7 +34,7 @@ pub trait HasRoot {
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> { pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
#[track_caller] #[track_caller]
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>; fn add(self, rsc: &mut Rsc) -> WidgetArr<LEN>;
} }
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> { impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
@@ -58,6 +58,13 @@ macro_rules! impl_widget_arr {
) )
} }
} }
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> IntoWidgetVec<Rsc, ($($Tag,)*), ArrTag> for ($($W,)*) {
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
#[allow(non_snake_case)]
let ($($W,)*) = self;
vec![$($W.add(rsc).upgrade(rsc),)*]
}
}
}; };
} }
+14 -1
View File
@@ -1,4 +1,4 @@
use crate::{Axis, AxisT, Len, Painter, SizeCtx}; use crate::{Axis, AxisT, Len, Painter, SizeCtx, UiRsc};
use std::any::Any; use std::any::Any;
mod data; mod data;
@@ -85,3 +85,16 @@ impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> f
self(state) self(state)
} }
} }
pub trait IntoWidgetVec<Rsc, WTag, GTag> {
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget>;
}
impl<Rsc: UiRsc, I: IntoIterator, Tag> IntoWidgetVec<Rsc, Tag, IterTag> for I
where
I::Item: WidgetLike<Rsc, Tag>,
{
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
self.into_iter().map(|w| w.add_strong(rsc).any()).collect()
}
}
+1
View File
@@ -62,3 +62,4 @@ impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
} }
pub struct ArrTag; pub struct ArrTag;
pub struct IterTag;
+1 -5
View File
@@ -10,11 +10,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state); rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
+3 -6
View File
@@ -1,3 +1,4 @@
use cosmic_text::Family;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent; use winit::event::WindowEvent;
@@ -15,11 +16,7 @@ pub struct Client {
} }
impl DefaultAppState for Client { impl DefaultAppState for Client {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
@@ -147,7 +144,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = WidgetPtr::new().add(rsc); let main = WidgetPtr::empty().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
+1 -5
View File
@@ -11,11 +11,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rect = rect(Color::RED).add(rsc); let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
+1 -5
View File
@@ -36,11 +36,7 @@ impl Test {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| { test.on(CursorSense::click(), move |_, rsc| {
+1 -1
View File
@@ -6,7 +6,7 @@ edition.workspace = true
[dependencies] [dependencies]
proc-macro2 = "1.0.103" proc-macro2 = "1.0.103"
quote = "1.0.42" quote = "1.0.42"
syn = { version = "2.0.111", features = ["full"] } syn = { version = "3.0.3", features = ["full"] }
[lib] [lib]
proc-macro = true proc-macro = true
-3
View File
@@ -1,3 +0,0 @@
[toolchain]
channel = "nightly"
components = ["clippy", "rustfmt"]
+8
View File
@@ -7,3 +7,11 @@ impl Event for Submit {}
#[derive(Eq, PartialEq, Hash, Clone)] #[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited; pub struct Edited;
impl Event for Edited {} impl Event for Edited {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Draw;
impl Event for Draw {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Undraw;
impl Event for Undraw {}
+145 -45
View File
@@ -29,7 +29,26 @@ pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub struct EventSender<State: DefaultAppState> {
proxy: EventLoopProxy<UiMainEvent<State>>,
}
impl<State: DefaultAppState> Clone for EventSender<State> {
fn clone(&self) -> Self {
Self {
proxy: self.proxy.clone(),
}
}
}
impl<State: DefaultAppState> EventSender<State> {
pub fn send(&self, event: State::Event) {
let _ = self.proxy.send_event(UiMainEvent::App(event));
}
pub fn run(&self, f: impl MainCallback<State>) {
let _ = self.proxy.send_event(UiMainEvent::Callback(Box::new(f)));
}
}
pub struct DefaultUiState { pub struct DefaultUiState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -70,9 +89,8 @@ pub trait HasDefaultUiState: Sized + 'static {
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DefaultAppState: HasDefaultUiState {
type Event = (); type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self;
-> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(
&mut self, &mut self,
@@ -96,23 +114,54 @@ pub trait DefaultAppState: HasDefaultUiState {
} }
} }
pub struct DefaultRsc<State: 'static> { pub struct DefaultRsc<State: 'static + DefaultAppState> {
pub ui: UiData, pub ui: UiData,
pub events: EventManager<Self>, pub events: EventManager<Self>,
pub tasks: Tasks<Self>, pub tasks: Tasks<Self>,
pub state: WidgetState, pub state: WidgetState,
pub widget_events: Vec<WidgetEvent>,
pub window_event: EventSender<State>,
_state: PhantomData<State>, _state: PhantomData<State>,
} }
impl<State> DefaultRsc<State> { pub struct WidgetEvent {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) { id: WidgetId,
let (tasks, recv) = Tasks::init(window); ty: WidgetEventType,
}
pub enum WidgetEventType {
Draw,
Undraw,
Remove,
}
pub trait MainCallback<State>: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static {}
impl<F: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static, State> MainCallback<State> for F {}
pub enum UiMainEvent<State: DefaultAppState> {
RequestUpdate,
Callback(Box<dyn MainCallback<State>>),
App(State::Event),
}
impl<State: DefaultAppState> DefaultRsc<State> {
fn init(proxy: EventLoopProxy<UiMainEvent<State>>) -> (Self, TaskMsgReceiver<Self>) {
let window_event = EventSender {
proxy: proxy.clone(),
};
let (tasks, recv) = Tasks::init(move || {
if proxy.send_event(UiMainEvent::RequestUpdate).is_err() {
panic!("main thread blew up or smth");
}
});
( (
Self { Self {
ui: Default::default(), ui: Default::default(),
events: Default::default(), events: Default::default(),
tasks, tasks,
widget_events: Default::default(),
state: Default::default(), state: Default::default(),
window_event,
_state: Default::default(), _state: Default::default(),
}, },
recv, recv,
@@ -124,7 +173,7 @@ impl<State> DefaultRsc<State> {
} }
} }
impl<State> UiRsc for DefaultRsc<State> { impl<State: DefaultAppState> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData { fn ui(&self) -> &UiData {
&self.ui &self.ui
} }
@@ -133,25 +182,39 @@ impl<State> UiRsc for DefaultRsc<State> {
&mut self.ui &mut self.ui
} }
fn on_draw(&mut self, active: &ActiveData) { fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {
self.events.draw(active); self.events.draw(active);
if !redrawn {
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Draw,
});
}
} }
fn on_undraw(&mut self, active: &ActiveData) { fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active); self.events.undraw(active);
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Undraw,
});
} }
fn on_remove(&mut self, id: WidgetId) { fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id); self.events.remove(id);
self.state.remove(id); self.state.remove(id);
self.widget_events.push(WidgetEvent {
id,
ty: WidgetEventType::Remove,
});
} }
} }
impl<State: 'static> HasState for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasState for DefaultRsc<State> {
type State = State; type State = State;
} }
impl<State: 'static> HasEvents for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> { fn events(&self) -> &EventManager<Self> {
&self.events &self.events
} }
@@ -161,13 +224,13 @@ impl<State: 'static> HasEvents for DefaultRsc<State> {
} }
} }
impl<State: 'static> HasTasks for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> { fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks &mut self.tasks
} }
} }
impl<State: 'static> HasWidgetState for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState { fn widget_state(&self) -> &WidgetState {
&self.state &self.state
} }
@@ -185,15 +248,15 @@ pub struct DefaultApp<State: DefaultAppState> {
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event; type Event = UiMainEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes()) .create_window(State::window_attributes())
.unwrap(); .unwrap();
let default_state = DefaultUiState::new(window); let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); let (mut rsc, task_recv) = DefaultRsc::init(proxy);
let state = State::new(default_state, &mut rsc, proxy); let state = State::new(default_state, &mut rsc);
let render = UiRenderState::new(); let render = UiRenderState::new();
Self { Self {
rsc, rsc,
@@ -204,38 +267,39 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render); match event {
UiMainEvent::RequestUpdate => {
self.check_updates();
}
UiMainEvent::App(event) => {
self.state.event(event, &mut self.rsc, &mut self.render);
}
UiMainEvent::Callback(f) => f(&mut self.rsc),
}
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { let Self {
rsc, rsc, render, state, ..
render,
state,
task_recv,
} = self; } = self;
for update in task_recv.try_iter() { // input handling
update(state, rsc);
}
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event); if ui_state.input.event(&event) {
let cursor_state = ui_state.cursor_state().clone(); let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus; let old = ui_state.focus;
if cursor_state.buttons.left.is_start() { if cursor_state.buttons.left.is_start() {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed {
let window_size = ui_state.window_size(); let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size); render.run_sensors(rsc, state, cursor_state, window_size);
if old != state.default_state().focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
} }
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
if old != ui_state.focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
@@ -297,11 +361,9 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (), _ => (),
} }
state.window_event(event, rsc, render); state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) { self.check_updates();
ui_state.renderer.window().request_redraw(); self.state.default_state_mut().input.end_frame();
}
ui_state.input.end_frame();
} }
fn exit(&mut self) { fn exit(&mut self) {
@@ -309,13 +371,49 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
} }
impl<State: DefaultAppState> DefaultApp<State> {
pub fn check_updates(&mut self) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let mut events = std::mem::take(&mut rsc.widget_events);
for event in events.drain(..) {
match event.ty {
WidgetEventType::Draw => {
rsc.run_event::<Draw>(event.id, (), state);
}
WidgetEventType::Undraw => {
rsc.run_event::<Undraw>(event.id, (), state);
}
_ => (),
}
}
rsc.widget_events = events;
let ui_state = state.default_state();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
}
}
pub trait RscIdx<Rsc> { pub trait RscIdx<Rsc> {
type Output; type Output;
fn get(self, rsc: &Rsc) -> &Self::Output; fn get(self, rsc: &Rsc) -> &Self::Output;
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output; fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
} }
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> { impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I>
for DefaultRsc<State>
{
type Output = I::Output; type Output = I::Output;
fn index(&self, index: I) -> &Self::Output { fn index(&self, index: I) -> &Self::Output {
@@ -323,7 +421,9 @@ impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for Defaul
} }
} }
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> { impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I>
for DefaultRsc<State>
{
fn index_mut(&mut self, index: I) -> &mut Self::Output { fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self) index.get_mut(self)
} }
+15 -7
View File
@@ -22,7 +22,11 @@ impl UiRenderer {
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap(); let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(v) => v,
CurrentSurfaceTexture::Suboptimal(v) => v,
_ => panic!("failed"),
};
let view = output let view = output
.texture .texture
.create_view(&TextureViewDescriptor::default()); .create_view(&TextureViewDescriptor::default());
@@ -45,15 +49,14 @@ impl UiRenderer {
} }
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.queue.present(output);
output.present();
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width; self.config.width = size.width;
self.config.height = size.height; self.config.height = size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
self.ui.resize((size.width, size.height), &self.queue); self.ui.resize(size, &self.queue);
} }
fn create_encoder(device: &Device) -> CommandEncoder { fn create_encoder(device: &Device) -> CommandEncoder {
@@ -65,9 +68,12 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self { pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size(); let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor { let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY, backends: Backends::PRIMARY,
..Default::default() flags: Default::default(),
memory_budget_thresholds: Default::default(),
backend_options: Default::default(),
display: None,
}); });
let surface = instance let surface = instance
@@ -79,6 +85,7 @@ impl UiRenderer {
power_preference: PowerPreference::default(), power_preference: PowerPreference::default(),
compatible_surface: Some(&surface), compatible_surface: Some(&surface),
force_fallback_adapter: false, force_fallback_adapter: false,
apply_limit_buckets: false,
}) })
.block_on() .block_on()
.expect("Could not get adapter!"); .expect("Could not get adapter!");
@@ -116,10 +123,11 @@ impl UiRenderer {
format: surface_format, format: surface_format,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoNoVsync,
alpha_mode: surface_caps.alpha_modes[0], alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2, desired_maximum_frame_latency: 2,
view_formats: vec![], view_formats: vec![],
color_space: Default::default(),
}; };
surface.configure(&device, &config); surface.configure(&device, &config);
+5 -6
View File
@@ -13,7 +13,6 @@ use tokio::{
unbounded_channel as async_channel, unbounded_channel as async_channel,
}, },
}; };
use winit::window::Window;
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
@@ -23,7 +22,7 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
window: Arc<Window>, request_update: Arc<dyn Fn() + Send + Sync>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>, msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
@@ -45,7 +44,7 @@ impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) { pub fn init(request_update: impl Fn() + 'static + Send + Sync) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel(); let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
@@ -56,7 +55,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self { Self {
start, start,
msg_send: msgs, msg_send: msgs,
window, request_update: Arc::new(request_update),
}, },
msgs_recv, msgs_recv,
) )
@@ -67,10 +66,10 @@ impl<Rsc: HasState> Tasks<Rsc> {
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let send = self.msg_send.clone(); let send = self.msg_send.clone();
let window = self.window.clone(); let request_update = self.request_update.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await; task(TaskCtx::new(send)).await;
window.request_redraw(); request_update();
})); }));
} }
} }
+1
View File
@@ -1,5 +1,6 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
+2
View File
@@ -5,6 +5,7 @@ mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod selector;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
@@ -13,3 +14,4 @@ pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use selector::*;
+9 -9
View File
@@ -152,32 +152,32 @@ impl Span {
} }
} }
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct SpanBuilder<Children, Rsc, Tag, GTag> {
pub children: Wa, pub children: Children,
pub dir: Dir, pub dir: Dir,
pub gap: f32, pub gap: f32,
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(Rsc, Tag, GTag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
for SpanBuilder<Rsc, LEN, Wa, Tag> for SpanBuilder<Children, Rsc, Tag, GTag>
{ {
type Widget = Span; type Widget = Span;
#[track_caller] #[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget { fn run(self, rsc: &mut Rsc) -> Self::Widget {
Span { Span {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.into_vec(rsc),
dir: self.dir, dir: self.dir,
gap: self.gap, gap: self.gap,
} }
} }
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
SpanBuilder<State, LEN, Wa, Tag> SpanBuilder<Children, Rsc, Tag, GTag>
{ {
pub fn new(children: Wa, dir: Dir) -> Self { pub fn new(children: Children, dir: Dir) -> Self {
Self { Self {
children, children,
dir, dir,
+8 -10
View File
@@ -42,30 +42,28 @@ pub enum StackSize {
Child(usize), Child(usize),
} }
pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct StackBuilder<Children, Rsc, Tag, GTag> {
pub children: Wa, pub children: Children,
pub size: StackSize, pub size: StackSize,
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(Rsc, Tag, GTag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
for StackBuilder<Rsc, LEN, Wa, Tag> for StackBuilder<Children, Rsc, Tag, GTag>
{ {
type Widget = Stack; type Widget = Stack;
#[track_caller] #[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget { fn run(self, rsc: &mut Rsc) -> Self::Widget {
Stack { Stack {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.into_vec(rsc),
size: self.size, size: self.size,
} }
} }
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> StackBuilder<Children, Rsc, Tag, GTag> {
StackBuilder<State, LEN, Wa, Tag> pub fn new(children: Children) -> Self {
{
pub fn new(children: Wa) -> Self {
Self { Self {
children, children,
size: StackSize::default(), size: StackSize::default(),
+4 -2
View File
@@ -30,8 +30,10 @@ impl Widget for WidgetPtr {
} }
impl WidgetPtr { impl WidgetPtr {
pub fn new() -> Self { pub fn new(widget: StrongWidget) -> Self {
Self::default() Self {
inner: Some(widget),
}
} }
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self {
+48
View File
@@ -0,0 +1,48 @@
use std::hash::Hash;
use iris_core::util::HashMap;
use crate::prelude::*;
pub struct WidgetSelector<T> {
current: (T, StrongWidget),
map: HashMap<T, StrongWidget>,
}
impl<T: Hash + Eq> WidgetSelector<T> {
pub fn new(key: T, widget: StrongWidget) -> Self {
Self {
current: (key, widget),
map: Default::default(),
}
}
pub fn set(&mut self, key: T, widget: StrongWidget) {
self.map.insert(key, widget);
}
pub fn select(&mut self, key: T) -> bool {
if let Some(val) = self.map.remove(&key) {
let mut new = (key, val);
std::mem::swap(&mut new, &mut self.current);
self.map.insert(new.0, new.1);
true
} else {
false
}
}
}
impl<T: 'static> Widget for WidgetSelector<T> {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.current.1);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.current.1)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.current.1)
}
}
+20 -5
View File
@@ -1,4 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics};
use std::marker::{PhantomData, Sized}; use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
@@ -19,7 +20,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
self.attrs.color = color; self.attrs.color = color;
self self
} }
pub fn family(mut self, family: Family) -> Self { pub fn family(mut self, family: Family<'static>) -> Self {
self.attrs.family = family; self.attrs.family = family;
self self
} }
@@ -81,13 +82,19 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
state: &mut Rsc, state: &mut Rsc,
builder: TextBuilder<Rsc, Self, H>, builder: TextBuilder<Rsc, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let hint = builder.hint.get(state); let hint = builder.hint.get(state);
let font_system = &mut state.ui_mut().text.font_system;
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
let mut text = Text { let mut text = Text {
content: builder.content.into(), content: builder.content.into(),
view: TextView::new(buf, builder.attrs, hint), view: TextView::new(buf, builder.attrs, hint),
}; };
text.content.changed = false; text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text text
} }
} }
@@ -103,11 +110,19 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
state: &mut State, state: &mut State,
builder: TextBuilder<State, Self, H>, builder: TextBuilder<State, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let buf = TextBuffer::new_empty(Metrics::new(
TextEdit::new( builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)), TextView::new(buf, builder.attrs, builder.hint.get(state)),
builder.output.mode, builder.output.mode,
) );
let font_system = &mut state.ui_mut().text.font_system;
text.buf
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
builder.attrs.apply(font_system, &mut text.buf, None);
text
} }
} }
+390 -249
View File
@@ -1,30 +1,17 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::{TextData, UiColor}; use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use parley::{Affinity, Layout, Selection};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use unicode_segmentation::UnicodeSegmentation;
use winit::{ use winit::{
event::KeyEvent, event::KeyEvent,
keyboard::{Key, NamedKey}, keyboard::{Key, NamedKey},
}; };
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Motion {
Left,
Right,
LeftWord,
RightWord,
Up,
Down,
LineStart,
LineEnd,
}
pub struct TextEdit { pub struct TextEdit {
view: TextView, view: TextView,
/// `None` represents unfocused, which Parley's `Selection` cannot express. selection: TextSelection,
selection: Option<Selection>, history: Vec<(String, TextSelection)>,
history: Vec<(String, Option<Selection>)>, double_hit: Option<Cursor>,
double_hit: Option<usize>,
pub mode: EditMode, pub mode: EditMode,
} }
@@ -38,19 +25,27 @@ impl TextEdit {
pub fn new(view: TextView, mode: EditMode) -> Self { pub fn new(view: TextView, mode: EditMode) -> Self {
Self { Self {
view, view,
selection: None, selection: Default::default(),
history: Default::default(), history: Default::default(),
double_hit: None, double_hit: None,
mode, mode,
} }
} }
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
pub fn selected_text(&self) -> Option<String> { let (start, end) = sort_cursors(start, end);
let sel = self.selection?; let mut iter = self.buf.lines.iter().skip(start.line);
if sel.is_collapsed() { let first = iter.next().unwrap();
return None; if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
} }
Some(self.buf.text()[sel.text_range()].to_string())
} }
} }
@@ -62,29 +57,39 @@ impl Widget for TextEdit {
painter.layer = base; painter.layer = base;
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let size = vec2(1, self.attrs.line_height);
return; match self.selection {
}; TextSelection::None => (),
let layout = self.view.buf.layout(); TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
// parley reports selection as boxes in layout space, so bidi and painter.primitive_within(
// wrapped lines come out right without this code knowing about either. RectPrimitive::color(Color::WHITE),
for (rect, _) in selection.geometry(layout) { size.align(Align::TOP_LEFT).offset(offset).within(&region),
let size = vec2(rect.width() as f32, rect.height() as f32); );
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); }
painter.primitive_within( }
RectPrimitive::color(Color::SKY), TextSelection::Span { start, end } => {
size.align(Align::TOP_LEFT).offset(top_left).within(&region), let (start, end) = sort_cursors(start, end);
); for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
let top_left = vec2(x, self.attrs.line_height * l as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.with_x(width)
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
}
if let Some(end_offset) = cursor_pos(end, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT)
.offset(end_offset)
.within(&region),
);
}
}
} }
let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
@@ -96,58 +101,154 @@ impl Widget for TextEdit {
} }
} }
const CARET_WIDTH: f32 = 1.0; /// provides top left + width
fn iter_layout_lines(
start: Cursor,
end: Cursor,
buf: &TextBuffer,
) -> impl Iterator<Item = (usize, f32, f32)> {
gen move {
let mut iter = buf.layout_runs().enumerate();
for (i, line) in iter.by_ref() {
if line.line_i == start.line
&& let Some(start_x) = index_x(&line, start.index)
{
if start.line == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, start_x, end_x - start_x);
return;
}
yield (i, start_x, line.line_w - start_x);
break;
}
}
for (i, line) in iter {
if line.line_i > end.line {
return;
}
if line.line_i == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, 0.0, end_x);
return;
}
yield (i, 0.0, line.line_w);
}
}
}
/// copied & modified from fn found in Editor in cosmic_text
/// returns x pos of a (non layout) index within an layout run
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
for glyph in run.glyphs.iter() {
if index == glyph.start {
return Some(glyph.x);
} else if index > glyph.start && index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset);
}
}
None
}
/// returns top of line segment where cursor should visually select
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some(vec2(run.line_w, run.line_top));
if let Some(pos) = index_x(&run, cursor.index) {
return Some(vec2(pos, run.line_top));
}
}
prev
}
pub struct TextEditCtx<'a> { pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit, pub text: &'a mut TextEdit,
pub data: &'a mut TextData, pub font_system: &'a mut FontSystem,
} }
impl<'a> TextEditCtx<'a> { impl<'a> TextEditCtx<'a> {
fn layout(&mut self) -> &Layout<UiColor> {
let attrs = self.text.view.attrs.clone();
let width = self.text.view.wrap_width();
self.text.view.buf.shape(self.data, &attrs, width);
self.text.view.buf.layout()
}
fn refresh(&mut self) {
if let Some(sel) = self.text.selection {
let layout = self.layout();
self.text.selection = Some(sel.refresh(layout));
}
}
pub fn take(&mut self) -> String { pub fn take(&mut self) -> String {
let text = self.text.view.buf.text().to_string(); let text = self
self.set(""); .text
.buf
.lines
.drain(..)
.map(|l| l.into_text())
.collect::<Vec<_>>()
.join("\n");
self.text
.buf
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
self.text.selection.clear();
text text
} }
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.text
self.text.view.buf.changed = true; .buf
self.text.selection = None; .set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
} }
pub fn motion(&mut self, motion: Motion, select: bool) { pub fn motion(&mut self, motion: Motion, select: bool) {
let Some(sel) = self.text.selection else { if let TextSelection::Pos(cursor) = self.text.selection
return; && let Some(new) = self.buf_motion(cursor, motion)
}; {
let layout = self.layout(); if select {
let sel = apply_motion(sel, layout, motion, select); self.text.selection = TextSelection::Span {
self.text.selection = Some(sel); start: cursor,
end: new,
};
} else {
self.text.selection = TextSelection::Pos(new);
}
} else if let TextSelection::Span { start, end } = self.text.selection {
if select {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Span { start, end: cursor };
}
} else {
let (start, end) = sort_cursors(start, end);
let sel = &mut self.text.selection;
match motion {
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
_ => {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Pos(cursor);
}
}
}
}
}
} }
/// Replace the `len` characters before the caret. This is the IME's
/// preedit path: it re-sends the whole composition each time.
pub fn replace(&mut self, len: usize, text: &str) { pub fn replace(&mut self, len: usize, text: &str) {
let text = self.string(text); let text = self.string(text);
for _ in 0..len { for _ in 0..len {
self.backspace(false); self.delete(false);
} }
self.insert_str(&text); self.insert_inner(&text, false);
} }
fn string(&self, text: &str) -> String { fn string(&self, text: &str) -> String {
@@ -160,183 +261,202 @@ impl<'a> TextEditCtx<'a> {
pub fn insert(&mut self, text: &str) { pub fn insert(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.insert_str(&text); let mut lines = text.split('\n');
} let Some(first) = lines.next() else {
fn insert_str(&mut self, text: &str) {
if text.is_empty() {
return; return;
}
self.clear_span();
let at = match self.text.selection {
Some(sel) => sel.focus().index(),
None => return,
}; };
let at = at.min(self.text.view.buf.text().len()); self.insert_inner(first, true);
self.text.view.buf.edit().insert_str(at, text); for line in lines {
self.text.view.buf.changed = true; self.newline();
self.set_caret(at + text.len()); self.insert_inner(line, true);
}
} }
pub fn clear_span(&mut self) -> bool { pub fn clear_span(&mut self) -> bool {
let Some(sel) = self.text.selection else { if let TextSelection::Span { start, end } = self.text.selection {
return false; self.delete_between(start, end);
}; let (start, _) = sort_cursors(start, end);
if sel.is_collapsed() { self.text.selection = TextSelection::Pos(start);
return false; true
} else {
false
} }
let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start);
true
} }
fn set_caret(&mut self, index: usize) { pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
let index = index.min(self.text.view.buf.text().len()); let lines = &mut self.text.view.buf.lines;
let layout = self.layout(); let (start, end) = sort_cursors(start, end);
self.text.selection = Some(Selection::from_byte_index( if start.line == end.line {
layout, let line = &mut lines[start.line];
index, let text = line.text();
Affinity::default(), let text = text[..start.index].to_string() + &text[end.index..];
)); edit_line(line, text);
} else {
// start
let start_text = lines[start.line].text()[..start.index].to_string();
let end_text = &lines[end.line].text()[end.index..];
let text = start_text + end_text;
edit_line(&mut lines[start.line], text);
}
// between
let range = (start.line + 1)..=end.line;
if !range.is_empty() {
lines.splice(range, None);
}
}
fn insert_inner(&mut self, text: &str, mov: bool) {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
edit_line(line, line_text);
if mov {
for _ in 0..text.chars().count() {
self.motion(Motion::Right, false);
}
}
}
} }
pub fn newline(&mut self) { pub fn newline(&mut self) {
if self.text.mode == EditMode::MultiLine { if self.text.mode == EditMode::SingleLine {
self.insert_str("\n"); return;
}
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
cursor.line += 1;
lines.insert(cursor.line, new);
cursor.index = 0;
} }
} }
pub fn backspace(&mut self, word: bool) { pub fn backspace(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
self.delete(word);
} }
let Some(sel) = self.text.selection else {
return;
};
let end = sel.focus().index();
if end == 0 {
return;
}
let start = {
let layout = self.layout();
if word {
sel.focus().previous_logical_word(layout).index()
} else {
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
return;
};
let range = cluster.text_range();
if cluster.is_hard_line_break() || cluster.is_emoji() {
range.start
} else {
self.text.view.buf.text()[..range.end]
.char_indices()
.next_back()
.map_or(range.start, |(start, _)| start)
}
}
};
self.delete_range(start, end);
} }
pub fn delete(&mut self, word: bool) { pub fn delete(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
} {
let Some(sel) = self.text.selection else {
return;
};
let start = sel.focus().index();
if start >= self.text.view.buf.text().len() {
return;
}
let end = {
let layout = self.layout();
if word { if word {
sel.focus().next_logical_word(layout).index() let start = *cursor;
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
self.delete_between(start, end);
}
} else { } else {
let clusters = sel.focus().logical_clusters(layout); let lines = &mut self.text.view.buf.lines;
let Some(cluster) = clusters[1].as_ref() else { let line = &mut lines[cursor.line];
return; if cursor.index == line.text().len() {
}; if cursor.line == lines.len() - 1 {
cluster.text_range().end return;
}
let add = lines.remove(cursor.line + 1).into_text();
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
edit_line(line, cur);
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
edit_line(line, text);
}
} }
}; }
self.delete_range(start, end);
} }
fn delete_range(&mut self, start: usize, end: usize) { fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
let len = self.text.view.buf.text().len(); self.text
let (start, end) = (start.min(end).min(len), start.max(end).min(len)); .buf
if start == end { .cursor_motion(self.font_system, cursor, None, motion)
return; .map(|r| r.0)
}
self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start);
} }
pub fn select_all(&mut self) { pub fn select_word_at(&mut self, cursor: Cursor) {
let len = self.text.view.buf.text().len(); if let (Some(start), Some(end)) = (
if len == 0 { self.buf_motion(cursor, Motion::LeftWord),
return; self.buf_motion(cursor, Motion::RightWord),
) {
self.text.selection = TextSelection::Span { start, end };
}
}
pub fn select_line_at(&mut self, cursor: Cursor) {
let end = self.text.buf.lines[cursor.line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(cursor.line, 0),
end: Cursor::new(cursor.line, end),
} }
let layout = self.layout();
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
self.text.selection = Some(Selection::new(anchor, focus));
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_abs(size); let pos = pos - self.text.region().top_left().to_abs(size);
let prev_sel = self.text.selection; let hit = self.text.buf.hit(pos.x, pos.y);
let prev_hit = self.text.double_hit; let sel = &mut self.text.selection;
match sel {
let outcome = { TextSelection::None => {
let layout = self.layout(); if !drag && let Some(hit) = hit {
if drag { *sel = TextSelection::Pos(hit)
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit)) }
} else {
let hit = Selection::from_point(layout, pos.x, pos.y);
let index = hit.focus().index();
// Successive clicks at one index select the word, then the line.
Some(if recent && prev_hit == Some(index) {
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
(
Some(Selection::word_from_point(layout, pos.x, pos.y)),
Some(index),
)
} else {
(Some(hit), None)
})
} }
}; TextSelection::Pos(pos) => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
if let Some((selection, double_hit)) = outcome { (None, true) => (),
self.text.selection = selection; (Some(hit), false) => {
self.text.double_hit = double_hit; if recent && hit == *pos {
self.text.double_hit = Some(hit);
return self.select_word_at(hit);
} else {
*pos = hit
}
}
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
},
TextSelection::Span { start, end } => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => *sel = TextSelection::Pos(*start),
(Some(hit), false) => {
if recent
&& let Some(double) = self.text.double_hit
&& double == hit
{
return self.select_line_at(hit);
} else {
*sel = TextSelection::Pos(hit)
}
}
(Some(hit), true) => *end = hit,
},
}
if let TextSelection::Span { start, end } = sel
&& start == end
{
*sel = TextSelection::Pos(*start);
} }
} }
pub fn deselect(&mut self) { pub fn deselect(&mut self) {
self.text.selection = None; self.text.selection = TextSelection::None;
self.text.double_hit = None;
} }
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.view.buf.text().to_string(), self.text.selection); let old = (self.text.content(), self.text.selection);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo && let Some((old, selection)) = self.text.history.pop() {
if let Some((old, selection)) = self.text.history.pop() { self.set(&old);
self.set(&old); self.text.selection = selection;
self.text.selection = selection; } else if self.text.content() != old.0 {
self.refresh();
}
} else if self.text.view.buf.text() != old.0 {
self.text.history.push(old); self.text.history.push(old);
} }
res res
@@ -361,25 +481,21 @@ impl<'a> TextEditCtx<'a> {
} }
} }
NamedKey::ArrowRight => { NamedKey::ArrowRight => {
let motion = if modifiers.control { if modifiers.control {
Motion::RightWord self.motion(Motion::RightWord, modifiers.shift)
} else { } else {
Motion::Right self.motion(Motion::Right, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowLeft => { NamedKey::ArrowLeft => {
let motion = if modifiers.control { if modifiers.control {
Motion::LeftWord self.motion(Motion::LeftWord, modifiers.shift)
} else { } else {
Motion::Left self.motion(Motion::Left, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
NamedKey::Escape => { NamedKey::Escape => {
self.deselect(); self.deselect();
return TextInputResult::Unfocus; return TextInputResult::Unfocus;
@@ -391,18 +507,34 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() { match text.as_str() {
"v" => return TextInputResult::Paste, "v" => return TextInputResult::Paste,
"c" => { "c" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"x" => { "x" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
self.clear_span(); self.clear_span();
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"a" => self.select_all(), "a" => {
"z" => *undo = true, if !self.text.buf.lines[0].text().is_empty()
|| self.text.buf.lines.len() > 1
{
let lines = &self.text.buf.lines;
let last_line = lines.len() - 1;
let last_idx = lines[last_line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(0, 0),
end: Cursor::new(last_line, last_idx),
};
}
}
"z" => {
*undo = true;
}
_ => self.insert(text), _ => self.insert(text),
} }
} else { } else {
@@ -415,24 +547,6 @@ impl<'a> TextEditCtx<'a> {
} }
} }
fn apply_motion(
sel: Selection,
layout: &Layout<UiColor>,
motion: Motion,
extend: bool,
) -> Selection {
match motion {
Motion::Left => sel.previous_visual(layout, extend),
Motion::Right => sel.next_visual(layout, extend),
Motion::LeftWord => sel.previous_visual_word(layout, extend),
Motion::RightWord => sel.next_visual_word(layout, extend),
Motion::Up => sel.previous_line(layout, extend),
Motion::Down => sel.next_line(layout, extend),
Motion::LineStart => sel.line_start(layout, extend),
Motion::LineEnd => sel.line_end(layout, extend),
}
}
#[derive(Default)] #[derive(Default)]
pub struct Modifiers { pub struct Modifiers {
pub shift: bool, pub shift: bool,
@@ -455,6 +569,33 @@ pub enum TextInputResult {
Paste, Paste,
} }
#[derive(Debug, Default, Clone, Copy)]
pub enum TextSelection {
#[default]
None,
Pos(Cursor),
Span {
start: Cursor,
end: Cursor,
},
}
impl TextSelection {
pub fn clear(&mut self) {
match self {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
TextSelection::Span { start: _, end: _ } => {
*self = TextSelection::None;
}
}
}
}
impl TextInputResult { impl TextInputResult {
pub fn unfocus(&self) -> bool { pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus) matches!(self, TextInputResult::Unfocus)
@@ -484,7 +625,7 @@ impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
let ui = ui.ui_mut(); let ui = ui.ui_mut();
TextEditCtx { TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(), text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text, font_system: &mut ui.text.font_system,
} }
} }
} }
+55 -26
View File
@@ -6,8 +6,11 @@ pub use edit::*;
use iris_core::util::MutDetect; use iris_core::util::MutDetect;
use crate::prelude::*; use crate::prelude::*;
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
pub const SHAPING: Shaping = Shaping::Advanced;
pub struct Text { pub struct Text {
pub content: MutDetect<String>, pub content: MutDetect<String>,
view: TextView, view: TextView,
@@ -22,16 +25,6 @@ pub struct TextView {
pub hint: Option<StrongWidget>, pub hint: Option<StrongWidget>,
} }
impl TextView {
fn is_blank(&self) -> bool {
self.buf.is_empty()
}
pub fn wrap_width(&self) -> Option<f32> {
self.width
}
}
impl TextView { impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self { Self {
@@ -52,6 +45,15 @@ impl TextView {
.align(self.align) .align(self.align)
} }
fn tex_region(&self, tex: &RenderedText) -> UiRegion {
let region = tex.size.align(self.align);
let dims = tex.handle.size();
let mut region = region.offset(tex.top_left_offset);
region.x.end = region.x.start + UiScalar::abs(dims.x);
region.y.end = region.y.start + UiScalar::abs(dims.y);
region
}
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText { fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
let width = if self.attrs.wrap { let width = if self.attrs.wrap {
Some(ctx.px_size().x) Some(ctx.px_size().x)
@@ -66,7 +68,10 @@ impl TextView {
return tex.clone(); return tex.clone();
} }
self.width = width; self.width = width;
let tex = ctx.draw_text(&mut self.buf, &self.attrs, width); let font_system = &mut ctx.text.font_system;
self.attrs.apply(font_system, &mut self.buf, width);
self.buf.shape_until_scroll(font_system, false);
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
self.tex = Some(tex.clone()); self.tex = Some(tex.clone());
self.attrs.changed = false; self.attrs.changed = false;
self.buf.changed = false; self.buf.changed = false;
@@ -76,8 +81,9 @@ impl TextView {
self.tex.as_ref() self.tex.as_ref()
} }
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
ctx.width(hint) ctx.width(hint)
} else { } else {
@@ -85,8 +91,9 @@ impl TextView {
} }
} }
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
ctx.height(hint) ctx.height(hint)
} else { } else {
@@ -95,35 +102,47 @@ impl TextView {
} }
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion { pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx()); let tex = self.render(&mut painter.size_ctx());
let region = tex.size.align(self.align); let region = self.tex_region(&tex);
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
painter.widget(hint); painter.widget(hint);
} else { } else {
let within = region.within(&painter.region()); painter.texture_within(&tex.handle, region);
painter.glyphs(&tex, within);
} }
region region
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_string() self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
} }
} }
impl Text { impl Text {
pub fn new(content: impl Into<String>) -> Self { pub fn new(content: impl Into<String>) -> Self {
let content: String = content.into(); let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
Self { Self {
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None), content: content.into().into(),
content: content.into(), view: TextView::new(buf, attrs, None),
} }
} }
fn update_buf(&mut self, _ctx: &mut SizeCtx) { fn update_buf(&mut self, ctx: &mut SizeCtx) {
if self.content.changed { if self.content.changed {
self.content.changed = false; self.content.changed = false;
self.view.buf.set_text(self.content.as_str()); self.view.buf.set_text(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
} }
} }
} }
@@ -145,6 +164,16 @@ impl Widget for Text {
} }
} }
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
let start = a.min(b);
let end = a.max(b);
(start, end)
}
pub fn edit_line(line: &mut BufferLine, text: String) {
line.set_text(text, line.ending(), line.attrs_list().clone());
}
impl Deref for Text { impl Deref for Text {
type Target = TextAttrs; type Target = TextAttrs;
+50 -7
View File
@@ -131,18 +131,61 @@ widget_trait! {
} }
} }
pub trait CoreWidgetArr<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> { pub trait CoreWidgetArr<Children, Rsc, Tag, GTag> {
fn span(self, dir: Dir) -> SpanBuilder<Rsc, LEN, Wa, Tag>; fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag>;
fn stack(self) -> StackBuilder<Rsc, LEN, Wa, Tag>; fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag>;
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
CoreWidgetArr<State, LEN, Wa, Tag> for Wa CoreWidgetArr<Children, Rsc, Tag, GTag> for Children
{ {
fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag> { fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag> {
SpanBuilder::new(self, dir) SpanBuilder::new(self, dir)
} }
fn stack(self) -> StackBuilder<State, LEN, Wa, Tag> { fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag> {
StackBuilder::new(self) StackBuilder::new(self)
} }
} }
pub trait RscFnMap<Rsc> {
type Input;
fn rsc_map<O>(
self,
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O>;
}
impl<I: IntoIterator, Rsc> RscFnMap<Rsc> for I {
type Input = I::Item;
fn rsc_map<O>(
self,
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O> {
self.into_iter().map(move |i| {
let f = f.clone();
move |rsc: &mut Rsc| f(i, rsc)
})
}
}
pub trait WidgetFnMap<Rsc: UiRsc> {
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
self,
f: impl Fn(WeakWidget) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget>;
}
impl<I: IntoIterator, Rsc: UiRsc> WidgetFnMap<Rsc> for I
where
I::Item: WidgetIdFn<Rsc>,
{
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
self,
f: impl Fn(WeakWidget) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget> {
self.into_iter().map(move |f2| {
let f = f.clone();
move |rsc: &mut Rsc| f(f2(rsc)).add(rsc) as WeakWidget
})
}
}
-67
View File
@@ -1,67 +0,0 @@
use iris::prelude::*;
fn editor(text: &str, mode: EditMode) -> (TextEdit, TextData) {
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
(TextEdit::new(view, mode), TextData::default())
}
fn press(edit: &mut TextEditCtx<'_>, x: f32) {
edit.select(vec2(x, 10.0), vec2(400.0, 200.0), false, false);
}
#[test]
fn pressing_an_empty_field_places_input() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, 40.0);
edit.insert("hello");
assert_eq!(edit.text.content(), "hello");
}
#[test]
fn preedit_replaces_the_previous_composition() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, 0.0);
edit.replace(0, "");
edit.replace(1, "日本");
assert_eq!(edit.text.content(), "日本");
}
#[test]
fn backspace_respects_utf8_boundaries() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, f32::MAX);
edit.backspace(false);
assert_eq!(edit.text.content(), "a");
}
#[test]
fn typing_replaces_the_selection() {
let (mut text, mut data) = editor("hello", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
edit.select_all();
edit.insert("goodbye");
assert_eq!(edit.text.content(), "goodbye");
}