iris: a dp length unit, resolved against density; crisp glyphs at physical size
Iris asked for this 2026-09-06 (IRIS_TODO.md, "a third length kind beside relative and pixels ... a unit resolved against the display's density at layout time"): before this, a Len was abs (physical pixels) or rel/rest (a fraction of the parent), and the only way to make a design size look the same physical size on a denser display was a single global multiply applied after layout -- which the previous commit found is also what made text blurry. Len gains a `dp` field, resolved against a `density: f32` (physical pixels per dp) now carried on UiRenderState/Painter (`UiRenderState::set_density`/`density()`, `Painter::density()`) and threaded through every `apply_rest`/`to_uivec2` call site. `len_fns::dp` / `Len::dp` construct one, exactly parallel to the existing `abs`/`rel`/ `rest`. A bare number is unaffected (still `abs`, physical pixels) -- `dp` is opt-in. Text: `TextBuffer::shape` now takes `density` and multiplies `font_size`/`line_height` (and any span override) by it before handing them to parley, so the size that reaches the shaper and the rasteriser (`TextData::place`) is the display's real physical size -- the atlas holds a bitmap at the resolution it is actually shown at, instead of a low-resolution one stretched afterward. `GlyphKey.size` already keys on the resolved `font_size`, so a cache entry is naturally per physical size with no further change. `TextData` also carries its own `density` copy for `TextEditCtx::layout` (cursor movement/hit-testing), which shapes text from an input callback with no `Painter` to read it from. `Span::gap` and `Padding`'s four sides move from bare `f32` to `Len`, so `.gap(dp(4))`/`.pad(dp(10))` work the same way any other size does; a bare number still means physical pixels, unchanged. Migrated transcript-ui's non-text sizes (row gap/padding, composer padding) and one example to the new unit, per IRIS_TODO.md's "done when" list. Android's own density (`DisplayMetrics.density`) is wired to both copies in `new_peer`; the winit backend has no per-monitor density wired up yet and stays at the default (1.0). docs/IRIS.md, docs/LAYOUT.md and IRIS_TODO.md updated next. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0da383e28
commit
6102e0d4d9
15 files changed
+244
-95
No files matched your search
@@ -9,7 +9,31 @@ pub struct Size {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Len {
|
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,
|
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 rel: f32,
|
||||||
pub rest: 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 {
|
UiVec2 {
|
||||||
x: self.x.apply_rest(),
|
x: self.x.apply_rest(density),
|
||||||
y: self.y.apply_rest(),
|
y: self.y.apply_rest(density),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,26 +122,43 @@ impl Size {
|
|||||||
impl Len {
|
impl Len {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const REST: Self = Self {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 1.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 {
|
UiScalar {
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
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 {
|
pub fn abs(abs: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: abs.to_f32(),
|
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,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
@@ -125,6 +166,7 @@ impl Len {
|
|||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
@@ -132,6 +174,7 @@ impl Len {
|
|||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn rest(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
@@ -144,6 +187,15 @@ pub mod len_fns {
|
|||||||
pub fn abs(abs: impl UiNum) -> Len {
|
pub fn abs(abs: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: abs.to_f32(),
|
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,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
@@ -151,6 +203,7 @@ pub mod len_fns {
|
|||||||
pub fn rel(rel: impl UiNum) -> Len {
|
pub fn rel(rel: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
@@ -158,14 +211,15 @@ pub mod len_fns {
|
|||||||
pub fn rest(ratio: impl UiNum) -> Len {
|
pub fn rest(ratio: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: 0.0,
|
abs: 0.0,
|
||||||
|
dp: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(Len Add add; abs rel rest);
|
impl_op!(Len Add add; abs dp rel rest);
|
||||||
impl_op!(Len Sub sub; abs rel rest);
|
impl_op!(Len Sub sub; abs dp rel rest);
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
impl_op!(Size Add add; x y);
|
||||||
impl_op!(Size Sub sub; x y);
|
impl_op!(Size Sub sub; x y);
|
||||||
@@ -187,6 +241,9 @@ impl std::fmt::Display for Len {
|
|||||||
if self.abs != 0.0 {
|
if self.abs != 0.0 {
|
||||||
write!(f, "{} abs;", self.abs)?;
|
write!(f, "{} abs;", self.abs)?;
|
||||||
}
|
}
|
||||||
|
if self.dp != 0.0 {
|
||||||
|
write!(f, "{} dp;", self.dp)?;
|
||||||
|
}
|
||||||
if self.rel != 0.0 {
|
if self.rel != 0.0 {
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,17 @@ pub struct TextData {
|
|||||||
pub layout_cx: LayoutContext<UiColor>,
|
pub layout_cx: LayoutContext<UiColor>,
|
||||||
scale_cx: ScaleContext,
|
scale_cx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
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 {
|
impl Default for TextData {
|
||||||
@@ -75,6 +86,7 @@ impl Default for TextData {
|
|||||||
layout_cx: LayoutContext::new(),
|
layout_cx: LayoutContext::new(),
|
||||||
scale_cx: ScaleContext::new(),
|
scale_cx: ScaleContext::new(),
|
||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
|
density: 1.0,
|
||||||
};
|
};
|
||||||
data.register_bundled_fonts();
|
data.register_bundled_fonts();
|
||||||
data
|
data
|
||||||
@@ -363,7 +375,7 @@ pub struct TextBuffer {
|
|||||||
/// `set_spans` forces `shaped` to `None` directly, the same way `edit`
|
/// `set_spans` forces `shaped` to `None` directly, the same way `edit`
|
||||||
/// does, since spans change far less often than a naive equality check
|
/// does, since spans change far less often than a naive equality check
|
||||||
/// on the whole `Vec` would cost to compute every frame.
|
/// on the whole `Vec` would cost to compute every frame.
|
||||||
shaped: Option<(TextAttrs, Option<f32>)>,
|
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextBuffer {
|
impl TextBuffer {
|
||||||
@@ -419,19 +431,42 @@ impl TextBuffer {
|
|||||||
Vec2::new(self.layout.width(), self.layout.height())
|
Vec2::new(self.layout.width(), self.layout.height())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lay the text out, unless it is already laid out for these attributes and
|
/// Lay the text out, unless it is already laid out for these
|
||||||
/// this width.
|
/// attributes, this width and this density.
|
||||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
///
|
||||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
|
/// **`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<f32>,
|
||||||
|
density: f32,
|
||||||
|
) {
|
||||||
|
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut builder = data
|
let mut builder = data
|
||||||
.layout_cx
|
.layout_cx
|
||||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||||
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
|
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(
|
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||||
attrs.line_height,
|
attrs.line_height * density,
|
||||||
)));
|
)));
|
||||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||||
for span in &self.spans {
|
for span in &self.spans {
|
||||||
@@ -443,7 +478,7 @@ impl TextBuffer {
|
|||||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||||
}
|
}
|
||||||
if let Some(size) = span.font_size {
|
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 {
|
if span.bold {
|
||||||
builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone());
|
builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone());
|
||||||
@@ -459,7 +494,7 @@ impl TextBuffer {
|
|||||||
self.layout.break_all_lines(width);
|
self.layout.break_all_lines(width);
|
||||||
self.layout
|
self.layout
|
||||||
.align(Alignment::Start, AlignmentOptions::default());
|
.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,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
textures: &mut Textures,
|
textures: &mut Textures,
|
||||||
|
density: f32,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
buffer.shape(self, attrs, width);
|
buffer.shape(self, attrs, width, density);
|
||||||
let glyphs = self.place(buffer, textures);
|
let glyphs = self.place(buffer, textures);
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs: std::sync::Arc::new(glyphs),
|
glyphs: std::sync::Arc::new(glyphs),
|
||||||
|
|||||||
@@ -165,8 +165,10 @@ impl<'a> Painter<'a> {
|
|||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
|
let density = self.state.density;
|
||||||
let ui = self.rsc.ui_mut();
|
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.
|
/// 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
|
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 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
self.region.size().to_abs(self.state.output_size)
|
self.region.size().to_abs(self.state.output_size)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ pub struct UiRenderState {
|
|||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: PrimitiveLayers,
|
pub layers: PrimitiveLayers,
|
||||||
pub(super) output_size: Vec2,
|
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<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
resized: bool,
|
resized: bool,
|
||||||
@@ -35,6 +41,7 @@ impl UiRenderState {
|
|||||||
active: Default::default(),
|
active: Default::default(),
|
||||||
layers: Default::default(),
|
layers: Default::default(),
|
||||||
output_size: Vec2::ZERO,
|
output_size: Vec2::ZERO,
|
||||||
|
density: 1.0,
|
||||||
old_root: None,
|
old_root: None,
|
||||||
resized: false,
|
resized: false,
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
@@ -60,6 +67,20 @@ impl UiRenderState {
|
|||||||
self.resized = true;
|
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<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
||||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
@@ -311,7 +332,7 @@ impl UiRenderState {
|
|||||||
};
|
};
|
||||||
let from = active
|
let from = active
|
||||||
.size
|
.size
|
||||||
.to_uivec2()
|
.to_uivec2(self.density)
|
||||||
.align(RegionAlign::TOP_LEFT)
|
.align(RegionAlign::TOP_LEFT)
|
||||||
.within(&active.region);
|
.within(&active.region);
|
||||||
let slot = active.move_slot;
|
let slot = active.move_slot;
|
||||||
|
|||||||
@@ -68,12 +68,15 @@ fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
|
|||||||
let mut span = Span::empty(Dir::DOWN);
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
span.push(text);
|
span.push(text);
|
||||||
span.push(img);
|
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 {
|
} else {
|
||||||
wtext(row_text(i))
|
wtext(row_text(i))
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.color(text_color)
|
.color(text_color)
|
||||||
.pad(8.0)
|
.pad(dp(8.0))
|
||||||
.background(rect(tint))
|
.background(rect(tint))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any()
|
.any()
|
||||||
|
|||||||
@@ -606,9 +606,10 @@ impl List {
|
|||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let output_len = painter.output_size().axis(axis);
|
let output_len = painter.output_size().axis(axis);
|
||||||
let container_len = painter.region().axis(axis).len();
|
let container_len = painter.region().axis(axis).len();
|
||||||
|
let density = painter.density();
|
||||||
let resolve = move |used: Size| -> f32 {
|
let resolve = move |used: Size| -> f32 {
|
||||||
used.axis(axis)
|
used.axis(axis)
|
||||||
.apply_rest()
|
.apply_rest(density)
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_abs(output_len)
|
.to_abs(output_len)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ impl Widget for Aligned {
|
|||||||
// already-resolved region double-applies that composition and is
|
// already-resolved region double-applies that composition and is
|
||||||
// wrong for any widget nested below the root.
|
// wrong for any widget nested below the root.
|
||||||
let used = painter.widget(&self.inner);
|
let used = painter.widget(&self.inner);
|
||||||
|
let density = painter.density();
|
||||||
let region = match self.align.tuple() {
|
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) => {
|
(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)
|
UiRegion::new(x, UiSpan::FULL)
|
||||||
}
|
}
|
||||||
(None, Some(y)) => {
|
(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)
|
UiRegion::new(UiSpan::FULL, y)
|
||||||
}
|
}
|
||||||
(None, None) => UiRegion::FULL,
|
(None, None) => UiRegion::FULL,
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ pub struct MaxSize {
|
|||||||
impl MaxSize {
|
impl MaxSize {
|
||||||
/// Caps a reported length at `max`, comparing in pixels since `Len`'s
|
/// Caps a reported length at `max`, comparing in pixels since `Len`'s
|
||||||
/// rel/abs/rest components are not otherwise comparable.
|
/// rel/abs/rest components are not otherwise comparable.
|
||||||
fn clamp(len: Len, max: Option<Len>, output: f32) -> Len {
|
fn clamp(len: Len, max: Option<Len>, output: f32, density: f32) -> Len {
|
||||||
let Some(max) = max else {
|
let Some(max) = max else {
|
||||||
return len;
|
return len;
|
||||||
};
|
};
|
||||||
let len_px = len.apply_rest().to_abs(output);
|
let len_px = len.apply_rest(density).to_abs(output);
|
||||||
let max_px = max.apply_rest().to_abs(output);
|
let max_px = max.apply_rest(density).to_abs(output);
|
||||||
if len_px > max_px { max } else { len }
|
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
|
/// start, if it does not. Needed so the child is never painted bigger
|
||||||
/// than the size this widget reports for it -- see the identical
|
/// than the size this widget reports for it -- see the identical
|
||||||
/// requirement noted on `Sized::draw`.
|
/// requirement noted on `Sized::draw`.
|
||||||
fn clamp_region(offered_px: f32, max: Option<Len>, output: f32) -> UiSpan {
|
fn clamp_region(offered_px: f32, max: Option<Len>, output: f32, density: f32) -> UiSpan {
|
||||||
let Some(max) = max else {
|
let Some(max) = max else {
|
||||||
return UiSpan::FULL;
|
return UiSpan::FULL;
|
||||||
};
|
};
|
||||||
let max_scalar = max.apply_rest();
|
let max_scalar = max.apply_rest(density);
|
||||||
let max_px = max_scalar.to_abs(output);
|
let max_px = max_scalar.to_abs(output);
|
||||||
if offered_px > max_px {
|
if offered_px > max_px {
|
||||||
max_scalar.align(AxisAlign::Neg)
|
max_scalar.align(AxisAlign::Neg)
|
||||||
@@ -41,15 +41,16 @@ impl MaxSize {
|
|||||||
impl Widget for MaxSize {
|
impl Widget for MaxSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output = painter.output_size();
|
let output = painter.output_size();
|
||||||
|
let density = painter.density();
|
||||||
let offered = painter.px_size();
|
let offered = painter.px_size();
|
||||||
let region = UiRegion {
|
let region = UiRegion {
|
||||||
x: Self::clamp_region(offered.x, self.x, output.x),
|
x: Self::clamp_region(offered.x, self.x, output.x, density),
|
||||||
y: Self::clamp_region(offered.y, self.y, output.y),
|
y: Self::clamp_region(offered.y, self.y, output.y, density),
|
||||||
};
|
};
|
||||||
let used = painter.widget_within(&self.inner, region);
|
let used = painter.widget_within(&self.inner, region);
|
||||||
Size {
|
Size {
|
||||||
x: Self::clamp(used.x, self.x, output.x),
|
x: Self::clamp(used.x, self.x, output.x, density),
|
||||||
y: Self::clamp(used.y, self.y, output.y),
|
y: Self::clamp(used.y, self.y, output.y, density),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,12 @@ pub struct Pad {
|
|||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let used = painter.widget_within(&self.inner, self.padding.region());
|
let density = painter.density();
|
||||||
let width = self.padding.left + self.padding.right;
|
let used = painter.widget_within(&self.inner, self.padding.region(density));
|
||||||
let height = self.padding.top + self.padding.bottom;
|
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 {
|
Size {
|
||||||
x: used.x + Len::abs(width),
|
x: used.x + Len::abs(width),
|
||||||
y: used.y + Len::abs(height),
|
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<T: UiNum>` 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 struct Padding {
|
||||||
pub left: f32,
|
pub left: Len,
|
||||||
pub right: f32,
|
pub right: Len,
|
||||||
pub top: f32,
|
pub top: Len,
|
||||||
pub bottom: f32,
|
pub bottom: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Padding {
|
impl Padding {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
left: 0.0,
|
left: Len::ZERO,
|
||||||
right: 0.0,
|
right: Len::ZERO,
|
||||||
top: 0.0,
|
top: Len::ZERO,
|
||||||
bottom: 0.0,
|
bottom: Len::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn uniform(amt: impl UiNum) -> Self {
|
pub fn uniform(amt: impl Into<Len>) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = amt.into();
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
@@ -41,80 +50,84 @@ impl Padding {
|
|||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self, density: f32) -> UiRegion {
|
||||||
let mut region = UiRegion::FULL;
|
let mut region = UiRegion::FULL;
|
||||||
region.x.start.abs += self.left;
|
region.x.start.abs += self.left.apply_rest(density).abs;
|
||||||
region.y.start.abs += self.top;
|
region.y.start.abs += self.top.apply_rest(density).abs;
|
||||||
region.x.end.abs -= self.right;
|
region.x.end.abs -= self.right.apply_rest(density).abs;
|
||||||
region.y.end.abs -= self.bottom;
|
region.y.end.abs -= self.bottom.apply_rest(density).abs;
|
||||||
region
|
region
|
||||||
}
|
}
|
||||||
pub fn x(amt: impl UiNum) -> Self {
|
pub fn x(amt: impl Into<Len>) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = amt.into();
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
top: 0.0,
|
top: Len::ZERO,
|
||||||
bottom: 0.0,
|
bottom: Len::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn y(amt: impl UiNum) -> Self {
|
pub fn y(amt: impl Into<Len>) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = amt.into();
|
||||||
Self {
|
Self {
|
||||||
left: 0.0,
|
left: Len::ZERO,
|
||||||
right: 0.0,
|
right: Len::ZERO,
|
||||||
top: amt,
|
top: amt,
|
||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn top(amt: impl UiNum) -> Self {
|
pub fn top(amt: impl Into<Len>) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.top = amt.to_f32();
|
s.top = amt.into();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bottom(amt: impl UiNum) -> Self {
|
pub fn bottom(amt: impl Into<Len>) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.bottom = amt.to_f32();
|
s.bottom = amt.into();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left(amt: impl UiNum) -> Self {
|
pub fn left(amt: impl Into<Len>) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.left = amt.to_f32();
|
s.left = amt.into();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn right(amt: impl UiNum) -> Self {
|
pub fn right(amt: impl Into<Len>) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.right = amt.to_f32();
|
s.right = amt.into();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_top(mut self, amt: impl UiNum) -> Self {
|
pub fn with_top(mut self, amt: impl Into<Len>) -> Self {
|
||||||
self.top = amt.to_f32();
|
self.top = amt.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
|
pub fn with_bottom(mut self, amt: impl Into<Len>) -> Self {
|
||||||
self.bottom = amt.to_f32();
|
self.bottom = amt.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_left(mut self, amt: impl UiNum) -> Self {
|
pub fn with_left(mut self, amt: impl Into<Len>) -> Self {
|
||||||
self.left = amt.to_f32();
|
self.left = amt.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_right(mut self, amt: impl UiNum) -> Self {
|
pub fn with_right(mut self, amt: impl Into<Len>) -> Self {
|
||||||
self.right = amt.to_f32();
|
self.right = amt.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: UiNum> From<T> for Padding {
|
/// Covers both a bare number (`.pad(8)`, via `Len`'s own `From<N: UiNum>`
|
||||||
|
/// blanket -- an `abs`/physical-pixel `Len`) and a `Len` directly
|
||||||
|
/// (`.pad(dp(10))`) with the one impl, since `Len: Into<Len>` is the
|
||||||
|
/// reflexive case of the same bound.
|
||||||
|
impl<T: Into<Len>> From<T> for Padding {
|
||||||
fn from(amt: T) -> Self {
|
fn from(amt: T) -> Self {
|
||||||
Self::uniform(amt.to_f32())
|
Self::uniform(amt.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ impl Widget for Scroll {
|
|||||||
|
|
||||||
self.content_len = used
|
self.content_len = used
|
||||||
.axis(axis)
|
.axis(axis)
|
||||||
.apply_rest()
|
.apply_rest(painter.density())
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_abs(output_len);
|
.to_abs(output_len);
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ impl Widget for Sized {
|
|||||||
// learn its size, then moves it into place with a pure
|
// learn its size, then moves it into place with a pure
|
||||||
// translation; that translation is only valid if what got painted
|
// translation; that translation is only valid if what got painted
|
||||||
// is already the reported size, anchored the same way both times.
|
// is already the reported size, anchored the same way both times.
|
||||||
|
let density = painter.density();
|
||||||
let mut region = UiRegion::FULL;
|
let mut region = UiRegion::FULL;
|
||||||
if let Some(x) = self.x {
|
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 {
|
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);
|
let used = painter.widget_within(&self.inner, region);
|
||||||
Size {
|
Size {
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ use std::marker::PhantomData;
|
|||||||
pub struct Span {
|
pub struct Span {
|
||||||
pub children: Vec<StrongWidget>,
|
pub children: Vec<StrongWidget>,
|
||||||
pub dir: Dir,
|
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 {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let axis = self.dir.axis;
|
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)
|
// Phase 1: draw each child once, at the ambient (unmodified, full)
|
||||||
// region a size-only query used to see before this migration, to
|
// 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))
|
.map(|child| painter.widget(child).axis(axis))
|
||||||
.collect();
|
.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);
|
let total = lens.iter().fold(Len::abs(gap_total), |s, &l| s + l);
|
||||||
|
|
||||||
// Phase 2: place each child for real, using the lengths just
|
// Phase 2: place each child for real, using the lengths just
|
||||||
@@ -54,7 +60,7 @@ impl Widget for Span {
|
|||||||
child_region.flip(axis);
|
child_region.flip(axis);
|
||||||
}
|
}
|
||||||
let used = painter.widget_within(child, child_region);
|
let used = painter.widget_within(child, child_region);
|
||||||
start.abs += self.gap;
|
start.abs += gap;
|
||||||
|
|
||||||
let ortho = used.axis(!axis);
|
let ortho = used.axis(!axis);
|
||||||
if ortho.rel > 0.0 || ortho.rest > 0.0 {
|
if ortho.rel > 0.0 || ortho.rest > 0.0 {
|
||||||
@@ -82,12 +88,12 @@ impl Span {
|
|||||||
Self {
|
Self {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Len::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl Into<Len>) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = gap.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +109,7 @@ impl Span {
|
|||||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||||
pub children: Wa,
|
pub children: Wa,
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
pub gap: f32,
|
pub gap: Len,
|
||||||
_pd: PhantomData<(State, Tag)>,
|
_pd: PhantomData<(State, Tag)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,13 +135,13 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
|||||||
Self {
|
Self {
|
||||||
children,
|
children,
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Len::ZERO,
|
||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl Into<Len>) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = gap.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
fn layout(&mut self) -> &Layout<UiColor> {
|
fn layout(&mut self) -> &Layout<UiColor> {
|
||||||
let attrs = self.text.view.attrs.clone();
|
let attrs = self.text.view.attrs.clone();
|
||||||
let width = self.text.view.wrap_width();
|
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()
|
self.text.view.buf.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ where
|
|||||||
.label("Message")
|
.label("Message")
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let bar: WeakWidget = (field.pad(12).width(rest(1)),)
|
let bar: WeakWidget = (field.pad(dp(12)).width(rest(1)),)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.background(rect(UiColor::new(40, 40, 46, 255)))
|
.background(rect(UiColor::new(40, 40, 46, 255)))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ where
|
|||||||
|
|
||||||
(header, field.width(rest(1)))
|
(header, field.width(rest(1)))
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.gap(4)
|
.gap(dp(4))
|
||||||
.pad(10)
|
.pad(dp(10))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any()
|
.any()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user