Move text layout and rendering to Parley
This commit is contained in:
1 parent
b90c855cf5
commit
f8b7912181
19 files changed
+1307
-797
No files matched your search
+2
-1
@@ -7,5 +7,6 @@ edition.workspace = true
|
||||
wgpu = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
cosmic-text = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
@@ -5,7 +5,6 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_destruct)]
|
||||
#![feature(portable_simd)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
|
||||
@@ -10,6 +10,12 @@ pub struct Color<T> {
|
||||
pub a: T,
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Default for Color<T> {
|
||||
fn default() -> Self {
|
||||
Self::BLACK
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Color<T> {
|
||||
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
||||
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
||||
|
||||
+204
-145
@@ -1,60 +1,63 @@
|
||||
use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
|
||||
SwashContent,
|
||||
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
};
|
||||
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 font_system: FontSystem,
|
||||
pub swash_cache: SwashCache,
|
||||
glyph_cache: Vec<(Placement, CacheKey, Color)>,
|
||||
pub font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<UiColor>,
|
||||
scale_cx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_system: FontSystem::new(),
|
||||
swash_cache: SwashCache::new(),
|
||||
glyph_cache: Default::default(),
|
||||
font_cx: FontContext::new(),
|
||||
layout_cx: LayoutContext::new(),
|
||||
scale_cx: ScaleContext::new(),
|
||||
atlas: GlyphAtlas::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
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 color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family<'static>,
|
||||
pub family: Family,
|
||||
pub wrap: bool,
|
||||
/// inner alignment of text region (within where it's drawn)
|
||||
pub align: RegionAlign,
|
||||
}
|
||||
|
||||
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;
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
@@ -70,122 +73,178 @@ impl Default for TextAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
/// Keeps text and its corresponding layout from getting out of sync.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
shaped: Option<(TextAttrs, Option<f32>)>,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
textures: &mut Textures,
|
||||
) -> 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 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 {
|
||||
handle: textures.add(image),
|
||||
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
|
||||
size: Vec2::new(max_width, height),
|
||||
impl TextBuffer {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
layout: Layout::new(),
|
||||
shaped: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidates the layout and returns the underlying string for editing.
|
||||
pub fn edit(&mut self) -> &mut String {
|
||||
self.shaped = None;
|
||||
&mut self.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
|
||||
return;
|
||||
}
|
||||
let mut builder = data
|
||||
.layout_cx
|
||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
builder.build_into(&mut self.layout, &self.text);
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.shaped = Some((attrs.clone(), width));
|
||||
}
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
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);
|
||||
// `font.data.id()` rather than the pointer, so the same font
|
||||
// loaded twice is still one set of entries.
|
||||
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: (font_size * 16.0).round() as u32,
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let entry = match self.atlas.get(&key) {
|
||||
Some(entry) => entry,
|
||||
None => {
|
||||
let mut scaler = self
|
||||
.scale_cx
|
||||
.builder(font_ref)
|
||||
.size(font_size)
|
||||
.hint(true)
|
||||
.normalized_coords(coords)
|
||||
.build();
|
||||
let image = Render::new(&[
|
||||
Source::ColorOutline(0),
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
Source::Outline,
|
||||
])
|
||||
.format(Format::Alpha)
|
||||
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
|
||||
.render(&mut scaler, glyph.id as u16);
|
||||
match image {
|
||||
Some(image) => self.atlas.insert(key, &image, textures),
|
||||
None => {
|
||||
self.atlas.insert_empty(key);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let Some(entry) = entry 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 hash_coords(coords: &[i16]) -> u64 {
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for c in coords {
|
||||
h ^= *c as u16 as u64;
|
||||
h = h.wrapping_mul(0x1000_0000_01b3);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub handle: TextureHandle,
|
||||
pub top_left_offset: Vec2,
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
}
|
||||
|
||||
pub trait HasTextures {
|
||||
fn add_texture(&mut self, image: DynamicImage) -> TextureHandle;
|
||||
impl TextData {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer, textures);
|
||||
RenderedText {
|
||||
glyphs: std::sync::Arc::new(glyphs),
|
||||
size: buffer.size(),
|
||||
color: attrs.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,24 @@ pub struct Textures {
|
||||
pub enum TextureUpdate<'a> {
|
||||
Push(&'a DynamicImage),
|
||||
Set(u32, &'a DynamicImage),
|
||||
Patch(u32, PatchRect, &'a DynamicImage),
|
||||
Free(u32),
|
||||
PushFree,
|
||||
SetFree,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatchRect {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(u32),
|
||||
Set(u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
|
||||
@@ -81,6 +91,18 @@ 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) {
|
||||
for idx in self.recv.try_iter() {
|
||||
self.images[idx as usize] = None;
|
||||
@@ -99,6 +121,10 @@ impl Textures {
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
use crate::{
|
||||
PatchRect, TextureHandle, Textures,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
const PAGE: u32 = 1024;
|
||||
|
||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||
/// pick up its neighbour along a shared edge.
|
||||
const PAD: u32 = 1;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphKey {
|
||||
pub font: u64,
|
||||
pub glyph: u32,
|
||||
/// Font size in 1/16 px, so sizes that round to the same pixels share a
|
||||
/// raster instead of filling the atlas with near-duplicates.
|
||||
pub size: u32,
|
||||
/// Horizontal subpixel phase, in 1/4 px.
|
||||
pub subpixel: u8,
|
||||
/// Hash of the variation coordinates; a variable font at two weights is two
|
||||
/// different sets of pixels from one glyph id.
|
||||
pub coords: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GlyphEntry {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
/// Offset from the glyph's pen position to the top-left of its pixels.
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_color: bool,
|
||||
pub view_idx: u32,
|
||||
pub sampler_idx: u32,
|
||||
}
|
||||
|
||||
struct Page {
|
||||
handle: TextureHandle,
|
||||
x: u32,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.entries.get(key).copied()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: GlyphKey,
|
||||
image: &Image,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
||||
let page = &self.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
write_glyph(rgba, image, x, y);
|
||||
|
||||
let handle = page.handle.clone();
|
||||
let rect = PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
textures.patch(&handle, rect);
|
||||
|
||||
let page = &self.pages[page_idx];
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
||||
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
|
||||
left: image.placement.left,
|
||||
top: image.placement.top,
|
||||
width: w,
|
||||
height: h,
|
||||
is_color: matches!(image.content, Content::Color),
|
||||
view_idx: page.handle.primitive().view_idx,
|
||||
sampler_idx: page.handle.primitive().sampler_idx,
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
||||
let need_w = w + PAD;
|
||||
let need_h = h + PAD;
|
||||
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
|
||||
let page = &mut self.pages[i];
|
||||
if page.x + need_w > PAGE {
|
||||
page.y += page.shelf_height;
|
||||
page.x = PAD;
|
||||
page.shelf_height = 0;
|
||||
}
|
||||
let (x, y) = (page.x, page.y);
|
||||
page.x += need_w;
|
||||
page.shelf_height = page.shelf_height.max(need_h);
|
||||
return (i, x, y);
|
||||
}
|
||||
|
||||
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
|
||||
self.pages.push(Page {
|
||||
handle,
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
(self.pages.len() - 1, PAD, PAD)
|
||||
}
|
||||
|
||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pages.len()
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
||||
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|
||||
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
|
||||
}
|
||||
|
||||
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
|
||||
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
match image.content {
|
||||
Content::Mask => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let a = image.data[(row * w + col) as usize];
|
||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::Color => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let i = ((row * w + col) * 4) as usize;
|
||||
let px = [
|
||||
image.data[i],
|
||||
image.data[i + 1],
|
||||
image.data[i + 2],
|
||||
image.data[i + 3],
|
||||
];
|
||||
page.put_pixel(x + col, y + row, image::Rgba(px));
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::SubpixelMask => {
|
||||
// Preserve readable output if the rasterizer returns a subpixel mask.
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let i = ((row * w + col) * 4) as usize;
|
||||
let a = image.data[i + 1];
|
||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PlacedGlyph {
|
||||
pub entry: GlyphEntry,
|
||||
pub offset: Vec2,
|
||||
}
|
||||
@@ -11,11 +11,13 @@ use wgpu::{
|
||||
*,
|
||||
};
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ impl PrimitiveHandle {
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
textures: TexturePrimitive => 1,
|
||||
glyphs: GlyphPrimitive => 2,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
@@ -230,6 +231,21 @@ pub struct TexturePrimitive {
|
||||
pub sampler_idx: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
pub view_idx: u32,
|
||||
pub sampler_idx: u32,
|
||||
pub color: Color<u8>,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
impl GlyphPrimitive {
|
||||
pub const IS_COLOR: u32 = 1;
|
||||
}
|
||||
|
||||
pub struct PrimitiveVec<T> {
|
||||
vec: Vec<T>,
|
||||
free: Vec<usize>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const RECT: u32 = 0u;
|
||||
const TEXTURE: u32 = 1u;
|
||||
const GLYPH: u32 = 2u;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@@ -7,6 +8,8 @@ var<uniform> window: WindowUniform;
|
||||
var<storage> rects: array<Rect>;
|
||||
@group(1) @binding(TEXTURE)
|
||||
var<storage> textures: array<TextureInfo>;
|
||||
@group(1) @binding(GLYPH)
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
struct Rect {
|
||||
color: u32,
|
||||
@@ -20,6 +23,15 @@ struct TextureInfo {
|
||||
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 {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
@@ -125,6 +137,9 @@ fn fs_main(
|
||||
case TEXTURE: {
|
||||
color = draw_texture(region, textures[i]);
|
||||
}
|
||||
case GLYPH: {
|
||||
color = draw_glyph(region, glyphs[i]);
|
||||
}
|
||||
default: {
|
||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
@@ -148,6 +163,17 @@ fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
|
||||
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> {
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
|
||||
|
||||
+75
-13
@@ -1,11 +1,13 @@
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{TextureUpdate, Textures};
|
||||
use crate::{PatchRect, TextureUpdate, Textures};
|
||||
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
/// Parallel to `views`; patches require textures rather than views.
|
||||
textures: Vec<Option<Texture>>,
|
||||
views: Vec<TextureView>,
|
||||
view_count: usize,
|
||||
samplers: Vec<Sampler>,
|
||||
@@ -17,37 +19,95 @@ impl GpuTextures {
|
||||
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
||||
let mut changed = false;
|
||||
for update in textures.updates() {
|
||||
changed = true;
|
||||
match update {
|
||||
TextureUpdate::Push(image) => self.push(image),
|
||||
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::Push(image) => {
|
||||
self.push(image);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Set(i, image) => {
|
||||
self.set(i, image);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Patch(i, rect, image) => {
|
||||
// Patching contents leaves the binding array unchanged.
|
||||
self.patch(i, rect, image);
|
||||
}
|
||||
TextureUpdate::SetFree => {
|
||||
self.view_count += 1;
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Free(i) => {
|
||||
self.free(i);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::PushFree => {
|
||||
self.push_free();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
fn set(&mut self, i: u32, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let view = self.create_view(image);
|
||||
let (texture, view) = self.create(image);
|
||||
self.textures[i as usize] = Some(texture);
|
||||
self.views[i as usize] = view;
|
||||
}
|
||||
fn free(&mut self, i: u32) {
|
||||
self.view_count -= 1;
|
||||
self.textures[i as usize] = None;
|
||||
self.views[i as usize] = self.null_view.clone();
|
||||
}
|
||||
fn push(&mut self, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let view = self.create_view(image);
|
||||
let (texture, view) = self.create(image);
|
||||
self.textures.push(Some(texture));
|
||||
self.views.push(view);
|
||||
}
|
||||
fn push_free(&mut self) {
|
||||
self.view_count += 1;
|
||||
self.textures.push(None);
|
||||
self.views.push(self.null_view.clone());
|
||||
}
|
||||
|
||||
fn create_view(&self, image: &DynamicImage) -> TextureView {
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
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 (width, height) = image.dimensions();
|
||||
let texture = self.device.create_texture_with_data(
|
||||
@@ -63,13 +123,14 @@ impl GpuTextures {
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
image.as_bytes(),
|
||||
);
|
||||
texture.create_view(&TextureViewDescriptor::default())
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
(texture, view)
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
@@ -77,6 +138,7 @@ impl GpuTextures {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
textures: Vec::new(),
|
||||
views: Vec::new(),
|
||||
samplers: vec![default_sampler(device)],
|
||||
no_views: vec![null_view.clone()],
|
||||
|
||||
+38
-5
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId,
|
||||
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
@@ -90,10 +90,43 @@ impl<'a> Painter<'a> {
|
||||
self.primitive_at(handle.primitive(), region);
|
||||
}
|
||||
|
||||
/// returns (handle, offset from top left)
|
||||
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.draw(buffer, attrs, &mut ui.textures)
|
||||
ui.text.render(buffer, attrs, width, &mut ui.textures)
|
||||
}
|
||||
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
let flags_for = |is_color| {
|
||||
if is_color {
|
||||
GlyphPrimitive::IS_COLOR
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
region.y.end = region.y.start;
|
||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||
self.primitive_at(
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
view_idx: glyph.entry.view_idx,
|
||||
sampler_idx: glyph.entry.sampler_idx,
|
||||
color: text.color,
|
||||
flags: flags_for(glyph.entry.is_color),
|
||||
},
|
||||
region,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
|
||||
+7
-2
@@ -76,8 +76,13 @@ impl SizeCtx<'_> {
|
||||
self.output_size
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||
self.text.draw(buffer, attrs, self.textures)
|
||||
pub fn draw_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width, self.textures)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
|
||||
Reference in new issue
Block a user