Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa771a99eb | ||
|
|
e817bc83af | ||
|
|
a648c62aa2 | ||
|
|
c118bb446b | ||
|
|
1102dc7338 | ||
|
|
1aadef0e7e | ||
|
|
426ff0adfc | ||
|
|
dab6cf298a | ||
|
|
38d896d44d |
No files matched your search
Generated
+732
-912
File diff suppressed because it is too large.
Load diff
+6
-6
@@ -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,15 +28,14 @@ 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"
|
||||||
log = "0.4.29"
|
|
||||||
arboard = "3.6.1"
|
arboard = "3.6.1"
|
||||||
iris-core = { path = "core" }
|
iris-core = { path = "core" }
|
||||||
iris-macro = { path = "macro" }
|
iris-macro = { path = "macro" }
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
images
|
images
|
||||||
settings (sampler)
|
settings (sampler)
|
||||||
consider typed TextureHandle<T> variants for distinct texture uses
|
|
||||||
|
text
|
||||||
|
figure out ways to speed up / what costs the most
|
||||||
|
resizing (per frame) is really slow (assuming painter isn't griefing)
|
||||||
|
j is weird / fix x offset
|
||||||
|
|
||||||
|
masks r just made to bare minimum work
|
||||||
|
|
||||||
|
scaling
|
||||||
|
could be just a simple scaling factor that multiplies abs
|
||||||
|
and need to ensure text uses raw abs and not scaled abs
|
||||||
|
naming? (pt, px)
|
||||||
|
want to keep (drawn) regions using px? or should I add another field to UiScalar/Vec
|
||||||
|
field could be best solution so redrawing stuff isn't needed & you can specify both as user
|
||||||
|
|
||||||
WidgetRef<W> or smth instead of Id
|
WidgetRef<W> or smth instead of Id
|
||||||
enum that's either an Id or an actual concrete instance of W
|
enum that's either an Id or an actual concrete instance of W
|
||||||
@@ -11,6 +24,17 @@ WidgetRef<W> or smth instead of Id
|
|||||||
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
|
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
|
||||||
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
|
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
|
||||||
|
|
||||||
|
really weird limitation:
|
||||||
|
I don't think you can currently remove an element from a parent and put it in a child of the same parent
|
||||||
|
because it removes the unused children after the entire parent redraw
|
||||||
|
but the child gets drawn during that, so it will think the child is still active !!!
|
||||||
|
or something like that idk, maybe I need a special enum for parent that includes a undecided state where it may or may not get redrawn by the parent
|
||||||
|
or just do ref counting and ensure all drawn things == 1 afterwards (seems like best way)
|
||||||
|
ok so I'm removing the limit for now
|
||||||
|
|
||||||
|
don't forget I'm streaming
|
||||||
|
|
||||||
|
tags
|
||||||
vecs for each widget type?
|
vecs for each widget type?
|
||||||
|
|
||||||
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
||||||
+2
-3
@@ -4,10 +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 }
|
||||||
log = { workspace = true }
|
|
||||||
+1
-1
@@ -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;
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+145
-245
@@ -1,67 +1,60 @@
|
|||||||
use crate::{
|
use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
|
use cosmic_text::{
|
||||||
util::Vec2,
|
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
|
||||||
};
|
SwashContent,
|
||||||
use parley::{
|
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
|
||||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
|
||||||
};
|
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
||||||
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_ctx: FontContext,
|
pub font_system: FontSystem,
|
||||||
pub layout_ctx: LayoutContext<UiColor>,
|
pub swash_cache: SwashCache,
|
||||||
scale_ctx: 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_ctx: FontContext::new(),
|
font_system: FontSystem::new(),
|
||||||
layout_ctx: LayoutContext::new(),
|
swash_cache: SwashCache::new(),
|
||||||
scale_ctx: 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 {
|
||||||
@@ -77,215 +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>,
|
|
||||||
layout_key: Option<LayoutKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct LayoutKey {
|
|
||||||
attrs: TextAttrs,
|
|
||||||
max_width: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextBuffer {
|
|
||||||
pub fn new(text: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.into(),
|
|
||||||
layout: Layout::new(),
|
|
||||||
layout_key: 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.layout_key = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invalidates the layout and returns the underlying string for editing.
|
|
||||||
pub fn edit(&mut self) -> &mut String {
|
|
||||||
self.layout_key = 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>) {
|
|
||||||
let layout_key = LayoutKey {
|
|
||||||
attrs: attrs.clone(),
|
|
||||||
max_width: width,
|
|
||||||
};
|
|
||||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut builder = data
|
|
||||||
.layout_ctx
|
|
||||||
.ranged_builder(&mut data.font_ctx, &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.layout_key = Some(layout_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
pub fn draw(
|
||||||
let mut placed = Vec::new();
|
|
||||||
for line in buffer.layout.lines() {
|
|
||||||
for item in line.items() {
|
|
||||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let font = run.run().font();
|
|
||||||
let font_size = run.run().font_size();
|
|
||||||
let coords = run.run().normalized_coords();
|
|
||||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let coords_hash = hash_coords(coords);
|
|
||||||
let font_id = font.data.id();
|
|
||||||
|
|
||||||
for glyph in run.positioned_glyphs() {
|
|
||||||
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
|
|
||||||
let key = GlyphKey {
|
|
||||||
font: font_id,
|
|
||||||
glyph: glyph.id,
|
|
||||||
size: glyph_size_key(font_size),
|
|
||||||
subpixel,
|
|
||||||
coords: coords_hash,
|
|
||||||
};
|
|
||||||
let Some(entry) = self.glyph_entry(
|
|
||||||
GlyphRaster {
|
|
||||||
key,
|
|
||||||
font: font_ref,
|
|
||||||
font_size,
|
|
||||||
coords,
|
|
||||||
subpixel,
|
|
||||||
glyph_id: glyph.id,
|
|
||||||
},
|
|
||||||
textures,
|
|
||||||
) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
placed.push(PlacedGlyph {
|
|
||||||
entry,
|
|
||||||
offset: Vec2::new(
|
|
||||||
glyph.x.floor() + entry.left as f32,
|
|
||||||
glyph.y.floor() - entry.top as f32,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
placed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn glyph_entry(
|
|
||||||
&mut self,
|
|
||||||
glyph: GlyphRaster<'_>,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut scaler = self
|
|
||||||
.scale_ctx
|
|
||||||
.builder(glyph.font)
|
|
||||||
.size(glyph.font_size)
|
|
||||||
.hint(true)
|
|
||||||
.normalized_coords(glyph.coords)
|
|
||||||
.build();
|
|
||||||
let image = Render::new(&[
|
|
||||||
Source::ColorOutline(0),
|
|
||||||
Source::ColorBitmap(StrikeWith::BestFit),
|
|
||||||
Source::Outline,
|
|
||||||
])
|
|
||||||
.format(Format::Alpha)
|
|
||||||
.offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0))
|
|
||||||
.render(&mut scaler, glyph.glyph_id as u16);
|
|
||||||
|
|
||||||
if let Some(image) = image {
|
|
||||||
self.atlas.insert(glyph.key, &image, textures)
|
|
||||||
} else {
|
|
||||||
self.atlas.insert_empty(glyph.key);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlyphRaster<'a> {
|
|
||||||
key: GlyphKey,
|
|
||||||
font: FontRef<'a>,
|
|
||||||
font_size: f32,
|
|
||||||
coords: &'a [i16],
|
|
||||||
subpixel: u8,
|
|
||||||
glyph_id: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_coords(coords: &[i16]) -> u64 {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
coords.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0;
|
|
||||||
|
|
||||||
fn glyph_size_key(font_size: f32) -> u32 {
|
|
||||||
(font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RenderedText {
|
|
||||||
pub glyphs: Vec<PlacedGlyph>,
|
|
||||||
pub size: Vec2,
|
|
||||||
pub color: UiColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextData {
|
|
||||||
pub fn render(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut TextBuffer,
|
buffer: &mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
|
||||||
textures: &mut Textures,
|
textures: &mut Textures,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
buffer.shape(self, attrs, width);
|
// TODO: either this or the layout stuff (or both) is super slow,
|
||||||
let glyphs = self.place(buffer, textures);
|
// should probably do texture packing and things if possible.
|
||||||
|
// very visible if you add just a couple of wrapping texts and resize window
|
||||||
|
// should also be timed to figure out exactly what points need to be sped up
|
||||||
|
// let mut pixels = HashMap::<_, [u8; 4]>::default();
|
||||||
|
let mut min_x = 0;
|
||||||
|
let mut min_y = 0;
|
||||||
|
let mut max_x = 0;
|
||||||
|
let mut max_y = 0;
|
||||||
|
let text_color = {
|
||||||
|
let c = attrs.color;
|
||||||
|
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
|
||||||
|
};
|
||||||
|
let mut max_width = 0.0f32;
|
||||||
|
let mut height = 0.0;
|
||||||
|
|
||||||
|
for run in buffer.layout_runs() {
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
let physical_glyph = glyph.physical((0., 0.), 1.0);
|
||||||
|
|
||||||
|
let glyph_color = match glyph.color_opt {
|
||||||
|
Some(some) => some,
|
||||||
|
None => text_color,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(img) = self
|
||||||
|
.swash_cache
|
||||||
|
.get_image(&mut self.font_system, physical_glyph.cache_key)
|
||||||
|
{
|
||||||
|
let mut pos = img.placement;
|
||||||
|
pos.left += physical_glyph.x;
|
||||||
|
pos.top = physical_glyph.y + run.line_y as i32 - pos.top;
|
||||||
|
min_x = min_x.min(pos.left);
|
||||||
|
min_y = min_y.min(pos.top);
|
||||||
|
max_x = max_x.max(pos.left + pos.width as i32);
|
||||||
|
max_y = max_y.max(pos.top + pos.height as i32);
|
||||||
|
self.glyph_cache
|
||||||
|
.push((pos, physical_glyph.cache_key, glyph_color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_width = max_width.max(run.line_w);
|
||||||
|
height += run.line_height;
|
||||||
|
}
|
||||||
|
let img_width = (max_x - min_x + 1) as u32;
|
||||||
|
let img_height = (max_y - min_y + 1) as u32;
|
||||||
|
let mut image = RgbaImage::new(img_width, img_height);
|
||||||
|
|
||||||
|
for (pos, key, color) in self.glyph_cache.drain(..) {
|
||||||
|
let img = self
|
||||||
|
.swash_cache
|
||||||
|
.get_image(&mut self.font_system, key)
|
||||||
|
.as_ref()
|
||||||
|
.unwrap();
|
||||||
|
let mut merge = |i, color: [u8; 4]| {
|
||||||
|
let i = i as i32;
|
||||||
|
let x = (i % pos.width as i32 + pos.left - min_x) as u32;
|
||||||
|
let y = (i / pos.width as i32 + pos.top - min_y) as u32;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_dim = 8192;
|
||||||
|
if image.width() > max_dim || image.height() > max_dim {
|
||||||
|
let width = image.width().min(max_dim);
|
||||||
|
let height = image.height().min(max_dim);
|
||||||
|
eprintln!(
|
||||||
|
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
|
||||||
|
image.dimensions(),
|
||||||
|
(width, height)
|
||||||
|
);
|
||||||
|
image = image.view(0, 0, width, height).to_image();
|
||||||
|
}
|
||||||
|
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs,
|
handle: textures.add(image),
|
||||||
size: buffer.size(),
|
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
|
||||||
color: attrs.color,
|
size: Vec2::new(max_width, height),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RenderedText {
|
||||||
|
pub handle: TextureHandle,
|
||||||
|
pub top_left_offset: Vec2,
|
||||||
|
pub size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasTextures {
|
||||||
|
fn add_texture(&mut self, image: DynamicImage) -> TextureHandle;
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,232 +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: Vec2,
|
|
||||||
pub uv_max: Vec2,
|
|
||||||
/// 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_colored: bool,
|
|
||||||
pub view_idx: u32,
|
|
||||||
pub sampler_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlyphEntry {
|
|
||||||
const IS_COLORED: u32 = 1;
|
|
||||||
|
|
||||||
pub(crate) fn flags(&self) -> u32 {
|
|
||||||
if self.is_colored { Self::IS_COLORED } else { 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
log::warn!(
|
|
||||||
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
|
|
||||||
key.glyph,
|
|
||||||
key.font,
|
|
||||||
);
|
|
||||||
self.entries.insert(key, None);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
|
|
||||||
log::warn!(
|
|
||||||
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
|
|
||||||
key.glyph,
|
|
||||||
key.font,
|
|
||||||
);
|
|
||||||
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: Vec2::new(x as f32 * scale, y as f32 * scale),
|
|
||||||
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
|
|
||||||
left: image.placement.left,
|
|
||||||
top: image.placement.top,
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
is_colored: 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) {
|
|
||||||
if let Some((i, (x, y))) = self
|
|
||||||
.pages
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
|
|
||||||
let need_w = w + PAD;
|
|
||||||
let need_h = h + PAD;
|
|
||||||
if self.x + need_w > PAGE {
|
|
||||||
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.y += self.shelf_height;
|
|
||||||
self.x = PAD;
|
|
||||||
self.shelf_height = 0;
|
|
||||||
} else if self.y + need_h > PAGE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let position = (self.x, self.y);
|
|
||||||
self.x += need_w;
|
|
||||||
self.shelf_height = self.shelf_height.max(need_h);
|
|
||||||
Some(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 width = image.placement.width as usize;
|
|
||||||
let height = image.placement.height as usize;
|
|
||||||
let page_stride = page.width() as usize * 4;
|
|
||||||
let x = x as usize * 4;
|
|
||||||
let y = y as usize;
|
|
||||||
let page = page.as_mut();
|
|
||||||
|
|
||||||
for row in 0..height {
|
|
||||||
let start = (y + row) * page_stride + x;
|
|
||||||
let target = &mut page[start..start + width * 4];
|
|
||||||
match image.content {
|
|
||||||
Content::Color => {
|
|
||||||
let start = row * width * 4;
|
|
||||||
target.copy_from_slice(&image.data[start..start + width * 4]);
|
|
||||||
}
|
|
||||||
Content::Mask => {
|
|
||||||
let start = row * width;
|
|
||||||
for (target, &alpha) in target
|
|
||||||
.as_chunks_mut::<4>()
|
|
||||||
.0
|
|
||||||
.iter_mut()
|
|
||||||
.zip(&image.data[start..start + width])
|
|
||||||
{
|
|
||||||
target.copy_from_slice(&[255, 255, 255, alpha]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Content::SubpixelMask => {
|
|
||||||
let start = row * width * 4;
|
|
||||||
for (target, source) in target
|
|
||||||
.as_chunks_mut::<4>()
|
|
||||||
.0
|
|
||||||
.iter_mut()
|
|
||||||
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
|
|
||||||
{
|
|
||||||
target.copy_from_slice(&[255, 255, 255, source[1]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct PlacedGlyph {
|
|
||||||
pub entry: GlyphEntry,
|
|
||||||
pub offset: Vec2,
|
|
||||||
}
|
|
||||||
+8
-13
@@ -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 {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::{
|
|||||||
ArrBuf,
|
ArrBuf,
|
||||||
data::{MaskIdx, PrimitiveInstance},
|
data::{MaskIdx, PrimitiveInstance},
|
||||||
},
|
},
|
||||||
util::Vec2,
|
|
||||||
};
|
};
|
||||||
use bytemuck::Pod;
|
use bytemuck::Pod;
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
@@ -94,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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,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)]
|
||||||
@@ -232,17 +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: Vec2,
|
|
||||||
pub uv_max: Vec2,
|
|
||||||
pub view_idx: u32,
|
|
||||||
pub sampler_idx: u32,
|
|
||||||
pub color: Color<u8>,
|
|
||||||
pub flags: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PrimitiveVec<T> {
|
pub struct PrimitiveVec<T> {
|
||||||
vec: Vec<T>,
|
vec: Vec<T>,
|
||||||
free: Vec<usize>,
|
free: Vec<usize>,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+16
-78
@@ -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>,
|
||||||
@@ -17,97 +15,39 @@ pub struct GpuTextures {
|
|||||||
|
|
||||||
impl GpuTextures {
|
impl GpuTextures {
|
||||||
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
||||||
let mut bindings_changed = false;
|
let mut changed = false;
|
||||||
for update in textures.updates() {
|
for update in textures.updates() {
|
||||||
bindings_changed |= match update {
|
changed = true;
|
||||||
TextureUpdate::Push(image) => {
|
match update {
|
||||||
self.push(image);
|
TextureUpdate::Push(image) => self.push(image),
|
||||||
true
|
TextureUpdate::Set(i, image) => self.set(i, image),
|
||||||
|
TextureUpdate::SetFree => self.view_count += 1,
|
||||||
|
TextureUpdate::Free(i) => self.free(i),
|
||||||
|
TextureUpdate::PushFree => self.push_free(),
|
||||||
}
|
}
|
||||||
TextureUpdate::Set(i, image) => {
|
|
||||||
self.set(i, image);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
TextureUpdate::Patch(i, rect, image) => {
|
changed
|
||||||
self.patch(i, rect, image);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
TextureUpdate::SetFree => {
|
|
||||||
self.view_count += 1;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
TextureUpdate::Free(i) => {
|
|
||||||
self.free(i);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
TextureUpdate::PushFree => {
|
|
||||||
self.push_free();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
bindings_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
@@ -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
-31
@@ -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,36 +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) {
|
|
||||||
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: glyph.entry.flags(),
|
|
||||||
},
|
|
||||||
region,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
||||||
components = ["clippy", "rustfmt"]
|
|
||||||
@@ -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 {}
|
||||||
+137
-37
@@ -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) {
|
||||||
|
match event {
|
||||||
|
UiMainEvent::RequestUpdate => {
|
||||||
|
self.check_updates();
|
||||||
|
}
|
||||||
|
UiMainEvent::App(event) => {
|
||||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
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 ui_state = state.default_state_mut();
|
|
||||||
if old != ui_state.focus
|
|
||||||
&& let Some(old) = old
|
&& let Some(old) = old
|
||||||
{
|
{
|
||||||
old.edit(rsc).deselect();
|
old.edit(rsc).deselect();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let ui_state = state.default_state_mut();
|
||||||
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
@@ -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
@@ -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,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)]
|
||||||
|
|||||||
@@ -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::*;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+388
-237
@@ -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,30 +57,40 @@ 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
|
|
||||||
// wrapped lines come out right without this code knowing about either.
|
|
||||||
for (rect, _) in selection.geometry(layout) {
|
|
||||||
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),
|
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(Color::WHITE),
|
RectPrimitive::color(Color::WHITE),
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
TextSelection::Span { start, end } => {
|
||||||
|
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(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
self.view.desired_width(ctx)
|
self.view.desired_width(ctx)
|
||||||
@@ -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 clamp_selection_to_layout(&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 = std::mem::take(self.text.view.buf.edit());
|
let text = self
|
||||||
self.text.selection = None;
|
.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)
|
||||||
|
{
|
||||||
|
if select {
|
||||||
|
self.text.selection = TextSelection::Span {
|
||||||
|
start: cursor,
|
||||||
|
end: new,
|
||||||
};
|
};
|
||||||
let layout = self.layout();
|
} else {
|
||||||
let sel = apply_motion(sel, layout, motion, select);
|
self.text.selection = TextSelection::Pos(new);
|
||||||
self.text.selection = Some(sel);
|
}
|
||||||
|
} 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,173 +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;
|
|
||||||
}
|
|
||||||
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
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 layout = self.layout();
|
|
||||||
let start = 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
|
||||||
|
{
|
||||||
|
if word {
|
||||||
|
let start = *cursor;
|
||||||
|
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
|
||||||
|
self.delete_between(start, end);
|
||||||
}
|
}
|
||||||
let Some(sel) = self.text.selection else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let start = sel.focus().index();
|
|
||||||
if start >= self.text.view.buf.text().len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let layout = self.layout();
|
|
||||||
let end = if word {
|
|
||||||
sel.focus().next_logical_word(layout).index()
|
|
||||||
} 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
|
|
||||||
};
|
|
||||||
self.delete_range(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_range(&mut self, start: usize, end: usize) {
|
|
||||||
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) {
|
|
||||||
let len = self.text.view.buf.text().len();
|
|
||||||
if len == 0 {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let layout = self.layout();
|
let add = lines.remove(cursor.line + 1).into_text();
|
||||||
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
let line = &mut lines[cursor.line];
|
||||||
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
let mut cur = line.text().to_string();
|
||||||
self.text.selection = Some(Selection::new(anchor, focus));
|
cur.push_str(&add);
|
||||||
|
edit_line(line, cur);
|
||||||
|
} else {
|
||||||
|
let mut text = line.text().to_string();
|
||||||
|
text.remove(cursor.index);
|
||||||
|
edit_line(line, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
|
||||||
|
self.text
|
||||||
|
.buf
|
||||||
|
.cursor_motion(self.font_system, cursor, None, motion)
|
||||||
|
.map(|r| r.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_word_at(&mut self, cursor: Cursor) {
|
||||||
|
if let (Some(start), Some(end)) = (
|
||||||
|
self.buf_motion(cursor, Motion::LeftWord),
|
||||||
|
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 layout = self.layout();
|
TextSelection::None => {
|
||||||
let (selection, double_hit) = if drag {
|
if !drag && let Some(hit) = hit {
|
||||||
let Some(selection) = prev_sel else {
|
*sel = TextSelection::Pos(hit)
|
||||||
return;
|
}
|
||||||
};
|
}
|
||||||
(selection.extend_to_point(layout, pos.x, pos.y), prev_hit)
|
TextSelection::Pos(pos) => match (hit, drag) {
|
||||||
|
(None, false) => *sel = TextSelection::None,
|
||||||
|
(None, true) => (),
|
||||||
|
(Some(hit), false) => {
|
||||||
|
if recent && hit == *pos {
|
||||||
|
self.text.double_hit = Some(hit);
|
||||||
|
return self.select_word_at(hit);
|
||||||
} else {
|
} else {
|
||||||
let hit = Selection::from_point(layout, pos.x, pos.y);
|
*pos = hit
|
||||||
let index = hit.focus().index();
|
}
|
||||||
// Successive clicks at one index select the word, then the line.
|
}
|
||||||
if recent && prev_hit == Some(index) {
|
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
|
||||||
(Selection::line_from_point(layout, pos.x, pos.y), None)
|
},
|
||||||
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
|
TextSelection::Span { start, end } => match (hit, drag) {
|
||||||
(
|
(None, false) => *sel = TextSelection::None,
|
||||||
Selection::word_from_point(layout, pos.x, pos.y),
|
(None, true) => *sel = TextSelection::Pos(*start),
|
||||||
Some(index),
|
(Some(hit), false) => {
|
||||||
)
|
if recent
|
||||||
|
&& let Some(double) = self.text.double_hit
|
||||||
|
&& double == hit
|
||||||
|
{
|
||||||
|
return self.select_line_at(hit);
|
||||||
} else {
|
} else {
|
||||||
(hit, None)
|
*sel = TextSelection::Pos(hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(hit), true) => *end = hit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if let TextSelection::Span { start, end } = sel
|
||||||
|
&& start == end
|
||||||
|
{
|
||||||
|
*sel = TextSelection::Pos(*start);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
self.text.selection = Some(selection);
|
|
||||||
self.text.double_hit = double_hit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
self.clamp_selection_to_layout();
|
} else if self.text.content() != old.0 {
|
||||||
}
|
|
||||||
} else if self.text.view.buf.text() != old.0 {
|
|
||||||
self.text.history.push(old);
|
self.text.history.push(old);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
@@ -351,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;
|
||||||
@@ -381,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 {
|
||||||
@@ -405,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,
|
||||||
@@ -445,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)
|
||||||
@@ -474,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+69
-37
@@ -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_empty(&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,26 +45,45 @@ impl TextView {
|
|||||||
.align(self.align)
|
.align(self.align)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
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 {
|
||||||
let width = if self.attrs.wrap {
|
let width = if self.attrs.wrap {
|
||||||
Some(ctx.px_size().x)
|
Some(ctx.px_size().x)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
if width == self.width
|
||||||
|
&& let Some(tex) = &self.tex
|
||||||
|
&& !self.attrs.changed
|
||||||
|
&& !self.buf.changed
|
||||||
|
{
|
||||||
|
return tex.clone();
|
||||||
|
}
|
||||||
self.width = width;
|
self.width = width;
|
||||||
self.tex = Some(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.attrs.changed = false;
|
self.attrs.changed = false;
|
||||||
self.buf.changed = false;
|
self.buf.changed = false;
|
||||||
}
|
tex
|
||||||
self.tex.as_ref().unwrap()
|
|
||||||
}
|
}
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
pub fn tex(&self) -> Option<&RenderedText> {
|
||||||
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_empty()
|
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 {
|
||||||
@@ -79,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_empty()
|
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 {
|
||||||
@@ -88,39 +101,48 @@ impl TextView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
||||||
let align = self.align;
|
|
||||||
if self.is_empty() && self.hint.is_some() {
|
|
||||||
let region = self.render(&mut painter.size_ctx()).size.align(align);
|
|
||||||
if let Some(hint) = &self.hint {
|
|
||||||
painter.widget(hint);
|
|
||||||
}
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tex = self.render(&mut painter.size_ctx());
|
let tex = self.render(&mut painter.size_ctx());
|
||||||
let region = tex.size.align(align);
|
let region = self.tex_region(&tex);
|
||||||
let within = region.within(&painter.region());
|
if let Some(hint) = &self.hint
|
||||||
painter.glyphs(tex, within);
|
&& let [line] = &self.buf.lines[..]
|
||||||
|
&& line.text().is_empty()
|
||||||
|
{
|
||||||
|
painter.widget(hint);
|
||||||
|
} else {
|
||||||
|
painter.texture_within(&tex.handle, region);
|
||||||
|
}
|
||||||
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,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
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("aé", 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");
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user