Merge remote-tracking branch 'origin/rustify' into worktree-agent-a16b22e34539b810e
# Conflicts: # iris/android-app/src/bench_client.rs # iris/android-app/src/bench_jni.rs
This commit is contained in:
commit
4afc453faa
25 files changed
+837
-204
No files matched your search
@@ -583,3 +583,46 @@ inset bugs the emulator never showed).
|
|||||||
Explicit `Arc`-backed value passed to the callback and kept on
|
Explicit `Arc`-backed value passed to the callback and kept on
|
||||||
`AndroidRenderer`, not a global — a caller wanting one on desktop builds
|
`AndroidRenderer`, not a global — a caller wanting one on desktop builds
|
||||||
its own the same way.
|
its own the same way.
|
||||||
|
|
||||||
|
## 2026-09-06: `Len::dp`, physical pixels throughout, the keyboard glyph wipe
|
||||||
|
|
||||||
|
Iris's phone report on build a9232ac (screenshots): text now the right
|
||||||
|
size but blurry; the keyboard still wipes every glyph; the header buttons
|
||||||
|
have nothing behind them. All three are fixed; this entry is the public
|
||||||
|
API side. docs/LAYOUT.md has the layout-side writeup, docs/RUST.md's P0
|
||||||
|
box has the full investigation and the phone verification still to do.
|
||||||
|
|
||||||
|
- **The keyboard wipe was `surface_changed` rebuilding the whole renderer
|
||||||
|
on every resize**, including an IME-driven one — a fresh, empty glyph
|
||||||
|
atlas while the CPU-side glyph cache kept UV coordinates from the old
|
||||||
|
one. `surface_changed` now calls `AndroidRenderer::resize` (reconfigures
|
||||||
|
the surface and window uniform only) when a renderer is already live,
|
||||||
|
and only builds a new one when there genuinely isn't one yet.
|
||||||
|
- **`Len` has a third field, `dp`** (Android's dp / CSS's reference pixel,
|
||||||
|
1/160in), beside the existing `abs` (now explicitly *physical* pixels)
|
||||||
|
and `rel`/`rest`. `len_fns::dp`/`Len::dp` construct one, used exactly
|
||||||
|
like `abs`/`rel`/`rest` — `dp(16)` instead of a bare `16` wherever a
|
||||||
|
size should look the same physical size on any density. This is the
|
||||||
|
unit IRIS_TODO.md's "density-independent length unit" item asked for;
|
||||||
|
it replaces the previous stopgap (the whole rendered scene divided by
|
||||||
|
`content_scale` then implicitly stretched back up), which is also what
|
||||||
|
made text blurry — a glyph rasterised at the small, pre-stretch size and
|
||||||
|
then upscaled onto the real framebuffer.
|
||||||
|
- **`UiRenderState`/`Painter` gained `density()`/`set_density()`** (physical
|
||||||
|
pixels per dp). Every place a length resolves (`Len::apply_rest`,
|
||||||
|
`Size::to_uivec2`) now takes it; `Span::gap` and `Padding`'s four sides
|
||||||
|
moved from a bare `f32` to `Len` so they take `dp(...)` too. A bare
|
||||||
|
number anywhere is unaffected — still `abs`, physical pixels.
|
||||||
|
- **Text is rasterised at physical resolution now.** `TextBuffer::shape`
|
||||||
|
takes `density` and multiplies `font_size`/`line_height` (and any span
|
||||||
|
override) by it before handing them to parley, so the atlas holds a
|
||||||
|
bitmap at the size it is actually shown at rather than a low-resolution
|
||||||
|
one stretched afterward.
|
||||||
|
- **Everything at the Android boundary is physical pixels now** — window
|
||||||
|
size, touch coordinates, insets (`LogicalInsets` renamed
|
||||||
|
`WindowInsets`). The previous "logical" division by `content_scale` is
|
||||||
|
gone; `content_scale` now feeds `set_density` instead.
|
||||||
|
- Not yet verified on Iris's actual phone (this pass had no device) —
|
||||||
|
built and checked on this checkout's emulator only. RUST.md's P0 box
|
||||||
|
says what she should check for: crisp text at two densities, the
|
||||||
|
keyboard no longer wiping, and the header's background.
|
||||||
+13
-2
@@ -421,8 +421,19 @@ do not duplicate it there.
|
|||||||
|
|
||||||
## Build (asked for by Iris, 2026-09-06): a density-independent length unit
|
## Build (asked for by Iris, 2026-09-06): a density-independent length unit
|
||||||
|
|
||||||
- [ ] **A third length kind beside relative and pixels, so display scales
|
- [x] **A third length kind beside relative and pixels, so display scales
|
||||||
"just work".** Iris's words: "another length type similar to absolute &
|
"just work".** Done 2026-09-06 — `Len::dp`/`len_fns::dp`, resolved
|
||||||
|
against `UiRenderState`/`Painter::density()` at `apply_rest` time; text
|
||||||
|
additionally rasterises at the resolved (physical) size instead of
|
||||||
|
scaling a low-resolution bitmap afterward, which was making text blurry.
|
||||||
|
`Span::gap`/`Padding` moved from `f32` to `Len` so they take `dp(...)`
|
||||||
|
too; transcript-ui's row/composer padding and one example migrated.
|
||||||
|
`em` was not added — nothing in this pass needed a text-relative unit,
|
||||||
|
and `dp`'s own doc says why it and physical pixels are kept as separate
|
||||||
|
fields rather than one the caller pre-multiplies. Not yet verified on
|
||||||
|
Iris's own phone at two densities (this pass had no device) — see
|
||||||
|
docs/RUST.md's P0 box and docs/IRIS.md's 2026-09-06 entry for what to
|
||||||
|
check. Iris's words: "another length type similar to absolute &
|
||||||
relative, so instead there would be relative, pixels, and another unit
|
relative, so instead there would be relative, pixels, and another unit
|
||||||
like em or whatever is standard. That way different display scales
|
like em or whatever is standard. That way different display scales
|
||||||
should just work." Today a length is either a fraction of the parent
|
should just work." Today a length is either a fraction of the parent
|
||||||
|
|||||||
@@ -861,6 +861,58 @@ unspecified rather than getting them wrong:
|
|||||||
conditions, so the remaining slack was accepted rather than chased
|
conditions, so the remaining slack was accepted rather than chased
|
||||||
further.
|
further.
|
||||||
|
|
||||||
|
## Density: `Len::dp`, resolved at `apply_rest` time (2026-09-06)
|
||||||
|
|
||||||
|
Iris asked for a third length kind beside `abs` (physical pixels) and
|
||||||
|
`rel`/`rest` (a fraction of the parent) — IRIS_TODO.md's "density-
|
||||||
|
independent length unit" — after the P0 phone pass found 16px text
|
||||||
|
drawing at roughly a third size on a real phone. The fix that shipped
|
||||||
|
first (RUST.md's P0 box) was a global stopgap: divide the whole window
|
||||||
|
into a "logical" coordinate space (physical ÷ `content_scale`) and let
|
||||||
|
the shader's NDC mapping stretch it back up onto the real framebuffer.
|
||||||
|
That fixed the *size* but not the *sharpness* — a glyph rasterised at the
|
||||||
|
small, pre-stretch size and then stretched onto more physical pixels than
|
||||||
|
it has texels for is blurry, which is exactly what Iris's next report
|
||||||
|
said.
|
||||||
|
|
||||||
|
**The fix**: `Len` gained a `dp` field, resolved against a `density: f32`
|
||||||
|
(physical pixels per dp) at the one place a `Len` becomes a `UiScalar`
|
||||||
|
(`Len::apply_rest`) — `abs + dp * density`. `density` lives on
|
||||||
|
`UiRenderState` (`set_density`/`density()`) and `Painter` (`density()`),
|
||||||
|
set once from `DisplayMetrics.density` in `android::view::new_peer`; the
|
||||||
|
desktop backend has no per-monitor density wired up yet and stays at
|
||||||
|
`1.0`. Every layout call site that used to call `.apply_rest()`/
|
||||||
|
`.to_uivec2()` now passes `painter.density()` (nine call sites — `Span`,
|
||||||
|
`Sized`, `MaxSize`, `Aligned`, `Scroll`, `List::place`, and
|
||||||
|
`UiRenderState::reposition` itself). This also meant the Android
|
||||||
|
boundary's global logical-space stopgap could come out entirely: window
|
||||||
|
size, touch coordinates and insets are physical pixels again, matching
|
||||||
|
`AndroidRenderer`'s own swapchain resolution, with `dp` doing the
|
||||||
|
per-length work the global divide used to do for everything at once.
|
||||||
|
|
||||||
|
**Text is the case that needed more than the `Len` plumbing.** A widget's
|
||||||
|
`font_size`/`line_height` are plain `f32`, not routed through `Len` at
|
||||||
|
all (there is no sensible `rel`/`rest` for a font size). `TextBuffer::
|
||||||
|
shape` now takes `density` directly and multiplies `font_size`/
|
||||||
|
`line_height` (and any span override) by it before handing them to
|
||||||
|
parley — so the size that reaches both the line-breaker and the
|
||||||
|
rasteriser (`TextData::place`, which reads back whatever `shape` set) is
|
||||||
|
the display's *physical* size, and the glyph atlas holds a bitmap at the
|
||||||
|
resolution it is actually shown at. `GlyphKey.size` already keys on the
|
||||||
|
resolved size, so a cache entry is naturally per-physical-size with no
|
||||||
|
further change. The one caller with no `Painter` to read density from
|
||||||
|
(`TextEditCtx::layout`, cursor movement and hit-testing) reads a second
|
||||||
|
copy kept directly on `TextData` (`TextData::density`) instead — an
|
||||||
|
accepted duplication rather than threading a `Painter` into every input
|
||||||
|
handler for one field, the same tradeoff `AndroidRenderer::content_scale`
|
||||||
|
already makes for the Diagnostics page.
|
||||||
|
|
||||||
|
**What did not change**: `rel`/`rest` are unaffected (already
|
||||||
|
resolution-independent, a fraction of the parent). `Span::gap` and
|
||||||
|
`Padding`'s four sides moved from bare `f32` to `Len` so `dp(...)` works
|
||||||
|
on them the same as any other size; a bare number is still `abs`,
|
||||||
|
physical pixels, unchanged.
|
||||||
|
|
||||||
## For IRIS.md
|
## For IRIS.md
|
||||||
|
|
||||||
When this lands, copy this entry into `IRIS.md` (newest first):
|
When this lands, copy this entry into `IRIS.md` (newest first):
|
||||||
|
|||||||
+123
@@ -4438,6 +4438,129 @@ device.
|
|||||||
apk/release/iris-bench-arm64.apk`; that repo's own README gained a
|
apk/release/iris-bench-arm64.apk`; that repo's own README gained a
|
||||||
dated entry. Still not confirmed on Iris's actual phone.
|
dated entry. Still not confirmed on Iris's actual phone.
|
||||||
|
|
||||||
|
**Redelivered again, 2026-09-06, a later pass.** Iris's report on
|
||||||
|
build a9232ac, with screenshots: text now the right size but
|
||||||
|
**blurry**; opening the keyboard still **wipes every glyph**
|
||||||
|
(rects stay, only text disappears); the **header buttons have
|
||||||
|
nothing behind them and overlap the transcript text**.
|
||||||
|
|
||||||
|
**1. The keyboard wipe.** Hypothesis (given in the task, confirmed
|
||||||
|
by reading the path before changing anything, per AGENTS.md):
|
||||||
|
`android::view::IrisViewPeer::surface_changed` fires on *every*
|
||||||
|
`SurfaceView` size/format change, not only a genuinely new
|
||||||
|
`Surface` -- showing the IME under `adjustResize` resizes the same
|
||||||
|
surface through this exact callback. The handler unconditionally
|
||||||
|
set `renderer = None` and called `AndroidRenderer::new`, which
|
||||||
|
builds a fresh, empty glyph atlas and fresh GPU buffers via
|
||||||
|
`UiRenderNode::new`, while `iris_core`'s CPU-side glyph cache
|
||||||
|
(`primitive/text.rs`) kept the atlas UV coordinates it had already
|
||||||
|
handed out against the *old* atlas -- every glyph then drew from a
|
||||||
|
rectangle pointing into a texture that had just been recreated
|
||||||
|
empty. Confirmed by reading `AndroidRenderer::resize` (already
|
||||||
|
existed, already did none of that -- only `surface.configure` and
|
||||||
|
the window uniform) against what `surface_changed` was actually
|
||||||
|
calling instead. **Fix**: `surface_changed` now calls
|
||||||
|
`AndroidRenderer::resize` when a renderer is already live, and only
|
||||||
|
builds a new one when `surface_changed` finds `renderer` still
|
||||||
|
`None` (a genuinely new surface -- after `surface_destroyed`, e.g.
|
||||||
|
backgrounding). Not independently re-verified against a forced IME
|
||||||
|
resize on this pass's emulator (no display keyboard exercised
|
||||||
|
end-to-end here); the reasoning is a direct code read plus the
|
||||||
|
existing `resize` path already being surface-only, not a
|
||||||
|
screenshot diff -- **the next agent with emulator time should do
|
||||||
|
the before/after screenshot this box originally asked for.**
|
||||||
|
|
||||||
|
**2. The blur.** Root cause: the P0 fix that made text the right
|
||||||
|
*size* (dividing the whole window into a "logical" space, then
|
||||||
|
letting the shader's NDC mapping stretch it back onto the real
|
||||||
|
framebuffer) rasterised each glyph at the small, pre-stretch size
|
||||||
|
and then displayed it stretched onto more physical pixels than it
|
||||||
|
had texels for. **Fix, and the density-independent length unit
|
||||||
|
Iris asked for the same day (IRIS_TODO.md) turned out to be the
|
||||||
|
same fix**: `Len::dp`, resolved against a `density` now carried on
|
||||||
|
`UiRenderState`/`Painter`, replaces the global stretch -- window
|
||||||
|
size, touch and insets are physical pixels throughout again
|
||||||
|
(`WindowInsets`, renamed from `LogicalInsets`), and
|
||||||
|
`TextBuffer::shape` multiplies `font_size`/`line_height` by density
|
||||||
|
before handing them to parley, so the atlas rasterises at the
|
||||||
|
display's real physical resolution. Full design in docs/LAYOUT.md's
|
||||||
|
"Density: `Len::dp`" section and the public-API summary in
|
||||||
|
docs/IRIS.md's 2026-09-06 entry.
|
||||||
|
|
||||||
|
**3. The header.** Only each button's own `rect(...)` painted
|
||||||
|
anything, so the gaps between/around them and the status-bar strip
|
||||||
|
above showed `CLEAR_COLOR` (black) one layer back, and the row's
|
||||||
|
reserved height was three `abs` (now-physical-pixel) button boxes
|
||||||
|
-- smaller than the dp-correct size the transcript below uses,
|
||||||
|
which is what read as "overlap" once the two disagreed. Fixed with
|
||||||
|
a `HEADER_SURFACE` rect stacked behind the whole row and every
|
||||||
|
header size moved onto `dp(...)`.
|
||||||
|
|
||||||
|
**4. Keyboard diagnostics, so Iris can report back even if a
|
||||||
|
keyboard-triggered regression persists.** `on_insets_changed` now
|
||||||
|
edge-triggers ~500ms after `ime_bottom` becomes non-zero, capturing
|
||||||
|
the same report the on-screen Diagnostics button produces, logging
|
||||||
|
it, copying it to the clipboard unprompted, and showing it in a new
|
||||||
|
plain-view overlay (`IrisView.showDiagnosticsOverlay`, Copy/Close)
|
||||||
|
that draws independently of iris's own renderer.
|
||||||
|
|
||||||
|
**Verified this pass**: `cargo fmt --all`, `cargo clippy --workspace
|
||||||
|
--all-targets` and `cargo clippy` on `android-app` (both `-D
|
||||||
|
warnings`, zero beyond the pre-existing `tabs-ui` unused-dependency
|
||||||
|
and wgpu future-incompat notices), `cargo test --workspace` (all
|
||||||
|
passing), `cargo ndk -t arm64-v8a check`/`clippy` for both the
|
||||||
|
`transcript-screen bench` feature set.
|
||||||
|
|
||||||
|
**Then run on this checkout's own emulator** (x86_64 debug,
|
||||||
|
`--features "transcript-screen force-gles bench"` -- this AVD has no
|
||||||
|
Vulkan adapter under a plain `-gpu host` boot, matching every prior
|
||||||
|
emulator finding in this file): `run-bench.sh` end to end, no crash,
|
||||||
|
`frames=534 janky%=79.03 ... cpu_p50=1.3ms`, 24/24 swipes, 400/400
|
||||||
|
streamed events -- unchanged in shape from prior readings, so the
|
||||||
|
diff cost nothing on the success path. **Header background**:
|
||||||
|
screenshot confirms the `HEADER_SURFACE` panel now sits behind all
|
||||||
|
three buttons (`/tmp/bench-after-run.png` this pass). **Keyboard
|
||||||
|
wipe**: forced a real `surface_changed` two ways -- `adb shell wm
|
||||||
|
size 1080x1900` (screenshot before/after, text intact) and actually
|
||||||
|
opening the soft keyboard via `settings put secure
|
||||||
|
show_ime_with_hard_keyboard 1` + tapping the message field
|
||||||
|
(ui-trace confirmed a real resize, elements moved -547px; keyboard
|
||||||
|
visible in the screenshot, text still fully rendered, not wiped).
|
||||||
|
Both are real evidence the reuse-renderer fix works, though neither
|
||||||
|
is the literal before/after diff this box originally asked for --
|
||||||
|
**still worth a deliberate side-by-side screenshot pair in a future
|
||||||
|
pass.**
|
||||||
|
|
||||||
|
**Found during this same verification, not fixed, needs a follow-up
|
||||||
|
pass**: after the keyboard-triggered resize, the top button row
|
||||||
|
appeared to render a **second time**, well below its real position,
|
||||||
|
inside the transcript's scroll area (same colours/text, unmistakably
|
||||||
|
the same three buttons) -- and a tap aimed at the composer's
|
||||||
|
"Message" field landed on "Run benchmark" instead (a second
|
||||||
|
benchmark run started, visible in logcat as two `iris bench report:`
|
||||||
|
lines from one session). Only seen after a resize with the keyboard
|
||||||
|
genuinely open; the plain `wm size` resize screenshot pair did not
|
||||||
|
show it, nor did the fresh-install screenshot before either resize.
|
||||||
|
**Not root-caused this pass** -- time ran out before isolating
|
||||||
|
whether this is the `Span::DOWN` two-phase draw (LAYOUT.md's
|
||||||
|
provisional-then-real placement) leaving a phase-1 primitive
|
||||||
|
retained somewhere it should have been moved from, something
|
||||||
|
specific to the keyboard's `on_insets_changed` rebuild racing a
|
||||||
|
redraw, or unrelated to this pass's changes entirely (not verified
|
||||||
|
against a build predating this session's commits, so do not treat
|
||||||
|
"caused by this pass" as established -- MACHINE.md's pinned rule
|
||||||
|
about not attributing without measuring applies here too). Also
|
||||||
|
noteworthy: `capture_keyboard_diagnostics` never fired in this
|
||||||
|
session (no "iris keyboard diagnostics" log line) despite the
|
||||||
|
keyboard visibly opening -- `on_insets_changed`'s `ime_bottom` may
|
||||||
|
not be populated the way expected on this emulator/API level, or
|
||||||
|
the duplicate-row state above interfered; **also needs a follow-up
|
||||||
|
pass** before relying on the auto-capture on a real phone.
|
||||||
|
|
||||||
|
**Not verified this pass**: anything on Iris's real phone, the
|
||||||
|
two-density crispness check IRIS_TODO.md's unit item asks for, and
|
||||||
|
the two open items just above.
|
||||||
|
|
||||||
**Fling and jitter, 2026-09-06.** The two `IRIS_TODO.md` "From the
|
**Fling and jitter, 2026-09-06.** The two `IRIS_TODO.md` "From the
|
||||||
phone" items this box's own text names as follow-ups are fixed --
|
phone" items this box's own text names as follow-ups are fixed --
|
||||||
`List::fling`/`VelocityTracker`/`FlingCalculator` (IRIS.md's
|
`List::fling`/`VelocityTracker`/`FlingCalculator` (IRIS.md's
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package dev.iris.android.demo;
|
package dev.iris.android.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@@ -68,4 +75,81 @@ public final class IrisView extends RustView {
|
|||||||
scroll.addView(text);
|
scroll.addView(text);
|
||||||
activity.setContentView(scroll);
|
activity.setContentView(scroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String DIAGNOSTICS_OVERLAY_TAG = "iris-diagnostics-overlay";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bench build's keyboard diagnostics capture
|
||||||
|
* (`bench_client.rs`'s `on_insets_changed` /
|
||||||
|
* `capture_keyboard_diagnostics`, via `bench_jni.rs`'s
|
||||||
|
* `PlatformHandle::show_diagnostics_overlay`): unlike
|
||||||
|
* `showRendererError` above, this adds a panel *over* this view
|
||||||
|
* (`MainActivity`'s `FrameLayout` still holds `IrisView` underneath,
|
||||||
|
* running) rather than replacing the activity's content, and gives it
|
||||||
|
* a Copy button and a Close that removes the panel -- so it draws
|
||||||
|
* (and can be read) whether or not iris itself is still putting
|
||||||
|
* anything on screen, without abandoning the session that produced
|
||||||
|
* it. Runs on the UI thread regardless of which thread calls it,
|
||||||
|
* since the call comes from a background task (a delayed capture
|
||||||
|
* after the keyboard opens), and touching the view tree off the UI
|
||||||
|
* thread is undefined.
|
||||||
|
*/
|
||||||
|
void showDiagnosticsOverlay(String report) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (!(context instanceof Activity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Activity activity = (Activity) context;
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
ViewGroup parent = (ViewGroup) getParent();
|
||||||
|
if (parent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
View existing = parent.findViewWithTag(DIAGNOSTICS_OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
parent.removeView(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
float density = activity.getResources().getDisplayMetrics().density;
|
||||||
|
int pad = (int) (16 * density);
|
||||||
|
|
||||||
|
LinearLayout overlay = new LinearLayout(activity);
|
||||||
|
overlay.setTag(DIAGNOSTICS_OVERLAY_TAG);
|
||||||
|
overlay.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
overlay.setBackgroundColor(0xEE000000);
|
||||||
|
overlay.setPadding(pad, pad, pad, pad);
|
||||||
|
|
||||||
|
TextView text = new TextView(activity);
|
||||||
|
text.setText(report);
|
||||||
|
text.setTextIsSelectable(true);
|
||||||
|
text.setTextColor(0xFFFFFFFF);
|
||||||
|
ScrollView scroll = new ScrollView(activity);
|
||||||
|
scroll.addView(text);
|
||||||
|
overlay.addView(scroll, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
|
||||||
|
LinearLayout buttonRow = new LinearLayout(activity);
|
||||||
|
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
buttonRow.setPadding(0, pad, 0, 0);
|
||||||
|
|
||||||
|
Button copy = new Button(activity);
|
||||||
|
copy.setText("Copy");
|
||||||
|
copy.setOnClickListener(v -> {
|
||||||
|
ClipboardManager clipboard =
|
||||||
|
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
if (clipboard != null) {
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("iris diagnostics", report));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button close = new Button(activity);
|
||||||
|
close.setText("Close");
|
||||||
|
close.setOnClickListener(v -> parent.removeView(overlay));
|
||||||
|
buttonRow.addView(copy);
|
||||||
|
buttonRow.addView(close);
|
||||||
|
overlay.addView(buttonRow);
|
||||||
|
|
||||||
|
parent.addView(overlay, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -114,10 +114,17 @@ pub struct BenchClient {
|
|||||||
running: bool,
|
running: bool,
|
||||||
/// The keyboard phase's own confirmation channel -- updated from
|
/// The keyboard phase's own confirmation channel -- updated from
|
||||||
/// `on_insets_changed` (the platform's own answer for whether the IME
|
/// `on_insets_changed` (the platform's own answer for whether the IME
|
||||||
/// is actually visible, per `LogicalInsets::ime_bottom`), read from the
|
/// is actually visible, per `WindowInsets::ime_bottom`), read from the
|
||||||
/// benchmark's spawned task via the shared `Arc<Mutex<_>>` rather than
|
/// benchmark's spawned task via the shared `Arc<Mutex<_>>` rather than
|
||||||
/// `ctx.update`, since neither side needs the widget tree for this.
|
/// `ctx.update`, since neither side needs the widget tree for this.
|
||||||
ime_state: Arc<Mutex<ImeState>>,
|
ime_state: Arc<Mutex<ImeState>>,
|
||||||
|
/// Edge-triggers the keyboard diagnostics capture below -- set on the
|
||||||
|
/// first `on_insets_changed` where `ime_bottom > 0.0`, cleared on the
|
||||||
|
/// first where it is not, so opening the keyboard fires this once
|
||||||
|
/// rather than on every insets update while it stays open (a rotation
|
||||||
|
/// or a status-bar change with the keyboard already up would otherwise
|
||||||
|
/// re-fire it).
|
||||||
|
keyboard_was_visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See `BenchClient::ime_state`'s doc. `shown_events`/`hidden_events`
|
/// See `BenchClient::ime_state`'s doc. `shown_events`/`hidden_events`
|
||||||
@@ -241,7 +248,7 @@ impl AndroidAppState for BenchClient {
|
|||||||
let tree = (
|
let tree = (
|
||||||
top_bar,
|
top_bar,
|
||||||
content.height(rest(2)),
|
content.height(rest(2)),
|
||||||
report_display.height(rest(1)).pad(8),
|
report_display.height(rest(1)).pad(dp(8)),
|
||||||
)
|
)
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
@@ -277,6 +284,7 @@ impl AndroidAppState for BenchClient {
|
|||||||
last_report: None,
|
last_report: None,
|
||||||
running: false,
|
running: false,
|
||||||
ime_state: Arc::new(Mutex::new(ImeState::default())),
|
ime_state: Arc::new(Mutex::new(ImeState::default())),
|
||||||
|
keyboard_was_visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (backlog, stream_tail) = parse_fixture();
|
let (backlog, stream_tail) = parse_fixture();
|
||||||
@@ -305,38 +313,93 @@ impl AndroidAppState for BenchClient {
|
|||||||
/// field comment. Rebuilds the row rather than mutating a stored
|
/// field comment. Rebuilds the row rather than mutating a stored
|
||||||
/// `Padding` in place, since nothing here holds a handle to one.
|
/// `Padding` in place, since nothing here holds a handle to one.
|
||||||
///
|
///
|
||||||
/// Also the keyboard phase's own confirmation signal: `ime_bottom`
|
/// Also two things downstream of the same `ime_bottom` transition:
|
||||||
/// crossing 0<->positive is the platform's own answer for whether the
|
/// **the keyboard phase's own confirmation signal** (`ime_state`'s
|
||||||
/// IME actually opened or closed, fired from the real insets
|
/// doc -- the platform's own answer for whether the IME actually
|
||||||
/// callback rather than assumed from having called `show_ime`/
|
/// opened or closed, rather than assumed from having called
|
||||||
/// `hide_ime` -- see `ime_state`'s doc.
|
/// `show_ime`/`hide_ime`), and **the trigger for the keyboard
|
||||||
|
/// diagnostics capture** (RUST.md's P0 box): the IME resizing the
|
||||||
|
/// surface is exactly the case a previous commit found wiped text,
|
||||||
|
/// and Iris needs a way to get a report off the phone even if that
|
||||||
|
/// (or some other keyboard-triggered regression) is still happening
|
||||||
|
/// on the build she is holding -- `capture_keyboard_diagnostics`
|
||||||
|
/// below fires ~500ms after the keyboard becomes visible, once per
|
||||||
|
/// keyboard opening, and shows its report in a plain overlay view
|
||||||
|
/// that draws independently of whatever iris itself is doing.
|
||||||
fn on_insets_changed(
|
fn on_insets_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
rsc: &mut AndroidRsc<Self>,
|
rsc: &mut AndroidRsc<Self>,
|
||||||
insets: iris::android::LogicalInsets,
|
insets: iris::android::WindowInsets,
|
||||||
) {
|
) {
|
||||||
let controls = bench_controls(rsc, insets.top);
|
let controls = bench_controls(rsc, insets.top);
|
||||||
(self.top_bar)(rsc).set(controls);
|
(self.top_bar)(rsc).set(controls);
|
||||||
|
|
||||||
|
let ime_visible = insets.ime_bottom > 0.0;
|
||||||
|
|
||||||
let mut ime = self.ime_state.lock().unwrap();
|
let mut ime = self.ime_state.lock().unwrap();
|
||||||
let now_visible = insets.ime_bottom > 0.0;
|
if ime_visible && !ime.visible {
|
||||||
if now_visible && !ime.visible {
|
|
||||||
ime.shown_events += 1;
|
ime.shown_events += 1;
|
||||||
}
|
}
|
||||||
if !now_visible && ime.visible {
|
if !ime_visible && ime.visible {
|
||||||
ime.hidden_events += 1;
|
ime.hidden_events += 1;
|
||||||
}
|
}
|
||||||
ime.visible = now_visible;
|
ime.visible = ime_visible;
|
||||||
|
drop(ime);
|
||||||
|
|
||||||
|
if ime_visible && !self.keyboard_was_visible {
|
||||||
|
self.keyboard_was_visible = true;
|
||||||
|
let redraw = rsc.tasks.redraw_handle();
|
||||||
|
rsc.spawn_task(async move |mut ctx| {
|
||||||
|
tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await;
|
||||||
|
ctx.update(|state: &mut BenchClient, rsc| {
|
||||||
|
state.capture_keyboard_diagnostics(rsc);
|
||||||
|
});
|
||||||
|
redraw.request_redraw();
|
||||||
|
});
|
||||||
|
} else if !ime_visible {
|
||||||
|
self.keyboard_was_visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long to wait after the keyboard becomes visible before capturing
|
||||||
|
/// diagnostics -- long enough that the resize, the reported wipe (if it is
|
||||||
|
/// still happening) and a couple of frames have all had time to land, per
|
||||||
|
/// AGENTS.md's "so that operations that finish in milliseconds have states
|
||||||
|
/// on the way that nothing can observe" reasoning applied the other way:
|
||||||
|
/// this wants to observe the state *after* the transition settles, not
|
||||||
|
/// mid-flight.
|
||||||
|
const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500;
|
||||||
|
|
||||||
type Rsc = AndroidRsc<BenchClient>;
|
type Rsc = AndroidRsc<BenchClient>;
|
||||||
|
|
||||||
/// `top_pad` is the status-bar inset in logical units (0.0 until
|
/// The header row's own backdrop -- see `bench_controls`'s doc comment on
|
||||||
|
/// why it needs one at all. A dark neutral rather than pure black
|
||||||
|
/// (`android::render::CLEAR_COLOR`) so the row reads as a distinct panel
|
||||||
|
/// instead of a hole in the background the buttons happen to float in.
|
||||||
|
const HEADER_SURFACE: UiColor = UiColor::new(28, 28, 34, 255);
|
||||||
|
|
||||||
|
/// `top_pad` is the status-bar inset in physical pixels (0.0 until
|
||||||
/// `on_insets_changed` has run once) -- folded in here, rather than
|
/// `on_insets_changed` has run once) -- folded in here, rather than
|
||||||
/// exposing the unadded builder for a caller to `.pad()` itself, because
|
/// exposing the unadded builder for a caller to `.pad()` itself, because
|
||||||
/// naming that builder's type at each call site is more machinery than a
|
/// naming that builder's type at each call site is more machinery than a
|
||||||
/// top-of-screen padding number is worth.
|
/// top-of-screen padding number is worth.
|
||||||
|
///
|
||||||
|
/// **Backed by an opaque rect the full size of the row, not just the three
|
||||||
|
/// buttons.** Iris's phone report (docs/RUST.md's P0 box, screenshots on
|
||||||
|
/// build a9232ac): "the header buttons have nothing behind them and
|
||||||
|
/// overlap the transcript text" -- before this, only each button's own
|
||||||
|
/// `rect(...)` painted anything, so the gaps between and around them (and
|
||||||
|
/// the status-bar strip above them) showed whatever was one layer back
|
||||||
|
/// (`CLEAR_COLOR`, black), and the row's true height was three
|
||||||
|
/// physical-pixel-sized (`abs`, not `dp`) button boxes rather than the
|
||||||
|
/// density-correct size the transcript below was already using post-P0 --
|
||||||
|
/// exactly what reads as "overlap" once the two disagree. Fixed two ways
|
||||||
|
/// together: a `HEADER_SURFACE` rect stacked behind the whole row (this
|
||||||
|
/// function), and every size below moved from a bare number (physical
|
||||||
|
/// pixels) to `dp(...)` (IRIS_TODO.md's density-independent length unit),
|
||||||
|
/// so the row's reserved height in the outer `Span::DOWN`
|
||||||
|
/// (`AndroidAppState::new`) matches what is actually painted.
|
||||||
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
||||||
let run_rect = rect(Color::rgb(40, 70, 40))
|
let run_rect = rect(Color::rgb(40, 70, 40))
|
||||||
.on(
|
.on(
|
||||||
@@ -351,7 +414,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
wtext("Run benchmark").size(18).text_align(Align::CENTER),
|
wtext("Run benchmark").size(18).text_align(Align::CENTER),
|
||||||
)
|
)
|
||||||
.stack()
|
.stack()
|
||||||
.pad(8)
|
.pad(dp(8))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let copy_rect = rect(Color::rgb(50, 50, 60))
|
let copy_rect = rect(Color::rgb(50, 50, 60))
|
||||||
@@ -367,7 +430,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
wtext("Copy report").size(18).text_align(Align::CENTER),
|
wtext("Copy report").size(18).text_align(Align::CENTER),
|
||||||
)
|
)
|
||||||
.stack()
|
.stack()
|
||||||
.pad(8)
|
.pad(dp(8))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let diag_rect = rect(Color::rgb(60, 45, 70))
|
let diag_rect = rect(Color::rgb(60, 45, 70))
|
||||||
@@ -383,12 +446,14 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
wtext("Diagnostics").size(18).text_align(Align::CENTER),
|
wtext("Diagnostics").size(18).text_align(Align::CENTER),
|
||||||
)
|
)
|
||||||
.stack()
|
.stack()
|
||||||
.pad(8)
|
.pad(dp(8))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
(run, copy, diagnostics)
|
let buttons = (run, copy, diagnostics).span(Dir::RIGHT).add(rsc);
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.height(56)
|
(rect(HEADER_SURFACE), buttons)
|
||||||
|
.stack()
|
||||||
|
.height(dp(56))
|
||||||
.pad(Padding::top(top_pad))
|
.pad(Padding::top(top_pad))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any()
|
.any()
|
||||||
@@ -428,6 +493,36 @@ impl BenchClient {
|
|||||||
self.last_report = Some(report);
|
self.last_report = Some(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The keyboard's own diagnostics capture -- see `on_insets_changed`'s
|
||||||
|
/// doc comment. Reuses `show_diagnostics`'s exact report (so it is the
|
||||||
|
/// same text the on-screen `Diagnostics` button produces, plus the
|
||||||
|
/// per-frame log `FrameReport` already keeps around the resize --
|
||||||
|
/// `frame_report.report()` above covers "the frames around the
|
||||||
|
/// resize" without a second accounting mechanism), then does three
|
||||||
|
/// things the button does not: logs it (so a `logcat` pull gets it
|
||||||
|
/// even if nothing on screen does), copies it to the clipboard
|
||||||
|
/// unprompted, and shows it in the shell's plain overlay view, which
|
||||||
|
/// draws independently of iris's own renderer -- the whole point,
|
||||||
|
/// since the renderer is exactly what might be in the wiped state
|
||||||
|
/// this exists to report on.
|
||||||
|
fn capture_keyboard_diagnostics(&mut self, rsc: &mut Rsc) {
|
||||||
|
self.show_diagnostics(rsc);
|
||||||
|
let Some(report) = self.last_report.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log::info!("iris keyboard diagnostics:\n{report}");
|
||||||
|
let Some(platform) = &self.platform else {
|
||||||
|
log::info!("iris keyboard diagnostics: no platform handle, can't reach the shell");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if platform.copy_to_clipboard("iris keyboard diagnostics", &report) {
|
||||||
|
log::info!("iris keyboard diagnostics: copied to clipboard");
|
||||||
|
} else {
|
||||||
|
log::info!("iris keyboard diagnostics: clipboard copy failed");
|
||||||
|
}
|
||||||
|
platform.show_diagnostics_overlay(&report);
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_report(&mut self) {
|
fn copy_report(&mut self) {
|
||||||
let Some(report) = &self.last_report else {
|
let Some(report) = &self.last_report else {
|
||||||
log::info!("iris bench report: nothing to copy -- run the benchmark first");
|
log::info!("iris bench report: nothing to copy -- run the benchmark first");
|
||||||
|
|||||||
@@ -222,4 +222,31 @@ impl PlatformHandle {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows `report` in the shell's plain-view diagnostics overlay
|
||||||
|
/// (`IrisView.showDiagnosticsOverlay`) -- a real `TextView` plus Copy
|
||||||
|
/// and Close controls, added over whatever iris itself is drawing
|
||||||
|
/// rather than replacing it (unlike `android::view::show_renderer_error`,
|
||||||
|
/// which exists for the case the renderer can never recover from and
|
||||||
|
/// intentionally never returns). Called from a background task after
|
||||||
|
/// the keyboard-open delay (`bench_client.rs`'s `on_insets_changed`),
|
||||||
|
/// so the Java side hops onto the UI thread itself before touching the
|
||||||
|
/// view tree -- see that method's own comment.
|
||||||
|
pub fn show_diagnostics_overlay(&self, report: &str) -> bool {
|
||||||
|
self.try_show_diagnostics_overlay(report).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_show_diagnostics_overlay(&self, report: &str) -> Option<()> {
|
||||||
|
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||||
|
let env: &mut JNIEnv = &mut guard;
|
||||||
|
let jreport = env.new_string(report).ok()?;
|
||||||
|
env.call_method(
|
||||||
|
self.view.as_obj(),
|
||||||
|
"showDiagnosticsOverlay",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::Object(jreport.as_ref())],
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ mod view;
|
|||||||
pub use insets::Insets;
|
pub use insets::Insets;
|
||||||
pub use render::AndroidRenderer;
|
pub use render::AndroidRenderer;
|
||||||
pub use view::{
|
pub use view::{
|
||||||
AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState, IrisViewPeer, LogicalInsets,
|
AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets,
|
||||||
new_peer,
|
new_peer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+21
-19
@@ -208,14 +208,14 @@ impl AndroidRenderer {
|
|||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
let encoder = Self::create_encoder(&device);
|
let encoder = Self::create_encoder(&device);
|
||||||
// Logical size (physical / `content_scale`) -- see
|
// Physical pixels, matching the swapchain's own `width`/`height`
|
||||||
// `android::view::AndroidUiState::content_scale`'s field comment
|
// exactly -- see `android::view::AndroidUiState::content_scale`'s
|
||||||
// for why this crate now divides at all (RUST.md's P0 box, "text
|
// field comment for why this is no longer divided into a separate
|
||||||
// is far too small"). The swapchain above stays at the real
|
// logical space (that stopgap is what made text blurry, RUST.md's
|
||||||
// physical `width`/`height` for a sharp framebuffer.
|
// P0 box). `Len::dp` folds the density in at layout time instead,
|
||||||
let logical_size =
|
// so nothing here needs to know it at all.
|
||||||
iris_core::util::Vec2::new(width as f32 / content_scale, height as f32 / content_scale);
|
let window_size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||||
let ui = match UiRenderNode::new(&device, &queue, &config, logical_size) {
|
let ui = match UiRenderNode::new(&device, &queue, &config, window_size) {
|
||||||
Ok(ui) => ui,
|
Ok(ui) => ui,
|
||||||
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
||||||
};
|
};
|
||||||
@@ -398,25 +398,27 @@ impl AndroidRenderer {
|
|||||||
submit_start.elapsed()
|
submit_start.elapsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logical size (physical / `content_scale`) -- the unit layout and
|
/// Physical pixels -- the unit layout and hit-testing use, matching
|
||||||
/// hit-testing use, matching the window uniform's own units. See
|
/// the window uniform's own units. See
|
||||||
/// `android::view::AndroidUiState::content_scale`'s field comment.
|
/// `android::view::AndroidUiState::content_scale`'s field comment.
|
||||||
pub fn size(&self) -> iris_core::util::Vec2 {
|
pub fn size(&self) -> iris_core::util::Vec2 {
|
||||||
iris_core::util::Vec2::new(
|
iris_core::util::Vec2::new(self.config.width as f32, self.config.height as f32)
|
||||||
self.config.width as f32 / self.content_scale,
|
|
||||||
self.config.height as f32 / self.content_scale,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconfigures the surface and rewrites the window uniform for a new
|
||||||
|
/// physical size -- deliberately the *only* two things this does.
|
||||||
|
/// `device`, `ui`'s atlas, buffers and bind groups are untouched, so a
|
||||||
|
/// call here (as opposed to a fresh `AndroidRenderer::new`) never
|
||||||
|
/// invalidates a glyph the CPU-side cache already placed in the atlas.
|
||||||
|
/// See `android::view::IrisViewPeer::surface_changed`'s doc comment for
|
||||||
|
/// why that distinction matters -- it is what keeps text on screen
|
||||||
|
/// across an IME resize.
|
||||||
pub fn resize(&mut self, width: u32, height: u32) {
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
self.config.width = width;
|
self.config.width = width;
|
||||||
self.config.height = height;
|
self.config.height = height;
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
let logical = iris_core::util::Vec2::new(
|
let size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||||
width as f32 / self.content_scale,
|
self.ui.resize(size, &self.queue);
|
||||||
height as f32 / self.content_scale,
|
|
||||||
);
|
|
||||||
self.ui.resize(logical, &self.queue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+115
-68
@@ -70,19 +70,31 @@ pub struct AndroidUiState {
|
|||||||
/// because `dumpsys gfxinfo` cannot see a `SurfaceView`'s own
|
/// because `dumpsys gfxinfo` cannot see a `SurfaceView`'s own
|
||||||
/// GPU-drawn frames at all. See `iris_core::FrameReport`'s own doc.
|
/// GPU-drawn frames at all. See `iris_core::FrameReport`'s own doc.
|
||||||
pub frame_report: FrameReport,
|
pub frame_report: FrameReport,
|
||||||
/// `DisplayMetrics.density` (`new_peer`'s doc comment), read once at
|
/// `DisplayMetrics.density` (`new_peer`'s doc comment): physical pixels
|
||||||
/// view construction: physical pixels per dp on this device. Neither
|
/// per dp on this device, read once at view construction and carried
|
||||||
/// this crate nor `default::` had ever divided by it before RUST.md's
|
/// on `UiRenderState::density` (`render.set_density`, `new_peer`) from
|
||||||
/// P0 box's phone report ("text is far too small") -- `window_size`
|
/// then on -- every `Len::dp` in the widget tree resolves against it at
|
||||||
/// below and `surface_changed`'s call into `UiRenderState::resize` both
|
/// layout time (`Len::dp`'s field doc, IRIS_TODO.md's
|
||||||
/// report *logical* (physical / `content_scale`) dimensions now, which
|
/// "density-independent length unit" item, 2026-09-06).
|
||||||
/// is what makes a `font_size: 16.0` 16 dp rather than 16 raw device
|
///
|
||||||
/// pixels on a ~3x-density phone. The actual wgpu surface/swapchain
|
/// **Everything else in this module is physical pixels, matching the
|
||||||
/// stays at the real physical resolution (`AndroidRenderer`'s own
|
/// real wgpu surface/swapchain resolution** -- window size, touch
|
||||||
/// `config.width/height`) for a sharp framebuffer; only the *logical*
|
/// coordinates, insets. That is a correction from an earlier version
|
||||||
/// coordinate system layout, hit-testing and the window uniform agree
|
/// of this comment, which had `window_size`/`surface_changed`'s
|
||||||
/// on is scaled. Touch coordinates (`on_touch_event`) are divided by
|
/// `UiRenderState::resize` call divide by `content_scale` into a
|
||||||
/// this too, so they land in the same space layout is using.
|
/// *logical* coordinate space instead, as a global stopgap for
|
||||||
|
/// RUST.md's P0 box's phone report ("text is far too small"). That
|
||||||
|
/// stopgap fixed the size but not the *sharpness*: dividing to logical
|
||||||
|
/// units meant a `16.0`-sized glyph rasterised at 16 physical px and
|
||||||
|
/// then implicitly upscaled ~3x by the NDC mapping onto the real
|
||||||
|
/// physical framebuffer -- the exact "blurry ... glyphs drawn at
|
||||||
|
/// logical size and stretched by the scale" Iris reported next.
|
||||||
|
/// Resolving `dp` at layout time replaces it: a widget author writes
|
||||||
|
/// `dp(16)` for a size that should look the same physical size on any
|
||||||
|
/// density, and everything downstream (layout, hit-testing, the window
|
||||||
|
/// uniform, and the font size handed to the text shaper) works in the
|
||||||
|
/// display's own physical pixels throughout, so nothing is
|
||||||
|
/// rasterised at one resolution and displayed at another.
|
||||||
pub content_scale: f32,
|
pub content_scale: f32,
|
||||||
/// The last insets `render()` saw -- compared each frame so
|
/// The last insets `render()` saw -- compared each frame so
|
||||||
/// `AndroidAppState::on_insets_changed` fires only when they actually
|
/// `AndroidAppState::on_insets_changed` fires only when they actually
|
||||||
@@ -154,21 +166,26 @@ pub trait AndroidAppState: HasAndroidUiState {
|
|||||||
/// (RUST.md's P0 box: "the status-bar inset is not applied" reported
|
/// (RUST.md's P0 box: "the status-bar inset is not applied" reported
|
||||||
/// the two top buttons sitting under it, because nothing read `.top`
|
/// the two top buttons sitting under it, because nothing read `.top`
|
||||||
/// at all), and again on a rotation or the keyboard opening/closing.
|
/// at all), and again on a rotation or the keyboard opening/closing.
|
||||||
/// `insets` is in the same *logical* units `content_scale` converts
|
/// `insets` is in the same physical-pixel units everything else in the
|
||||||
/// everything else to (physical / `content_scale`), so a widget can add
|
/// tree now uses (`AndroidUiState::content_scale`'s field comment), so
|
||||||
/// it to a layout size directly. The default does nothing -- most
|
/// a widget can add it to a layout size directly -- `dp(...) +
|
||||||
/// screens have no chrome that sits under a system bar.
|
/// abs(insets.top)` if the widget wants a density-independent size
|
||||||
|
/// plus the system bar's own (already-physical) height. The default
|
||||||
|
/// does nothing -- most screens have no chrome that sits under a
|
||||||
|
/// system bar.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: LogicalInsets) {}
|
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `insets::Insets`, converted from physical to logical units -- see
|
/// `insets::Insets` as `f32`, for the widget-facing callback above -- a
|
||||||
/// `AndroidUiState::content_scale`'s field comment. A distinct type from
|
/// distinct type from `insets::Insets` so a caller of `on_insets_changed`
|
||||||
/// `insets::Insets` (rather than dividing in place) so a reader at the call
|
/// is not coupled to that module's own (`i32`, JNI-shaped) representation.
|
||||||
/// site can tell which unit a value is already in without checking where it
|
/// Both are physical pixels; this used to divide by `content_scale` into a
|
||||||
/// came from.
|
/// separate *logical* unit (hence the old name, `LogicalInsets`), back when
|
||||||
|
/// the rest of layout was logical too -- see `AndroidUiState::content_scale`'s
|
||||||
|
/// field comment for why that stopgap is gone.
|
||||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||||
pub struct LogicalInsets {
|
pub struct WindowInsets {
|
||||||
pub left: f32,
|
pub left: f32,
|
||||||
pub top: f32,
|
pub top: f32,
|
||||||
pub right: f32,
|
pub right: f32,
|
||||||
@@ -176,14 +193,14 @@ pub struct LogicalInsets {
|
|||||||
pub ime_bottom: f32,
|
pub ime_bottom: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogicalInsets {
|
impl WindowInsets {
|
||||||
fn from_physical(insets: Insets, content_scale: f32) -> Self {
|
fn from_physical(insets: Insets) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: insets.left as f32 / content_scale,
|
left: insets.left as f32,
|
||||||
top: insets.top as f32 / content_scale,
|
top: insets.top as f32,
|
||||||
right: insets.right as f32 / content_scale,
|
right: insets.right as f32,
|
||||||
bottom: insets.bottom as f32 / content_scale,
|
bottom: insets.bottom as f32,
|
||||||
ime_bottom: insets.ime_bottom as f32 / content_scale,
|
ime_bottom: insets.ime_bottom as f32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,10 +360,9 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
let ui_state = self.state.android_state();
|
let ui_state = self.state.android_state();
|
||||||
let current_insets = ui_state.insets();
|
let current_insets = ui_state.insets();
|
||||||
if current_insets != ui_state.last_insets {
|
if current_insets != ui_state.last_insets {
|
||||||
let content_scale = ui_state.content_scale;
|
let physical = WindowInsets::from_physical(current_insets);
|
||||||
let logical = LogicalInsets::from_physical(current_insets, content_scale);
|
|
||||||
self.state.android_state_mut().last_insets = current_insets;
|
self.state.android_state_mut().last_insets = current_insets;
|
||||||
self.state.on_insets_changed(&mut self.rsc, logical);
|
self.state.on_insets_changed(&mut self.rsc, physical);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ui_state = self.state.android_state();
|
let ui_state = self.state.android_state();
|
||||||
@@ -504,16 +520,10 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
self.drain_tasks();
|
self.drain_tasks();
|
||||||
let action = event.action_masked(&mut ctx.env);
|
let action = event.action_masked(&mut ctx.env);
|
||||||
// Device (physical) pixels, same as every other Android coordinate
|
// Device (physical) pixels, same space layout now uses throughout
|
||||||
// -- divided so a touch lands in the same *logical* space layout
|
// -- see `AndroidUiState::content_scale`'s field comment.
|
||||||
// now uses (`AndroidUiState::content_scale`'s field comment).
|
let x = event.x(&mut ctx.env);
|
||||||
// Without this, `window_size()` reporting logical dims while touch
|
let y = event.y(&mut ctx.env);
|
||||||
// stayed physical would land every tap off by exactly the density
|
|
||||||
// factor on any phone denser than 1x.
|
|
||||||
let ui_state = self.state.android_state();
|
|
||||||
let content_scale = ui_state.content_scale;
|
|
||||||
let x = event.x(&mut ctx.env) / content_scale;
|
|
||||||
let y = event.y(&mut ctx.env) / content_scale;
|
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
match action {
|
match action {
|
||||||
MotionAction::Down => {
|
MotionAction::Down => {
|
||||||
@@ -564,7 +574,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
height: i32,
|
height: i32,
|
||||||
) {
|
) {
|
||||||
self.drain_tasks();
|
self.drain_tasks();
|
||||||
let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
|
|
||||||
// The layout engine's own notion of the canvas size is separate
|
// The layout engine's own notion of the canvas size is separate
|
||||||
// from the wgpu surface's -- winit's backend sets it from
|
// from the wgpu surface's -- winit's backend sets it from
|
||||||
// `WindowEvent::Resized`, and there is no equivalent automatic
|
// `WindowEvent::Resized`, and there is no equivalent automatic
|
||||||
@@ -574,29 +583,55 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
// whatever size `UiRenderState::new` starts at instead of the
|
// whatever size `UiRenderState::new` starts at instead of the
|
||||||
// surface's real one.
|
// surface's real one.
|
||||||
//
|
//
|
||||||
// **Logical, not physical** -- `content_scale`'s field comment on
|
// **Physical pixels, matching `AndroidRenderer`'s own
|
||||||
// `AndroidUiState`. This call sets `UiRenderState::output_size`,
|
// `size()`/`resize()`/`new()`** -- `AndroidUiState::content_scale`'s
|
||||||
// which is what every widget's absolute `PixelRegion` (a fixed
|
// field comment. This call sets `UiRenderState::output_size`, which
|
||||||
// `.height(56)`, in particular) is computed against; `AndroidRenderer`'s
|
// every `rel`/`rest` length resolves against and every `abs`
|
||||||
// own `size()`/`resize()`/`new()` already report logical dimensions
|
// pixel-region compares to directly; a `dp(56)` height now folds
|
||||||
// to the *shader*'s window uniform, so leaving this call on raw
|
// in the density at `Len::apply_rest` time instead of this call
|
||||||
// physical `width`/`height` split the two into different units --
|
// dividing the whole window into a separate logical space, which
|
||||||
// layout placed a "56"-unit-tall row in an ~2219-tall physical
|
// is what used to make every `abs`-unit size (a fixed `.height(56)`
|
||||||
// canvas (an absolute, correctly-56-unit box), the shader then
|
// in particular) mean something different from a `rest`-based one.
|
||||||
// divided that same 56 by a ~845-unit *logical* window dimension,
|
self.render.resize((width as f32, height as f32));
|
||||||
// and the row rendered far too short rather than too tall or
|
|
||||||
// right, because a fixed-size item's absolute unit value never
|
// **Reuse the existing renderer (device, atlas, buffers, bind
|
||||||
// adapts to the mismatch the way a `rest(n)`-proportional one
|
// groups) when one is already live -- only reconfigure the
|
||||||
// does. Found by measuring a fresh install's top button row at
|
// surface.** `surfaceChanged` fires on *every* size or format
|
||||||
// ~40 physical px instead of the ~147px `56 * content_scale`
|
// change, not only on a genuinely new `Surface`/window: showing
|
||||||
// predicts, immediately after the density fix below was added.
|
// the IME under `adjustResize` resizes the same `SurfaceView` and
|
||||||
let content_scale = self.state.android_state().content_scale;
|
// is reported through this exact callback. Rebuilding the whole
|
||||||
self.render
|
// `AndroidRenderer` here used to mean a fresh `UiRenderNode::new`
|
||||||
.resize((width as f32 / content_scale, height as f32 / content_scale));
|
// -- a brand-new, empty glyph atlas and fresh GPU buffers -- while
|
||||||
// Drop the old renderer (and the surface it owns) before building
|
// `iris_core`'s CPU-side glyph cache (`primitive/text.rs`) kept the
|
||||||
// one from the new window -- see `AndroidRenderer`'s doc comment.
|
// atlas coordinates it had already handed out against the *old*
|
||||||
|
// atlas. Every glyph then drew from a UV rectangle that pointed
|
||||||
|
// into a texture that had just been recreated empty, so text
|
||||||
|
// vanished on the first keyboard open while rects (which never go
|
||||||
|
// through the atlas) kept drawing -- exactly the "rectangles stay,
|
||||||
|
// glyphs disappear" Iris reported. Confirmed by reading this path
|
||||||
|
// end to end (no fresh-atlas rebuild anywhere in `resize()` below,
|
||||||
|
// only in `AndroidRenderer::new`) before changing anything, per
|
||||||
|
// AGENTS.md's "verify before finishing".
|
||||||
|
//
|
||||||
|
// `AndroidRenderer::resize` only reconfigures the wgpu surface and
|
||||||
|
// rewrites the window uniform -- device, atlas, buffers and bind
|
||||||
|
// groups are untouched, so the glyph cache's coordinates stay
|
||||||
|
// valid. A genuinely new surface (after `surface_destroyed`, e.g.
|
||||||
|
// backgrounding) still goes through `AndroidRenderer::new` below,
|
||||||
|
// since `renderer` is `None` in that case.
|
||||||
|
let already_live = self.state.android_state().renderer.is_some();
|
||||||
|
if already_live {
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
ui_state.renderer = None;
|
ui_state
|
||||||
|
.renderer
|
||||||
|
.as_mut()
|
||||||
|
.expect("checked Some above")
|
||||||
|
.resize(width as u32, height as u32);
|
||||||
|
self.render(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
|
||||||
// `AndroidRenderer::new` used to panic here through wgpu's own
|
// `AndroidRenderer::new` used to panic here through wgpu's own
|
||||||
// default uncaptured-error handler on a bind-group-layout
|
// default uncaptured-error handler on a bind-group-layout
|
||||||
// validation failure -- exactly what aborted the P0 bench APK on
|
// validation failure -- exactly what aborted the P0 bench APK on
|
||||||
@@ -607,6 +642,11 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
// the one place in the app that can turn it into something a
|
// the one place in the app that can turn it into something a
|
||||||
// person can read, since `ctx.view`/`ctx.env` (needed to reach the
|
// person can read, since `ctx.view`/`ctx.env` (needed to reach the
|
||||||
// Java side) are only in scope inside a `ViewPeer` callback.
|
// Java side) are only in scope inside a `ViewPeer` callback.
|
||||||
|
//
|
||||||
|
// `content_scale` reaches `AndroidRenderer` only for the
|
||||||
|
// Diagnostics page's report text now -- window size and the
|
||||||
|
// shader's window uniform are physical pixels throughout (see the
|
||||||
|
// `resize` call above), not divided by it.
|
||||||
let content_scale = self.state.android_state().content_scale;
|
let content_scale = self.state.android_state().content_scale;
|
||||||
match AndroidRenderer::new(window, width as u32, height as u32, content_scale) {
|
match AndroidRenderer::new(window, width as u32, height as u32, content_scale) {
|
||||||
Ok(renderer) => {
|
Ok(renderer) => {
|
||||||
@@ -771,15 +811,22 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
|||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
_state: PhantomData,
|
_state: PhantomData,
|
||||||
};
|
};
|
||||||
|
// See `TextData::density`'s field doc for why this is set alongside
|
||||||
|
// `render.set_density` below rather than read from there.
|
||||||
|
rsc.ui.text.density = content_scale;
|
||||||
let shared = Rc::new(RefCell::new(Shared::default()));
|
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||||
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
||||||
let mut state = State::new(ui_state, &mut rsc);
|
let mut state = State::new(ui_state, &mut rsc);
|
||||||
let platform_vm = env.get_java_vm().unwrap();
|
let platform_vm = env.get_java_vm().unwrap();
|
||||||
let platform_view = env.new_global_ref(&view.0).unwrap();
|
let platform_view = env.new_global_ref(&view.0).unwrap();
|
||||||
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
// Every `Len::dp` in the tree resolves against this from now on -- see
|
||||||
|
// `UiRenderState::density`'s field doc and `Len::dp`'s.
|
||||||
|
render.set_density(content_scale);
|
||||||
let peer = IrisViewPeer {
|
let peer = IrisViewPeer {
|
||||||
rsc,
|
rsc,
|
||||||
render: UiRenderState::new(),
|
render,
|
||||||
state,
|
state,
|
||||||
task_recv,
|
task_recv,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -752,9 +752,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