diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index ceeab5c..11e4353 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -5,7 +5,7 @@ use crate::{ use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, - fontique::{Blob, FontInfoOverride}, + fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth}, }; use std::{ cell::{Ref, RefCell, RefMut}, @@ -35,6 +35,7 @@ pub struct FontDiagnostics { #[derive(Clone, Debug, PartialEq, Eq)] pub enum FontRegistrationError { AlreadyRegistered(String), + NotRegistered(String), InvalidFont(String), } @@ -44,6 +45,9 @@ impl fmt::Display for FontRegistrationError { Self::AlreadyRegistered(family) => { write!(f, "font data is already registered for {family:?}") } + Self::NotRegistered(family) => { + write!(f, "no font data is registered for {family:?}") + } Self::InvalidFont(family) => { write!( f, @@ -56,6 +60,23 @@ impl fmt::Display for FontRegistrationError { impl std::error::Error for FontRegistrationError {} +/// The bucket used by [`crate::Ui::register_font`] when none is specified. +pub const DEFAULT_GLYPH_BUCKET: &str = "default"; + +struct RegisteredFace { + family: FamilyId, + width: FontWidth, + style: FontStyle, + weight: FontWeight, +} + +struct RegisteredFont { + private_name: String, + bucket: u64, + font_id: u64, + faces: Vec, +} + pub struct TextData { pub font_cx: FontContext, pub layout_cx: LayoutContext, @@ -71,8 +92,11 @@ pub struct TextData { /// truth would mean carrying a `Painter` (or output size) into every /// input handler for the sake of one field. pub density: f32, - registered_families: HashMap, + registered_families: HashMap, + glyph_buckets: HashMap, + font_buckets: HashMap, next_registered_family: u64, + next_glyph_bucket: u64, } impl Default for TextData { @@ -86,7 +110,10 @@ impl Default for TextData { atlas: GlyphAtlas::default(), density: 1.0, registered_families: HashMap::new(), + glyph_buckets: HashMap::new(), + font_buckets: HashMap::new(), next_registered_family: 0, + next_glyph_bucket: 1, } } } @@ -168,29 +195,145 @@ impl TextData { &mut self, family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), FontRegistrationError> { + self.register_font_in(family, DEFAULT_GLYPH_BUCKET, data) + } + + pub(crate) fn register_font_in( + &mut self, + family: impl AsRef, + bucket: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { let family = family.as_ref().to_owned(); if self.registered_families.contains_key(&family) { return Err(FontRegistrationError::AlreadyRegistered(family)); } - // Parley selects fonts by family rather than by a face handle. Give - // application data a private family name so selecting it cannot find - // a system font that happens to carry the same embedded metadata. let private_name = format!("__iris_registered_font_{}__", self.next_registered_family); + let blob = Blob::new(Arc::new(data)); + Self::validate_font(&family, blob.clone())?; + let bucket = self.glyph_bucket(bucket.as_ref()); + let registered = self.add_font(private_name, bucket, blob); + self.next_registered_family += 1; + self.font_buckets.insert(registered.font_id, bucket); + self.registered_families.insert(family, registered); + Ok(()) + } + + pub(crate) fn replace_font( + &mut self, + family: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), FontRegistrationError> { + let family = family.as_ref(); + let Some(bucket) = self.registered_families.get(family).map(|font| font.bucket) else { + return Err(FontRegistrationError::NotRegistered(family.to_owned())); + }; + self.replace_font_with(family, bucket, data) + } + + pub(crate) fn replace_font_in( + &mut self, + family: impl AsRef, + bucket: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), FontRegistrationError> { + let family = family.as_ref(); + if !self.registered_families.contains_key(family) { + return Err(FontRegistrationError::NotRegistered(family.to_owned())); + } + let bucket = self.glyph_bucket(bucket.as_ref()); + self.replace_font_with(family, bucket, data) + } + + fn replace_font_with( + &mut self, + family: &str, + bucket: u64, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), FontRegistrationError> { + let blob = Blob::new(Arc::new(data)); + Self::validate_font(family, blob.clone())?; + let old = self.registered_families.remove(family).unwrap(); + for face in old.faces { + self.font_cx.collection.unregister_font( + face.family, + face.width, + face.style, + face.weight, + ); + } + self.font_buckets.remove(&old.font_id); + self.atlas.clear_bucket(old.bucket); + + let registered = self.add_font(old.private_name, bucket, blob); + self.font_buckets.insert(registered.font_id, bucket); + self.registered_families + .insert(family.to_owned(), registered); + Ok(()) + } + + fn validate_font(family: &str, blob: Blob) -> Result<(), FontRegistrationError> { + let mut collection = Collection::new(CollectionOptions { + shared: false, + system_fonts: false, + }); + let fonts = collection.register_fonts(blob, None); + if fonts.is_empty() { + Err(FontRegistrationError::InvalidFont(family.to_owned())) + } else { + Ok(()) + } + } + + fn add_font(&mut self, private_name: String, bucket: u64, blob: Blob) -> RegisteredFont { + let font_id = blob.id(); let fonts = self.font_cx.collection.register_fonts( - Blob::new(Arc::new(data)), + blob, Some(FontInfoOverride { family_name: Some(&private_name), ..Default::default() }), ); - if fonts.is_empty() { - return Err(FontRegistrationError::InvalidFont(family)); + debug_assert!(!fonts.is_empty(), "validated font failed its second scan"); + let faces = fonts + .into_iter() + .flat_map(|(family, fonts)| { + fonts.into_iter().map(move |font| RegisteredFace { + family, + width: font.width(), + style: font.style(), + weight: font.weight(), + }) + }) + .collect(); + RegisteredFont { + private_name, + bucket, + font_id, + faces, } - self.next_registered_family += 1; - self.registered_families.insert(family, private_name); - Ok(()) + } + + fn glyph_bucket(&mut self, name: &str) -> u64 { + if name == DEFAULT_GLYPH_BUCKET { + return crate::render::DEFAULT_GLYPH_BUCKET_ID; + } + if let Some(bucket) = self.glyph_buckets.get(name) { + return *bucket; + } + let bucket = self.next_glyph_bucket; + self.next_glyph_bucket += 1; + self.glyph_buckets.insert(name.to_owned(), bucket); + bucket + } + + fn bucket_for_font(&self, font: u64) -> u64 { + self.font_buckets + .get(&font) + .copied() + .unwrap_or(crate::render::DEFAULT_GLYPH_BUCKET_ID) } pub(crate) fn is_font_registered(&self, family: impl AsRef) -> bool { @@ -201,8 +344,8 @@ impl TextData { /// layout builder holds `&mut self` -- a `String` per shaped registered /// run, paid only when the layout is rebuilt. pub fn resolve_family(&self, family: &str) -> String { - if let Some(name) = self.registered_families.get(family) { - return name.clone(); + if let Some(font) = self.registered_families.get(family) { + return font.private_name.clone(); } family.to_owned() } @@ -304,9 +447,10 @@ impl TextData { }; let coords_hash = hash_coords(coords); let font_id = font.data.id(); + let bucket = self.bucket_for_font(font_id); for glyph in run.positioned_glyphs() { - let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8; + let (x, subpixel) = horizontal_phase(glyph.x); let key = GlyphKey { font: font_id, glyph: glyph.id, @@ -314,7 +458,7 @@ impl TextData { subpixel, coords: coords_hash, }; - let entry = match self.atlas.get(&key) { + let entry = match self.atlas.get(bucket, &key) { Some(entry) => entry, None => { let mut scaler = self @@ -333,9 +477,9 @@ impl TextData { .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), + Some(image) => self.atlas.insert(bucket, key, &image, textures), None => { - self.atlas.insert_empty(key); + self.atlas.insert_empty(bucket, key); None } } @@ -345,7 +489,7 @@ impl TextData { placed.push(PlacedGlyph { entry, offset: Vec2::new( - glyph.x.floor() + entry.left as f32, + x + entry.left as f32, glyph.y.floor() - entry.top as f32, ), paint: run_color.slot(), @@ -376,6 +520,11 @@ impl TextData { } } +fn horizontal_phase(x: f32) -> (f32, u8) { + let quarters = (x * 4.0).round() as i32; + (quarters.div_euclid(4) as f32, quarters.rem_euclid(4) as u8) +} + pub const SANS_SERIF: &str = "sans-serif"; pub const SERIF: &str = "serif"; pub const MONOSPACE: &str = "monospace"; @@ -574,6 +723,10 @@ impl TextBuffer { } fn invalidate(&mut self) { + // A layout owns the font blobs it was shaped with. Drop it now so + // replacing a registered font does not retain the old bytes until an + // off-screen text resource happens to be shaped again. + self.layout = Layout::new(); self.shaped = None; } } @@ -849,6 +1002,13 @@ impl TextHandle { mod tests { use super::*; + #[test] + fn a_horizontal_phase_carries_across_pixel_boundaries() { + assert_eq!(horizontal_phase(10.20), (10.0, 1)); + assert_eq!(horizontal_phase(10.90), (11.0, 0)); + assert_eq!(horizontal_phase(-0.20), (-1.0, 3)); + } + #[test] fn invalid_font_data_is_reported() { let mut data = TextData::default(); diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index d94e0e1..c4943db 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -46,8 +46,10 @@ pub struct Textures { /// drawing it and its slot is never recycled underneath one. shared: HashMap, /// Next layer to hand out to an atlas page. Page layers and resource slots - /// are never reused, even if an explicit atlas clear drops their handles. + /// are separate identities: released page layers are reused without moving + /// any still-live page. next_page_layer: u32, + free_page_layers: Vec, updates: Vec, } @@ -87,6 +89,7 @@ impl Textures { kinds: Vec::new(), shared: HashMap::new(), next_page_layer: 0, + free_page_layers: Vec::new(), updates: Vec::new(), } } @@ -95,16 +98,20 @@ impl Textures { let image = image.into(); let size = image.dimensions().into(); let kind = TextureKind::Image; - self.push(kind, size, image, true) + self.push(kind, size, image) } pub fn add_page(&mut self, image: impl Into) -> TextureHandle { + self.free(); let image = image.into(); let size = image.dimensions().into(); - let layer = self.next_page_layer; - self.next_page_layer += 1; + let layer = self.free_page_layers.pop().unwrap_or_else(|| { + let layer = self.next_page_layer; + self.next_page_layer += 1; + layer + }); let kind = TextureKind::Page { layer }; - self.push(kind, size, image, false) + self.push(kind, size, image) } pub fn handle(&self, id: StrongRscId) -> TextureHandle { @@ -119,22 +126,10 @@ impl Textures { Some(self.handle(id)) } - fn push( - &mut self, - kind: TextureKind, - size: Vec2, - image: DynamicImage, - recycle: bool, - ) -> TextureHandle { + fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle { self.free(); let old_capacity = self.resources.borrow().capacity(); - let id = if recycle { - self.resources.borrow_mut().add(TextureRsc { kind, size }) - } else { - self.resources - .borrow_mut() - .add_unrecycled(TextureRsc { kind, size }) - }; + let id = self.resources.borrow_mut().add(TextureRsc { kind, size }); let i = id.slot(); if (i as usize) < old_capacity { self.images[i as usize] = Some(image); @@ -201,10 +196,14 @@ impl Textures { pub fn free(&mut self) { let updates = &mut self.updates; let images = &mut self.images; - self.resources.borrow_mut().apply(|id, _| { + let free_page_layers = &mut self.free_page_layers; + self.resources.borrow_mut().apply(|id, resource| { let idx = id.slot(); images[idx as usize] = None; updates.push(Update::Free(idx)); + if let TextureKind::Page { layer } = resource.kind { + free_page_layers.push(layer); + } }); } @@ -321,16 +320,19 @@ mod tests { } #[test] - fn a_released_atlas_page_slot_is_not_reused_by_an_image() { + fn a_released_atlas_page_layer_is_reused_without_moving_live_pages() { let mut textures = Textures::new(); - let page = textures.add_page(image(4)); - let page_slot = page.rsc.id().slot(); - drop(page); + let first = textures.add_page(image(4)); + let second = textures.add_page(image(4)); + let first_layer = first.layer(); + let second_layer = second.layer(); + drop(first); textures.free(); - let plain = textures.add(image(4)); + let replacement = textures.add_page(image(4)); - assert_ne!(plain.image_index(), page_slot); + assert_eq!(replacement.layer(), first_layer); + assert_eq!(second.layer(), second_layer); } #[test] diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index 470947b..1368425 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -6,6 +6,7 @@ use image::RgbaImage; use swash::scale::image::{Content, Image}; pub(crate) const PAGE: u32 = 1024; +pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0; const PAD: u32 = 1; @@ -42,40 +43,47 @@ struct Page { } #[derive(Default)] -pub struct GlyphAtlas { +struct Bucket { pages: Vec, - generation: u64, entries: HashMap>, } +#[derive(Default)] +pub struct GlyphAtlas { + buckets: HashMap, + generation: u64, +} + impl GlyphAtlas { - pub fn get(&self, key: &GlyphKey) -> Option> { - self.entries.get(key).copied() + pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option> { + self.buckets.get(&bucket)?.entries.get(key).copied() } /// Rasterised pixels in, a place in the atlas out. `None` means the glyph /// has no pixels, which is a normal answer rather than a failure. pub fn insert( &mut self, + bucket: u64, key: GlyphKey, image: &Image, textures: &mut Textures, ) -> Option { + let bucket = self.buckets.entry(bucket).or_default(); let w = image.placement.width; let h = image.placement.height; if w == 0 || h == 0 { - self.entries.insert(key, None); + bucket.entries.insert(key, None); return None; } if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE { // A single glyph larger than a page. Refusing is better than // silently drawing a cropped one; the caller draws nothing. - self.entries.insert(key, None); + bucket.entries.insert(key, None); return None; } - let (page_idx, x, y) = self.allocate(w, h, textures); - let page = &self.pages[page_idx]; + let (page_idx, x, y) = bucket.allocate(w, h, textures); + let page = &bucket.pages[page_idx]; let img = textures.image_mut(&page.handle); let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); @@ -89,7 +97,7 @@ impl GlyphAtlas { }; textures.patch(&page.handle, rect); - let page = &self.pages[page_idx]; + let page = &bucket.pages[page_idx]; let scale = 1.0 / PAGE as f32; let entry = GlyphEntry { uv_min: [x as f32 * scale, y as f32 * scale], @@ -101,10 +109,46 @@ impl GlyphAtlas { is_color: matches!(image.content, Content::Color), layer: page.handle.layer(), }; - self.entries.insert(key, Some(entry)); + bucket.entries.insert(key, Some(entry)); Some(entry) } + pub(crate) fn insert_empty(&mut self, bucket: u64, key: GlyphKey) { + self.buckets + .entry(bucket) + .or_default() + .entries + .insert(key, None); + } + + pub(crate) fn clear_bucket(&mut self, bucket: u64) { + if self.buckets.remove(&bucket).is_some() { + self.generation += 1; + } + } + + pub fn generation(&self) -> u64 { + self.generation + } + + pub fn page_count(&self) -> usize { + self.buckets.values().map(|bucket| bucket.pages.len()).sum() + } + + pub fn glyph_count(&self) -> usize { + self.buckets + .values() + .map(|bucket| bucket.entries.len()) + .sum() + } + + pub fn clear(&mut self) { + self.buckets.clear(); + self.generation += 1; + } +} + +impl Bucket { fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { let need_w = w + PAD; let need_h = h + PAD; @@ -130,28 +174,6 @@ impl GlyphAtlas { }); (self.pages.len() - 1, PAD, PAD) } - - pub fn insert_empty(&mut self, key: GlyphKey) { - self.entries.insert(key, None); - } - - pub fn generation(&self) -> u64 { - self.generation - } - - pub fn page_count(&self) -> usize { - self.pages.len() - } - - pub fn glyph_count(&self) -> usize { - self.entries.len() - } - - pub fn clear(&mut self) { - self.pages.clear(); - self.entries.clear(); - self.generation += 1; - } } fn fits(page: &Page, need_w: u32, need_h: u32) -> bool { diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl index 6d434f8..5c5de35 100644 --- a/core/src/render/shader.wgsl +++ b/core/src/render/shader.wgsl @@ -137,8 +137,8 @@ fn corners_of(inst: PrimitiveInstance) -> Corners { let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs); let move_delta = resolve_move(inst.move_idx); return Corners( - floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta, - floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta, + floor(top_left_rel * window.dim) + floor(top_left_abs + move_delta), + floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta), ); } diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index e151cac..361c97c 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -12,8 +12,8 @@ const MIN_ARRAY_LAYERS: u32 = 2; enum Slot { Empty, Image(ImageGpu), - /// The array layer a page occupies. Pages are never freed (see - /// `Textures::free`), so this is the only variant that outlives a `Free`. + /// The array layer a live page occupies. Dropping the page empties this + /// resource slot; its array layer may then be assigned to a new page. Page(u32), } @@ -41,7 +41,9 @@ pub struct GpuTextures { array_texture: Texture, array_view: TextureView, array_capacity: u32, - page_count: u32, + /// One past the highest layer ever populated. Holes below this remain in + /// place when the array grows, while `Textures` can reuse their numbers. + layer_high_water: u32, sampler: Sampler, /// Bound in the image slot of the main draw's bind group, which has @@ -125,7 +127,7 @@ impl GpuTextures { rebuilt = true; } self.write_full_layer(layer, image); - self.page_count = self.page_count.max(layer + 1); + self.layer_high_water = self.layer_high_water.max(layer + 1); (Slot::Page(layer), rebuilt) } } @@ -205,7 +207,7 @@ impl GpuTextures { self.pages_grown += 1; let new_capacity = self.array_capacity * 2; let new_texture = Self::create_array_texture(&self.device, new_capacity); - if self.page_count > 0 { + if self.layer_high_water > 0 { let mut encoder = self .device .create_command_encoder(&CommandEncoderDescriptor { @@ -227,7 +229,7 @@ impl GpuTextures { Extent3d { width: PAGE, height: PAGE, - depth_or_array_layers: self.page_count, + depth_or_array_layers: self.layer_high_water, }, ); self.queue.submit(std::iter::once(encoder.finish())); @@ -368,7 +370,7 @@ impl GpuTextures { array_texture, array_view, array_capacity, - page_count: 0, + layer_high_water: 0, sampler, null_view, bind_group_creates: 0, diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index f008893..2c5395a 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -75,10 +75,52 @@ impl Ui { &mut self, family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), crate::FontRegistrationError> { + self.update_fonts(|text| text.register_font(family, data)) + } + + /// Register application-owned font data in a named glyph-atlas bucket. + /// Fonts registered without this method share the default bucket. + #[track_caller] + pub fn register_font_in( + &mut self, + family: impl AsRef, + bucket: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), crate::FontRegistrationError> { + self.update_fonts(|text| text.register_font_in(family, bucket, data)) + } + + /// Replace an application's registered font while retaining its atlas + /// bucket. Text is reshaped and the bucket's old glyph pages are released. + #[track_caller] + pub fn replace_font( + &mut self, + family: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), crate::FontRegistrationError> { + self.update_fonts(|text| text.replace_font(family, data)) + } + + /// Replace an application's registered font and assign the replacement to + /// a named glyph-atlas bucket. + #[track_caller] + pub fn replace_font_in( + &mut self, + family: impl AsRef, + bucket: impl AsRef, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), crate::FontRegistrationError> { + self.update_fonts(|text| text.replace_font_in(family, bucket, data)) + } + + fn update_fonts( + &mut self, + update: impl FnOnce(&mut TextResources) -> Result<(), crate::FontRegistrationError>, ) -> Result<(), crate::FontRegistrationError> { let owners = { let mut text = self.data.text.borrow_mut(); - text.register_font(family, data)?; + update(&mut text)?; text.invalidate_all() }; let active: Vec = { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 928b21e..4f047b2 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1006,7 +1006,7 @@ impl UiRenderState { let inst = self.primitives.instance(slot); let delta = self.resolve_move_chain(inst.move_idx, rsc); let size = self.output_size; - let corner = |c: UiVec2| (c.get_rel() * size).floor() + c.get_abs().floor() + delta; + let corner = |c: UiVec2| (c.get_rel() * size).floor() + (c.get_abs() + delta).floor(); PixelRegion { top_left: corner(inst.region.top_left()), bot_right: corner(inst.region.bot_right()), diff --git a/src/layout_tests.rs b/src/layout_tests.rs index b775fa7..3cdfae4 100644 --- a/src/layout_tests.rs +++ b/src/layout_tests.rs @@ -362,6 +362,33 @@ fn scrolling_moves_in_o1_without_a_redraw() { assert_eq!(moves, 1, "the scrolled subtree should move in one write"); } +#[test] +fn fractional_scrolling_keeps_primitive_edges_on_physical_pixels() { + let mut rsc = TestRsc { ui: Ui::default() }; + let (scroll, root, rects) = scrolled_rects(&mut rsc, 20); + let mut render = UiRenderState::new(); + render.resize((800.0, 600.0)); + + render.update(&root, &mut rsc); + rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0); + render.update(&root, &mut rsc); + rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-37.25); + render.update(&root, &mut rsc); + + let slot = render + .first_primitive(rects[2].id()) + .expect("the scrolled rect has a primitive"); + let corners = render.primitive_corners(slot, &rsc); + for edge in [ + corners.top_left.x, + corners.top_left.y, + corners.bot_right.x, + corners.bot_right.y, + ] { + assert_eq!(edge.fract(), 0.0, "fractional rendered edge: {corners:?}"); + } +} + #[test] fn hit_testing_follows_a_scrolled_widget() { let mut rsc = TestRsc { ui: Ui::default() };