diff --git a/iris/core/src/orientation/len.rs b/iris/core/src/orientation/len.rs index 8725211..84c194d 100644 --- a/iris/core/src/orientation/len.rs +++ b/iris/core/src/orientation/len.rs @@ -9,7 +9,31 @@ pub struct Size { #[derive(Debug, Clone, Copy, PartialEq)] pub struct Len { + /// Physical pixels -- a raw device pixel, unaffected by the display's + /// density. Rare to want directly (a hairline border is the usual + /// case); most sizes should be `dp` instead. See `dp`'s own doc for why + /// the two are kept separate rather than one field a caller has to + /// remember to pre-multiply. pub abs: f32, + /// Density-independent pixels -- Android's `dp` / CSS's reference pixel + /// (1 unit = 1/160in), resolved against the display's density at + /// layout time (`apply_rest`'s `density` parameter) rather than at the + /// point a widget is built, since density is a property of the device + /// this ends up running on, not of the widget tree. This is the unit + /// IRIS_TODO.md's "a density-independent length unit" item asked for, + /// 2026-09-06: before it existed, every size in the tree was `abs` + /// (physical pixels), and the only way to make a 16px design draw at + /// the right *size* on a denser display was a single global multiply + /// applied to the whole rendered scene after layout -- which is also + /// what made text blurry (RUST.md's P0 box, "blurry ... glyphs drawn + /// at logical size and stretched by the scale"): a glyph rasterised at + /// 16 physical px and then stretched 3x by that global multiply is a + /// 48px area sampled from a 16px bitmap. Resolving `dp` per-length at + /// layout time instead means the font size handed to the text shaper + /// is already the physical size (`16.0.dp() * 3.0`), so the glyph + /// atlas rasterises at the display's real resolution and nothing + /// downstream needs to stretch anything. + pub dp: f32, pub rel: f32, pub rest: f32, } @@ -67,10 +91,10 @@ impl Size { } } - pub fn to_uivec2(self) -> UiVec2 { + pub fn to_uivec2(self, density: f32) -> UiVec2 { UiVec2 { - x: self.x.apply_rest(), - y: self.y.apply_rest(), + x: self.x.apply_rest(density), + y: self.y.apply_rest(density), } } @@ -98,26 +122,43 @@ impl Size { impl Len { pub const ZERO: Self = Self { abs: 0.0, + dp: 0.0, rel: 0.0, rest: 0.0, }; pub const REST: Self = Self { abs: 0.0, + dp: 0.0, rel: 0.0, rest: 1.0, }; - pub fn apply_rest(&self) -> UiScalar { + /// Resolves to a `UiScalar`, folding `dp` into `abs` pixels against + /// `density` (physical pixels per dp -- 1.0 on a desktop or an + /// unscaled display, `content_scale` on Android; see `dp`'s field + /// doc). Every other component of `Len` is already resolution- + /// independent (`rel` is a fraction of the parent; `rest` becomes a + /// fraction too, below), so `density` only ever touches this one term. + pub fn apply_rest(&self, density: f32) -> UiScalar { UiScalar { rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 }, - abs: self.abs, + abs: self.abs + self.dp * density, } } pub fn abs(abs: impl UiNum) -> Self { Self { abs: abs.to_f32(), + dp: 0.0, + rel: 0.0, + rest: 0.0, + } + } + pub fn dp(dp: impl UiNum) -> Self { + Self { + abs: 0.0, + dp: dp.to_f32(), rel: 0.0, rest: 0.0, } @@ -125,6 +166,7 @@ impl Len { pub fn rel(rel: impl UiNum) -> Self { Self { abs: 0.0, + dp: 0.0, rel: rel.to_f32(), rest: 0.0, } @@ -132,6 +174,7 @@ impl Len { pub fn rest(ratio: impl UiNum) -> Self { Self { abs: 0.0, + dp: 0.0, rel: 0.0, rest: ratio.to_f32(), } @@ -144,6 +187,15 @@ pub mod len_fns { pub fn abs(abs: impl UiNum) -> Len { Len { abs: abs.to_f32(), + dp: 0.0, + rel: 0.0, + rest: 0.0, + } + } + pub fn dp(dp: impl UiNum) -> Len { + Len { + abs: 0.0, + dp: dp.to_f32(), rel: 0.0, rest: 0.0, } @@ -151,6 +203,7 @@ pub mod len_fns { pub fn rel(rel: impl UiNum) -> Len { Len { abs: 0.0, + dp: 0.0, rel: rel.to_f32(), rest: 0.0, } @@ -158,14 +211,15 @@ pub mod len_fns { pub fn rest(ratio: impl UiNum) -> Len { Len { abs: 0.0, + dp: 0.0, rel: 0.0, rest: ratio.to_f32(), } } } -impl_op!(Len Add add; abs rel rest); -impl_op!(Len Sub sub; abs rel rest); +impl_op!(Len Add add; abs dp rel rest); +impl_op!(Len Sub sub; abs dp rel rest); impl_op!(Size Add add; x y); impl_op!(Size Sub sub; x y); @@ -187,6 +241,9 @@ impl std::fmt::Display for Len { if self.abs != 0.0 { write!(f, "{} abs;", self.abs)?; } + if self.dp != 0.0 { + write!(f, "{} dp;", self.dp)?; + } if self.rel != 0.0 { write!(f, "{} rel;", self.rel)?; } diff --git a/iris/core/src/primitive/text.rs b/iris/core/src/primitive/text.rs index 1480d43..2b67b81 100644 --- a/iris/core/src/primitive/text.rs +++ b/iris/core/src/primitive/text.rs @@ -66,6 +66,17 @@ pub struct TextData { pub layout_cx: LayoutContext, scale_cx: ScaleContext, pub atlas: GlyphAtlas, + /// Physical pixels per dp -- a second copy of + /// `UiRenderState::density`, kept here too because `TextEditCtx::layout` + /// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text + /// from an event callback that has a `TextData` but no `Painter`, so it + /// has nowhere else to read the display's density from. Both copies are + /// set together, from the one place either backend learns the real + /// value (`android::view::new_peer`); this is the same accepted + /// duplication as `AndroidRenderer::content_scale`; a single source of + /// truth would mean carrying a `Painter` (or output size) into every + /// input handler for the sake of one field. + pub density: f32, } impl Default for TextData { @@ -75,6 +86,7 @@ impl Default for TextData { layout_cx: LayoutContext::new(), scale_cx: ScaleContext::new(), atlas: GlyphAtlas::default(), + density: 1.0, }; data.register_bundled_fonts(); data @@ -363,7 +375,7 @@ pub struct TextBuffer { /// `set_spans` forces `shaped` to `None` directly, the same way `edit` /// does, since spans change far less often than a naive equality check /// on the whole `Vec` would cost to compute every frame. - shaped: Option<(TextAttrs, Option)>, + shaped: Option<(TextAttrs, Option, f32)>, } impl TextBuffer { @@ -419,19 +431,42 @@ impl TextBuffer { Vec2::new(self.layout.width(), self.layout.height()) } - /// Lay the text out, unless it is already laid out for these attributes and - /// this width. - pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option) { - if self.shaped.as_ref() == Some(&(attrs.clone(), width)) { + /// Lay the text out, unless it is already laid out for these + /// attributes, this width and this density. + /// + /// **`attrs.font_size`/`line_height` and every span's own `font_size` + /// are density-independent (dp) units, multiplied by `density` here -- + /// the one place text crosses from the widget tree's dp sizes into the + /// physical pixels the shaper and rasteriser (`TextData::place`) both + /// then work in.** This is what makes glyphs sharp on a dense display: + /// before this existed, `font_size` was already a physical-pixel value + /// (RUST.md's P0 box's global-scale stopgap resolved density by + /// stretching the whole rendered frame afterward instead), so a glyph + /// was rasterised small and then upscaled by whatever the display's + /// scale factor was -- exactly the blur Iris's report described. + /// Multiplying here instead means the font size hitting `ScaleContext` + /// in `place` below is already the display's real physical size, so + /// the atlas holds a bitmap at the resolution it is actually shown at. + /// `GlyphKey.size` already keys on that resolved `font_size` + /// (`(font_size * 16.0).round()`), so a cache entry is naturally per + /// physical size with no change needed there. + pub fn shape( + &mut self, + data: &mut TextData, + attrs: &TextAttrs, + width: Option, + density: f32, + ) { + if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) { 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::FontSize(attrs.font_size * density)); builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( - attrs.line_height, + attrs.line_height * density, ))); builder.push_default(StyleProperty::Brush(attrs.color)); for span in &self.spans { @@ -443,7 +478,7 @@ impl TextBuffer { builder.push(StyleProperty::FontFamily(family.family()), range.clone()); } if let Some(size) = span.font_size { - builder.push(StyleProperty::FontSize(size), range.clone()); + builder.push(StyleProperty::FontSize(size * density), range.clone()); } if span.bold { builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone()); @@ -459,7 +494,7 @@ impl TextBuffer { self.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); - self.shaped = Some((attrs.clone(), width)); + self.shaped = Some((attrs.clone(), width, density)); } } @@ -576,8 +611,9 @@ impl TextData { attrs: &TextAttrs, width: Option, textures: &mut Textures, + density: f32, ) -> RenderedText { - buffer.shape(self, attrs, width); + buffer.shape(self, attrs, width, density); let glyphs = self.place(buffer, textures); RenderedText { glyphs: std::sync::Arc::new(glyphs), diff --git a/iris/core/src/ui/painter.rs b/iris/core/src/ui/painter.rs index 801f279..09cd0cf 100644 --- a/iris/core/src/ui/painter.rs +++ b/iris/core/src/ui/painter.rs @@ -165,8 +165,10 @@ impl<'a> Painter<'a> { attrs: &TextAttrs, width: Option, ) -> RenderedText { + let density = self.state.density; let ui = self.rsc.ui_mut(); - ui.text.render(buffer, attrs, width, &mut ui.textures) + ui.text + .render(buffer, attrs, width, &mut ui.textures, density) } /// Draw a laid-out string: one quad per glyph, all sampling the atlas. @@ -210,6 +212,12 @@ impl<'a> Painter<'a> { self.state.output_size } + /// Physical pixels per `dp` -- see `UiRenderState::density`'s field + /// doc. What `Len::dp`'s `apply_rest` call resolves against. + pub fn density(&self) -> f32 { + self.state.density + } + pub fn px_size(&mut self) -> Vec2 { self.region.size().to_abs(self.state.output_size) } diff --git a/iris/core/src/ui/render_state.rs b/iris/core/src/ui/render_state.rs index 31f1183..a799091 100644 --- a/iris/core/src/ui/render_state.rs +++ b/iris/core/src/ui/render_state.rs @@ -9,6 +9,12 @@ pub struct UiRenderState { pub active: HashMap, pub layers: PrimitiveLayers, pub(super) output_size: Vec2, + /// Physical pixels per `dp` -- see `Len::dp`'s field doc. `1.0` (an + /// unscaled display) until a backend that knows its own density calls + /// `set_density` (Android's `content_scale`, read at `surface_changed` + /// time); the winit backend has no analogous per-monitor value wired up + /// yet and stays at the default. + pub(super) density: f32, old_root: Option, resized: bool, @@ -35,6 +41,7 @@ impl UiRenderState { active: Default::default(), layers: Default::default(), output_size: Vec2::ZERO, + density: 1.0, old_root: None, resized: false, draw_started: Default::default(), @@ -60,6 +67,20 @@ impl UiRenderState { self.resized = true; } + /// Sets the physical-pixels-per-dp ratio every `Len::dp` in the tree + /// resolves against from the next layout pass on -- see `density`'s + /// field doc. Not folded into `resize` because the two change on + /// different triggers (a surface resize on every rotation or keyboard + /// open; a density change only if the app follows the display to a + /// different screen, which Android surfaces separately). + pub fn set_density(&mut self, density: f32) { + self.density = density; + } + + pub fn density(&self) -> f32 { + self.density + } + pub fn update<'a>(&mut self, root: impl Into>, rsc: &mut dyn UiRsc) { // safety mechanism for memory leaks; might wanna return a result instead so user can // decide whether to panic or not @@ -311,7 +332,7 @@ impl UiRenderState { }; let from = active .size - .to_uivec2() + .to_uivec2(self.density) .align(RegionAlign::TOP_LEFT) .within(&active.region); let slot = active.move_slot; diff --git a/iris/examples/message_list.rs b/iris/examples/message_list.rs index f8a303a..6eb7237 100644 --- a/iris/examples/message_list.rs +++ b/iris/examples/message_list.rs @@ -68,12 +68,15 @@ fn build_row(rsc: &mut Rsc, i: usize) -> StrongWidget { let mut span = Span::empty(Dir::DOWN); span.push(text); span.push(img); - span.pad(8.0).background(rect(tint)).add_strong(rsc).any() + span.pad(dp(8.0)) + .background(rect(tint)) + .add_strong(rsc) + .any() } else { wtext(row_text(i)) .wrap(true) .color(text_color) - .pad(8.0) + .pad(dp(8.0)) .background(rect(tint)) .add_strong(rsc) .any() diff --git a/iris/src/widget/list.rs b/iris/src/widget/list.rs index d58a0bc..770c20b 100644 --- a/iris/src/widget/list.rs +++ b/iris/src/widget/list.rs @@ -606,9 +606,10 @@ impl List { let axis = self.axis; let output_len = painter.output_size().axis(axis); let container_len = painter.region().axis(axis).len(); + let density = painter.density(); let resolve = move |used: Size| -> f32 { used.axis(axis) - .apply_rest() + .apply_rest(density) .within_len(container_len) .to_abs(output_len) }; diff --git a/iris/src/widget/position/align.rs b/iris/src/widget/position/align.rs index 6581a9c..feb3c06 100644 --- a/iris/src/widget/position/align.rs +++ b/iris/src/widget/position/align.rs @@ -17,14 +17,15 @@ impl Widget for Aligned { // already-resolved region double-applies that composition and is // wrong for any widget nested below the root. let used = painter.widget(&self.inner); + let density = painter.density(); let region = match self.align.tuple() { - (Some(x), Some(y)) => used.to_uivec2().align(RegionAlign { x, y }), + (Some(x), Some(y)) => used.to_uivec2(density).align(RegionAlign { x, y }), (Some(x), None) => { - let x = used.x.apply_rest().align(x); + let x = used.x.apply_rest(density).align(x); UiRegion::new(x, UiSpan::FULL) } (None, Some(y)) => { - let y = used.y.apply_rest().align(y); + let y = used.y.apply_rest(density).align(y); UiRegion::new(UiSpan::FULL, y) } (None, None) => UiRegion::FULL, diff --git a/iris/src/widget/position/max_size.rs b/iris/src/widget/position/max_size.rs index bd56f57..45fed7f 100644 --- a/iris/src/widget/position/max_size.rs +++ b/iris/src/widget/position/max_size.rs @@ -9,12 +9,12 @@ pub struct MaxSize { impl MaxSize { /// Caps a reported length at `max`, comparing in pixels since `Len`'s /// rel/abs/rest components are not otherwise comparable. - fn clamp(len: Len, max: Option, output: f32) -> Len { + fn clamp(len: Len, max: Option, output: f32, density: f32) -> Len { let Some(max) = max else { return len; }; - let len_px = len.apply_rest().to_abs(output); - let max_px = max.apply_rest().to_abs(output); + let len_px = len.apply_rest(density).to_abs(output); + let max_px = max.apply_rest(density).to_abs(output); if len_px > max_px { max } else { len } } @@ -24,11 +24,11 @@ impl MaxSize { /// start, if it does not. Needed so the child is never painted bigger /// than the size this widget reports for it -- see the identical /// requirement noted on `Sized::draw`. - fn clamp_region(offered_px: f32, max: Option, output: f32) -> UiSpan { + fn clamp_region(offered_px: f32, max: Option, output: f32, density: f32) -> UiSpan { let Some(max) = max else { return UiSpan::FULL; }; - let max_scalar = max.apply_rest(); + let max_scalar = max.apply_rest(density); let max_px = max_scalar.to_abs(output); if offered_px > max_px { max_scalar.align(AxisAlign::Neg) @@ -41,15 +41,16 @@ impl MaxSize { impl Widget for MaxSize { fn draw(&mut self, painter: &mut Painter) -> Size { let output = painter.output_size(); + let density = painter.density(); let offered = painter.px_size(); let region = UiRegion { - x: Self::clamp_region(offered.x, self.x, output.x), - y: Self::clamp_region(offered.y, self.y, output.y), + x: Self::clamp_region(offered.x, self.x, output.x, density), + y: Self::clamp_region(offered.y, self.y, output.y, density), }; let used = painter.widget_within(&self.inner, region); Size { - x: Self::clamp(used.x, self.x, output.x), - y: Self::clamp(used.y, self.y, output.y), + x: Self::clamp(used.x, self.x, output.x, density), + y: Self::clamp(used.y, self.y, output.y, density), } } } diff --git a/iris/src/widget/position/pad.rs b/iris/src/widget/position/pad.rs index 065fce8..8e2b3ec 100644 --- a/iris/src/widget/position/pad.rs +++ b/iris/src/widget/position/pad.rs @@ -7,9 +7,12 @@ pub struct Pad { impl Widget for Pad { fn draw(&mut self, painter: &mut Painter) -> Size { - let used = painter.widget_within(&self.inner, self.padding.region()); - let width = self.padding.left + self.padding.right; - let height = self.padding.top + self.padding.bottom; + let density = painter.density(); + let used = painter.widget_within(&self.inner, self.padding.region(density)); + let width = + self.padding.left.apply_rest(density).abs + self.padding.right.apply_rest(density).abs; + let height = + self.padding.top.apply_rest(density).abs + self.padding.bottom.apply_rest(density).abs; Size { x: used.x + Len::abs(width), y: used.y + Len::abs(height), @@ -17,23 +20,29 @@ impl Widget for Pad { } } +/// Each side is a `Len`, not a bare `f32`, so `.pad(dp(10))` resolves +/// against the display's density the same way any other size does -- see +/// `Len::dp`'s field doc. `.pad(10)` (a bare number) still works via +/// `From` below, unchanged: it becomes an `abs` (physical-pixel) +/// `Len`, exactly as a bare number always has meant elsewhere in this +/// crate. pub struct Padding { - pub left: f32, - pub right: f32, - pub top: f32, - pub bottom: f32, + pub left: Len, + pub right: Len, + pub top: Len, + pub bottom: Len, } impl Padding { pub const ZERO: Self = Self { - left: 0.0, - right: 0.0, - top: 0.0, - bottom: 0.0, + left: Len::ZERO, + right: Len::ZERO, + top: Len::ZERO, + bottom: Len::ZERO, }; - pub fn uniform(amt: impl UiNum) -> Self { - let amt = amt.to_f32(); + pub fn uniform(amt: impl Into) -> Self { + let amt = amt.into(); Self { left: amt, right: amt, @@ -41,80 +50,84 @@ impl Padding { bottom: amt, } } - pub fn region(&self) -> UiRegion { + pub fn region(&self, density: f32) -> UiRegion { let mut region = UiRegion::FULL; - region.x.start.abs += self.left; - region.y.start.abs += self.top; - region.x.end.abs -= self.right; - region.y.end.abs -= self.bottom; + region.x.start.abs += self.left.apply_rest(density).abs; + region.y.start.abs += self.top.apply_rest(density).abs; + region.x.end.abs -= self.right.apply_rest(density).abs; + region.y.end.abs -= self.bottom.apply_rest(density).abs; region } - pub fn x(amt: impl UiNum) -> Self { - let amt = amt.to_f32(); + pub fn x(amt: impl Into) -> Self { + let amt = amt.into(); Self { left: amt, right: amt, - top: 0.0, - bottom: 0.0, + top: Len::ZERO, + bottom: Len::ZERO, } } - pub fn y(amt: impl UiNum) -> Self { - let amt = amt.to_f32(); + pub fn y(amt: impl Into) -> Self { + let amt = amt.into(); Self { - left: 0.0, - right: 0.0, + left: Len::ZERO, + right: Len::ZERO, top: amt, bottom: amt, } } - pub fn top(amt: impl UiNum) -> Self { + pub fn top(amt: impl Into) -> Self { let mut s = Self::ZERO; - s.top = amt.to_f32(); + s.top = amt.into(); s } - pub fn bottom(amt: impl UiNum) -> Self { + pub fn bottom(amt: impl Into) -> Self { let mut s = Self::ZERO; - s.bottom = amt.to_f32(); + s.bottom = amt.into(); s } - pub fn left(amt: impl UiNum) -> Self { + pub fn left(amt: impl Into) -> Self { let mut s = Self::ZERO; - s.left = amt.to_f32(); + s.left = amt.into(); s } - pub fn right(amt: impl UiNum) -> Self { + pub fn right(amt: impl Into) -> Self { let mut s = Self::ZERO; - s.right = amt.to_f32(); + s.right = amt.into(); s } - pub fn with_top(mut self, amt: impl UiNum) -> Self { - self.top = amt.to_f32(); + pub fn with_top(mut self, amt: impl Into) -> Self { + self.top = amt.into(); self } - pub fn with_bottom(mut self, amt: impl UiNum) -> Self { - self.bottom = amt.to_f32(); + pub fn with_bottom(mut self, amt: impl Into) -> Self { + self.bottom = amt.into(); self } - pub fn with_left(mut self, amt: impl UiNum) -> Self { - self.left = amt.to_f32(); + pub fn with_left(mut self, amt: impl Into) -> Self { + self.left = amt.into(); self } - pub fn with_right(mut self, amt: impl UiNum) -> Self { - self.right = amt.to_f32(); + pub fn with_right(mut self, amt: impl Into) -> Self { + self.right = amt.into(); self } } -impl From for Padding { +/// Covers both a bare number (`.pad(8)`, via `Len`'s own `From` +/// blanket -- an `abs`/physical-pixel `Len`) and a `Len` directly +/// (`.pad(dp(10))`) with the one impl, since `Len: Into` is the +/// reflexive case of the same bound. +impl> From for Padding { fn from(amt: T) -> Self { - Self::uniform(amt.to_f32()) + Self::uniform(amt.into()) } } diff --git a/iris/src/widget/position/scroll.rs b/iris/src/widget/position/scroll.rs index 4c837e5..e35f8d2 100644 --- a/iris/src/widget/position/scroll.rs +++ b/iris/src/widget/position/scroll.rs @@ -43,7 +43,7 @@ impl Widget for Scroll { self.content_len = used .axis(axis) - .apply_rest() + .apply_rest(painter.density()) .within_len(container_len) .to_abs(output_len); diff --git a/iris/src/widget/position/sized.rs b/iris/src/widget/position/sized.rs index 8c104de..968748b 100644 --- a/iris/src/widget/position/sized.rs +++ b/iris/src/widget/position/sized.rs @@ -17,12 +17,13 @@ impl Widget for Sized { // learn its size, then moves it into place with a pure // translation; that translation is only valid if what got painted // is already the reported size, anchored the same way both times. + let density = painter.density(); let mut region = UiRegion::FULL; if let Some(x) = self.x { - region.x = x.apply_rest().align(AxisAlign::Neg); + region.x = x.apply_rest(density).align(AxisAlign::Neg); } if let Some(y) = self.y { - region.y = y.apply_rest().align(AxisAlign::Neg); + region.y = y.apply_rest(density).align(AxisAlign::Neg); } let used = painter.widget_within(&self.inner, region); Size { diff --git a/iris/src/widget/position/span.rs b/iris/src/widget/position/span.rs index a244f74..9ecca40 100644 --- a/iris/src/widget/position/span.rs +++ b/iris/src/widget/position/span.rs @@ -4,12 +4,18 @@ use std::marker::PhantomData; pub struct Span { pub children: Vec, pub dir: Dir, - pub gap: f32, + /// A `Len` (not a bare `f32`) so `dp(4)` resolves against the display's + /// density the same way any other size in the tree does -- see + /// `Len::dp`'s field doc. Only the `abs` component (folded from `dp` at + /// draw time, `Widget::draw` below) is meaningful here; `rel`/`rest` + /// were never supported for a gap and still are not. + pub gap: Len, } impl Widget for Span { fn draw(&mut self, painter: &mut Painter) -> Size { let axis = self.dir.axis; + let gap = self.gap.apply_rest(painter.density()).abs; // Phase 1: draw each child once, at the ambient (unmodified, full) // region a size-only query used to see before this migration, to @@ -25,7 +31,7 @@ impl Widget for Span { .map(|child| painter.widget(child).axis(axis)) .collect(); - let gap_total = self.gap * self.children.len().saturating_sub(1) as f32; + let gap_total = gap * self.children.len().saturating_sub(1) as f32; let total = lens.iter().fold(Len::abs(gap_total), |s, &l| s + l); // Phase 2: place each child for real, using the lengths just @@ -54,7 +60,7 @@ impl Widget for Span { child_region.flip(axis); } let used = painter.widget_within(child, child_region); - start.abs += self.gap; + start.abs += gap; let ortho = used.axis(!axis); if ortho.rel > 0.0 || ortho.rest > 0.0 { @@ -82,12 +88,12 @@ impl Span { Self { children: Vec::new(), dir, - gap: 0.0, + gap: Len::ZERO, } } - pub fn gap(mut self, gap: impl UiNum) -> Self { - self.gap = gap.to_f32(); + pub fn gap(mut self, gap: impl Into) -> Self { + self.gap = gap.into(); self } @@ -103,7 +109,7 @@ impl Span { pub struct SpanBuilder, Tag> { pub children: Wa, pub dir: Dir, - pub gap: f32, + pub gap: Len, _pd: PhantomData<(State, Tag)>, } @@ -129,13 +135,13 @@ impl, Tag> Self { children, dir, - gap: 0.0, + gap: Len::ZERO, _pd: PhantomData, } } - pub fn gap(mut self, gap: impl UiNum) -> Self { - self.gap = gap.to_f32(); + pub fn gap(mut self, gap: impl Into) -> Self { + self.gap = gap.into(); self } } diff --git a/iris/src/widget/text/edit.rs b/iris/src/widget/text/edit.rs index 2044ab6..3feb162 100644 --- a/iris/src/widget/text/edit.rs +++ b/iris/src/widget/text/edit.rs @@ -141,7 +141,8 @@ impl<'a> TextEditCtx<'a> { fn layout(&mut self) -> &Layout { let attrs = self.text.view.attrs.clone(); let width = self.text.view.wrap_width(); - self.text.view.buf.shape(self.data, &attrs, width); + let density = self.data.density; + self.text.view.buf.shape(self.data, &attrs, width, density); self.text.view.buf.layout() } diff --git a/iris/transcript-ui/src/composer.rs b/iris/transcript-ui/src/composer.rs index f661f05..741c1b1 100644 --- a/iris/transcript-ui/src/composer.rs +++ b/iris/transcript-ui/src/composer.rs @@ -39,7 +39,7 @@ where .label("Message") .add(rsc); - let bar: WeakWidget = (field.pad(12).width(rest(1)),) + let bar: WeakWidget = (field.pad(dp(12)).width(rest(1)),) .span(Dir::RIGHT) .background(rect(UiColor::new(40, 40, 46, 255))) .add(rsc); diff --git a/iris/transcript-ui/src/row.rs b/iris/transcript-ui/src/row.rs index 92581ae..693a9e9 100644 --- a/iris/transcript-ui/src/row.rs +++ b/iris/transcript-ui/src/row.rs @@ -174,8 +174,8 @@ where (header, field.width(rest(1))) .span(Dir::DOWN) - .gap(4) - .pad(10) + .gap(dp(4)) + .pad(dp(10)) .add_strong(rsc) .any() }