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 {
fn draw(&mut self, painter: &mut Painter) {
let tex = self.view.draw(&mut painter.size_ctx());
let region = text_region(&tex, self.align);
painter.texture_within(&tex.handle, region);
let region = self.view.draw(painter);
if let Some(cursor) = &self.cursor
&& 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 {
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 {
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 buf: MutDetect<TextBuffer>,
// cache
tex: Option<TextTexture>,
tex: Option<RenderedText>,
width: Option<f32>,
}
@@ -32,7 +32,7 @@ impl TextView {
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 {
Some(ctx.px_size().x)
} else {
@@ -55,9 +55,25 @@ impl TextView {
self.buf.changed = false;
tex
}
pub fn tex(&self) -> Option<&TextTexture> {
pub fn tex(&self) -> Option<&RenderedText> {
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 {
@@ -69,7 +85,7 @@ impl Text {
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 {
self.content.changed = false;
self.view.buf.set_text(
@@ -80,36 +96,26 @@ impl Text {
None,
);
}
self.view.draw(ctx)
}
}
impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) {
let tex = self.update_buf(&mut painter.size_ctx());
let region = text_region(&tex, self.align);
painter.texture_within(&tex.handle, region);
self.update_buf(&mut painter.size_ctx());
self.view.draw(painter);
}
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 {
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 {
type Target = TextAttrs;

View File

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

View File

@@ -1,6 +1,6 @@
use crate::{
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,
},
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
@@ -422,7 +422,7 @@ impl<'a, 'c> Painter<'a, 'c> {
}
/// 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)
}
@@ -539,7 +539,6 @@ impl SizeCtx<'_> {
// if let Some(&(outer, len)) = self.cache_height.get(&id)
// && outer == self.outer
// {
// prntln!("FAIL {id:?}");
// self.checked_height.insert(id, (self.outer, len));
// return len;
// }
@@ -584,7 +583,7 @@ impl SizeCtx<'_> {
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)
}

View File

@@ -1,12 +1,11 @@
use cosmic_text::{
Attrs, AttrsList, Buffer, Family, FontSystem, Metrics, SwashCache, SwashContent,
};
use image::{Rgba, RgbaImage};
use std::simd::{Simd, num::SimdUint};
use crate::{
layout::{Align, RegionAlign, TextureHandle, Textures, UiColor, Vec2},
util::HashMap,
use crate::layout::{Align, RegionAlign, TextureHandle, Textures, UiColor, Vec2};
use cosmic_text::{
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
SwashContent,
};
use image::{GenericImageView, RgbaImage};
/// TODO: properly wrap this
pub mod text_lib {
@@ -16,6 +15,7 @@ pub mod text_lib {
pub struct TextData {
pub font_system: FontSystem,
pub swash_cache: SwashCache,
glyph_cache: Vec<(Placement, CacheKey, Color)>,
}
impl Default for TextData {
@@ -23,6 +23,7 @@ impl Default for TextData {
Self {
font_system: FontSystem::new(),
swash_cache: SwashCache::new(),
glyph_cache: Default::default(),
}
}
}
@@ -76,29 +77,30 @@ impl TextData {
buffer: &mut TextBuffer,
attrs: &TextAttrs,
textures: &mut Textures,
) -> TextTexture {
) -> RenderedText {
// TODO: either this or the layout stuff (or both) is super slow,
// 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 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 cosmic_color = {
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 => cosmic_color,
None => text_color,
};
if let Some(img) = self
@@ -112,35 +114,8 @@ impl TextData {
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);
let mut merge = |i, color: [u8; 4]| {
let x = i % pos.width as usize;
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);
}
}
}
self.glyph_cache
.push((pos, physical_glyph.cache_key, glyph_color));
}
}
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 mut image = RgbaImage::new(img_width, img_height);
for ((x, y), color) in pixels {
let x = (x - min_x) as u32;
let y = (y - min_y) as u32;
image.put_pixel(x, y, Rgba(color));
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);
}
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),
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height),
@@ -164,9 +179,8 @@ impl TextData {
}
#[derive(Clone)]
pub struct TextTexture {
pub struct RenderedText {
pub handle: TextureHandle,
pub top_left_offset: Vec2,
pub size: Vec2,
}

View File

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

View File

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