crop text images that are too big

This commit is contained in:
2025-11-21 18:18:28 -05:00
parent 97b284e81e
commit 23c5abe5a9
7 changed files with 97 additions and 79 deletions

View File

@@ -33,9 +33,7 @@ impl TextEdit {
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
let tex = self.view.draw(&mut painter.size_ctx()); let region = self.view.draw(painter);
let region = text_region(&tex, self.align);
painter.texture_within(&tex.handle, region);
if let Some(cursor) = &self.cursor if let Some(cursor) = &self.cursor
&& let Some(offset) = cursor_pos(cursor, &self.buf) && let Some(offset) = cursor_pos(cursor, &self.buf)
@@ -49,11 +47,11 @@ impl Widget for TextEdit {
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.view.draw(ctx).size.x) Len::abs(self.view.render(ctx).size.x)
} }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.view.draw(ctx).size.y) Len::abs(self.view.render(ctx).size.y)
} }
} }

View File

@@ -19,7 +19,7 @@ pub struct TextView {
pub attrs: MutDetect<TextAttrs>, pub attrs: MutDetect<TextAttrs>,
pub buf: MutDetect<TextBuffer>, pub buf: MutDetect<TextBuffer>,
// cache // cache
tex: Option<TextTexture>, tex: Option<RenderedText>,
width: Option<f32>, width: Option<f32>,
} }
@@ -32,7 +32,7 @@ impl TextView {
width: None, width: None,
} }
} }
pub fn draw(&mut self, ctx: &mut SizeCtx) -> TextTexture { pub 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 {
@@ -55,9 +55,25 @@ impl TextView {
self.buf.changed = false; self.buf.changed = false;
tex tex
} }
pub fn tex(&self) -> Option<&TextTexture> { pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref() self.tex.as_ref()
} }
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.render(ctx).size.x)
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.render(ctx).size.y)
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx());
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);
painter.texture_within(&tex.handle, region);
region
}
} }
impl Text { impl Text {
@@ -69,7 +85,7 @@ impl Text {
view: TextView::new(buf, attrs), view: TextView::new(buf, attrs),
} }
} }
fn update_buf(&mut self, ctx: &mut SizeCtx) -> TextTexture { 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.view.buf.set_text(
@@ -80,36 +96,26 @@ impl Text {
None, None,
); );
} }
self.view.draw(ctx)
} }
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
let tex = self.update_buf(&mut painter.size_ctx()); self.update_buf(&mut painter.size_ctx());
let region = text_region(&tex, self.align); self.view.draw(painter);
painter.texture_within(&tex.handle, region);
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.update_buf(ctx).size.x) self.update_buf(ctx);
self.view.desired_width(ctx)
} }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
Len::abs(self.update_buf(ctx).size.y) self.update_buf(ctx);
self.view.desired_height(ctx)
} }
} }
pub fn text_region(tex: &TextTexture, align: RegionAlign) -> UiRegion {
let tex_dims = tex.handle.size();
let mut region = tex.size.align(align);
region.x.start.abs += tex.top_left_offset.x;
region.y.start.abs += tex.top_left_offset.y;
region.x.end.abs = region.x.start.abs + tex_dims.x;
region.y.end.abs = region.y.start.abs + tex_dims.y;
region
}
impl Deref for Text { impl Deref for Text {
type Target = TextAttrs; type Target = TextAttrs;

View File

@@ -2,7 +2,7 @@
/// stored in linear for sane manipulation /// stored in linear for sane manipulation
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, Debug)] #[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
pub struct Color<T: ColorNum> { pub struct Color<T: ColorNum> {
pub r: T, pub r: T,
pub g: T, pub g: T,

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
layout::{ layout::{
Axis, Layers, Len, Modules, Size, TextAttrs, TextBuffer, TextData, TextTexture, Axis, Layers, Len, Modules, Size, TextAttrs, TextBuffer, TextData, RenderedText,
TextureHandle, Textures, UiRegion, UiVec2, Vec2, WidgetId, Widgets, TextureHandle, Textures, UiRegion, UiVec2, Vec2, WidgetId, Widgets,
}, },
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
@@ -422,7 +422,7 @@ impl<'a, 'c> Painter<'a, 'c> {
} }
/// returns (handle, offset from top left) /// returns (handle, offset from top left)
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> TextTexture { pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
self.ctx.text.draw(buffer, attrs, self.ctx.textures) self.ctx.text.draw(buffer, attrs, self.ctx.textures)
} }
@@ -539,7 +539,6 @@ impl SizeCtx<'_> {
// if let Some(&(outer, len)) = self.cache_height.get(&id) // if let Some(&(outer, len)) = self.cache_height.get(&id)
// && outer == self.outer // && outer == self.outer
// { // {
// prntln!("FAIL {id:?}");
// self.checked_height.insert(id, (self.outer, len)); // self.checked_height.insert(id, (self.outer, len));
// return len; // return len;
// } // }
@@ -584,7 +583,7 @@ impl SizeCtx<'_> {
self.output_size self.output_size
} }
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> TextTexture { pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
self.text.draw(buffer, attrs, self.textures) self.text.draw(buffer, attrs, self.textures)
} }

View File

@@ -1,12 +1,11 @@
use cosmic_text::{ use std::simd::{Simd, num::SimdUint};
Attrs, AttrsList, Buffer, Family, FontSystem, Metrics, SwashCache, SwashContent,
};
use image::{Rgba, RgbaImage};
use crate::{ use crate::layout::{Align, RegionAlign, TextureHandle, Textures, UiColor, Vec2};
layout::{Align, RegionAlign, TextureHandle, Textures, UiColor, Vec2}, use cosmic_text::{
util::HashMap, Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
SwashContent,
}; };
use image::{GenericImageView, RgbaImage};
/// TODO: properly wrap this /// TODO: properly wrap this
pub mod text_lib { pub mod text_lib {
@@ -16,6 +15,7 @@ pub mod text_lib {
pub struct TextData { pub struct TextData {
pub font_system: FontSystem, pub font_system: FontSystem,
pub swash_cache: SwashCache, pub swash_cache: SwashCache,
glyph_cache: Vec<(Placement, CacheKey, Color)>,
} }
impl Default for TextData { impl Default for TextData {
@@ -23,6 +23,7 @@ impl Default for TextData {
Self { Self {
font_system: FontSystem::new(), font_system: FontSystem::new(),
swash_cache: SwashCache::new(), swash_cache: SwashCache::new(),
glyph_cache: Default::default(),
} }
} }
} }
@@ -76,29 +77,30 @@ impl TextData {
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
textures: &mut Textures, textures: &mut Textures,
) -> TextTexture { ) -> RenderedText {
// TODO: either this or the layout stuff (or both) is super slow, // TODO: either this or the layout stuff (or both) is super slow,
// should probably do texture packing and things if possible. // should probably do texture packing and things if possible.
// very visible if you add just a couple of wrapping texts and resize window // 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 // should also be timed to figure out exactly what points need to be sped up
let mut pixels = HashMap::<_, [u8; 4]>::default(); // let mut pixels = HashMap::<_, [u8; 4]>::default();
let mut min_x = 0; let mut min_x = 0;
let mut min_y = 0; let mut min_y = 0;
let mut max_x = 0; let mut max_x = 0;
let mut max_y = 0; let mut max_y = 0;
let cosmic_color = { let text_color = {
let c = attrs.color; let c = attrs.color;
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a) cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
}; };
let mut max_width = 0.0f32; let mut max_width = 0.0f32;
let mut height = 0.0; let mut height = 0.0;
for run in buffer.layout_runs() { for run in buffer.layout_runs() {
for glyph in run.glyphs.iter() { for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((0., 0.), 1.0); let physical_glyph = glyph.physical((0., 0.), 1.0);
let glyph_color = match glyph.color_opt { let glyph_color = match glyph.color_opt {
Some(some) => some, Some(some) => some,
None => cosmic_color, None => text_color,
}; };
if let Some(img) = self if let Some(img) = self
@@ -112,35 +114,8 @@ impl TextData {
min_y = min_y.min(pos.top); min_y = min_y.min(pos.top);
max_x = max_x.max(pos.left + pos.width as i32); max_x = max_x.max(pos.left + pos.width as i32);
max_y = max_y.max(pos.top + pos.height as i32); max_y = max_y.max(pos.top + pos.height as i32);
let mut merge = |i, color: [u8; 4]| { self.glyph_cache
let x = i % pos.width as usize; .push((pos, physical_glyph.cache_key, glyph_color));
let y = i / pos.width as usize;
let pos = (x as i32 + pos.left, y as i32 + pos.top);
if let Some(pixel) = pixels.get_mut(&pos) {
for i in 0..4 {
// TODO: no clue if proper alpha blending should be done
pixel[i] = color[i].saturating_add(pixel[i]);
}
} else {
pixels.insert(pos, color);
}
};
match img.content {
SwashContent::Mask => {
for (i, a) in img.data.iter().enumerate() {
let mut color = glyph_color.as_rgba();
color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8;
merge(i, color);
}
}
SwashContent::SubpixelMask => todo!(),
SwashContent::Color => {
let (colors, _) = img.data.as_chunks::<4>();
for (i, color) in colors.iter().enumerate() {
merge(i, *color);
}
}
}
} }
} }
max_width = max_width.max(run.line_w); max_width = max_width.max(run.line_w);
@@ -150,12 +125,52 @@ impl TextData {
let img_height = (max_y - min_y + 1) as u32; let img_height = (max_y - min_y + 1) as u32;
let mut image = RgbaImage::new(img_width, img_height); let mut image = RgbaImage::new(img_width, img_height);
for ((x, y), color) in pixels { for (pos, key, color) in self.glyph_cache.drain(..) {
let x = (x - min_x) as u32; let img = self
let y = (y - min_y) as u32; .swash_cache
image.put_pixel(x, y, Rgba(color)); .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);
} }
TextTexture { }
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 {
handle: textures.add(image), handle: textures.add(image),
top_left_offset: Vec2::new(min_x as f32, min_y as f32), top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height), size: Vec2::new(max_width, height),
@@ -164,9 +179,8 @@ impl TextData {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct TextTexture { pub struct RenderedText {
pub handle: TextureHandle, pub handle: TextureHandle,
pub top_left_offset: Vec2, pub top_left_offset: Vec2,
pub size: Vec2, pub size: Vec2,
} }

View File

@@ -7,6 +7,7 @@
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_cmp)] #![feature(const_cmp)]
#![feature(const_destruct)] #![feature(const_destruct)]
#![feature(portable_simd)]
pub mod core; pub mod core;
pub mod layout; pub mod layout;

View File

@@ -48,7 +48,7 @@ macro_rules! impl_op {
} }
impl const $opa for $T { impl const $opa for $T {
fn $fna(&mut self, rhs: Self) { fn $fna(&mut self, rhs: Self) {
$(self.$field.$fna(rhs.$field);)* *self = self.$fn(rhs);
} }
} }
impl const $op<f32> for $T { impl const $op<f32> for $T {
@@ -71,7 +71,7 @@ macro_rules! impl_op {
} }
impl const $opa<f32> for $T { impl const $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) { fn $fna(&mut self, rhs: f32) {
$(self.$field.$fna(rhs);)* *self = self.$fn(rhs);
} }
} }
} }