Four fixes from Iris's phone report on the dc01f88 build, plus her same-day
follow-up on swipe-vs-tap:
- android/ime.rs: InputConnection now calls InputMethodManager.updateSelection
after every edit (new update_ime_selection, called from after_input) -- Gboard
was holding keystrokes back with nothing telling it the app's selection/
composing region had moved, which read as "doesn't enter it until I hit
space, doesn't move the caret". New unit tests in widget/text/edit.rs cover
the buffer-level composing/commit/delete/selection operations directly.
- attr.rs: Selector/Selectable rewritten around a shared on_press dispatcher
over PressStart/Pressing/PressEnd instead of click_or_drag(), so a field
that isn't already focused only grants focus (and requests the IME) on a
completed tap -- press and release with no frame past DRAG_SLOP. A drag
is never consumed, so whatever is behind the field still sees it. New
FocusHost::is_focused (both platform impls) and TextEdit::press_origin
back this. Verified on the emulator: dumpsys input_method's mInputShown
stays false after a swipe over the composer, true after a tap.
- iris_core: GlyphAtlas::clear()/Textures::reset(), called together from
android/view.rs's surface_changed exactly when a genuinely new renderer is
built (app-switch, not the keyboard-resize path that already reuses the
renderer) -- both CPU-side caches otherwise kept pointing at the old,
destroyed device's textures. Verified on the emulator: home, reopen, every
glyph still on screen.
- transcript-ui/composer.rs: rebuilt as one widget (unchanged Stack{rect,
span} idiom, capped at ~6 lines via MaxSize + .scrollable(), wrapped in one
Pad whose bottom Composer::set_bottom_inset rewrites in place so the bar
sits on the IME or nav-bar inset with no rebuild -- rebuilding would drop
focus/selection/in-progress text). Wired from bench_client.rs's existing
on_insets_changed.
A second, deeper bug found while verifying the composing fix is NOT fixed
this pass: composed text never becomes visible at all. A new layout_tests.rs
test proves the widget tree's own region math is correct across a keyboard
resize, ruling that out; RUST.md's P0 box has the full writeup and what to
check next (UiRenderState::redraw's single-widget path, or something
force-gles-specific -- this AVD has no Vulkan adapter to rule that out with).
cargo fmt/clippy/test --workspace and cargo ndk clippy all clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
4.9 KiB
Rust
103 lines
4.9 KiB
Rust
//! The message composer at the bottom of the transcript screen: a
|
|
//! multi-line editable field with a natural (not fixed) height, so it
|
|
//! grows as typed into -- IRIS_TODO.md's "input box" benchmark case
|
|
//! (`iris/benches/message_list.rs` exercises the mechanism in isolation;
|
|
//! this wires the same `TextEdit`-with-no-`Sized`-wrapper idiom into the
|
|
//! real screen). `lib.rs` gives the transcript `List` `.height(rest(1))`
|
|
//! beside this widget in a `Span::down`, so the list's own draw already
|
|
//! measures whatever vertical space is left each frame -- nothing here
|
|
//! computes a height by hand, and growing this field is exactly the
|
|
//! O(1)-move-chain case LAYOUT.md and I3's benchmark already measured.
|
|
//!
|
|
//! **Rebuilt 2026-09-06** (Iris's phone report on the dc01f88 build: the
|
|
//! grey bar drawn as a short, fixed strip with the typed text ~150px below
|
|
//! it on black, and empty black between the bar and the keyboard). One
|
|
//! widget now, top to bottom: an opaque background sized to its content
|
|
//! (`.background`, the same `Stack` idiom the header row's `HEADER_SURFACE`
|
|
//! already uses), the field inside `dp` padding and capped at
|
|
//! [`MAX_LINES`] before it scrolls instead of growing forever, and an
|
|
//! outer [`Pad`] whose `bottom` [`TranscriptScreen::set_bottom_inset`]
|
|
//! rewrites in place whenever the keyboard opens/closes -- never rebuilt,
|
|
//! since `field` is strongly owned inside this tree and this crate's
|
|
//! widgets cannot be re-parented once added (this module's own comment
|
|
//! below on why `build_composer` hands back a **weak** id).
|
|
|
|
use iris::prelude::*;
|
|
|
|
/// Caps the field's growth at roughly six lines of its own 18px text
|
|
/// before it scrolls instead of consuming the whole screen -- an
|
|
/// approximation (line-height and padding folded into one round `dp`
|
|
/// number) rather than a value derived from the font's real metrics,
|
|
/// which nothing in this crate exposes to a caller today.
|
|
const MAX_LINES: f32 = 6.0;
|
|
const APPROX_LINE_HEIGHT_DP: f32 = 24.0;
|
|
const FIELD_PAD_DP: f32 = 12.0;
|
|
|
|
/// `field` is exposed so the caller can read its content on submit
|
|
/// (`field.edit(rsc).text()`) and clear it afterward
|
|
/// (`field.edit(rsc).set("")`).
|
|
pub struct Composer {
|
|
pub field: WeakWidget<TextEdit>,
|
|
/// The bar's own outer padding -- only `bottom` is ever changed, by
|
|
/// [`Self::set_bottom_inset`]. A `Pad` around the whole bar rather than
|
|
/// a rebuilt tree, because `field` lives inside it and cannot be
|
|
/// re-added to a new wrapper once it is strongly owned here.
|
|
outer_pad: WeakWidget<Pad>,
|
|
}
|
|
|
|
impl Composer {
|
|
/// Called by the platform shell (Android's `on_insets_changed`, e.g.)
|
|
/// whenever the space below the bar changes: the IME's own inset while
|
|
/// it is open, the navigation-bar inset otherwise. Takes a plain
|
|
/// `f32` in the caller's own physical-pixel units rather than an
|
|
/// Android-specific insets type, so this crate stays usable from the
|
|
/// winit backend too, which has no navigation bar to report.
|
|
/// Rewrites the existing `Pad` in place (marking it dirty through the
|
|
/// ordinary `Widgets::get_mut` path) instead of swapping in a new one,
|
|
/// so the field's focus, selection and in-progress text are untouched.
|
|
pub fn set_bottom_inset(&self, rsc: &mut impl UiRsc, inset: f32) {
|
|
if let Some(pad) = rsc.ui_mut().widgets.get_mut(&self.outer_pad) {
|
|
pad.padding.bottom = Len::abs(inset);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the composer plus its own bar as a **weak** id -- the caller
|
|
/// (`lib.rs::build`) embeds it in the screen's own top-level tuple, whose
|
|
/// `set_root` performs the one real strong registration. Calling
|
|
/// `.add_strong`/`.upgrade` a second time on an id already strong-owned
|
|
/// panics ("was already added", `core/src/widget/like.rs:12`) -- the same
|
|
/// mistake this box's `row.rs` first made with its sender-label header, see
|
|
/// that file's comment for the fuller account.
|
|
pub fn build_composer<Rsc: HasEvents>(rsc: &mut Rsc) -> (Composer, WeakWidget)
|
|
where
|
|
Rsc::State: FocusHost,
|
|
{
|
|
let field = wtext("")
|
|
.editable(EditMode::MultiLine)
|
|
.text_align(Align::LEFT)
|
|
.wrap(true)
|
|
.size(18)
|
|
.color(UiColor::WHITE)
|
|
.attr::<Selectable>(())
|
|
.label("Message")
|
|
.add(rsc);
|
|
|
|
// One widget: an opaque bar sized to its own content (`.background`'s
|
|
// `Stack{child: 1}`, the header row's own idiom) wrapping the padded,
|
|
// height-capped field -- not a background rect and a field drawn as
|
|
// two independent siblings, which is what let the two disagree on
|
|
// where the bar actually was.
|
|
let content = field
|
|
.pad(dp(FIELD_PAD_DP))
|
|
.max_height(dp(APPROX_LINE_HEIGHT_DP * MAX_LINES + FIELD_PAD_DP * 2.0))
|
|
.scrollable()
|
|
.width(rest(1))
|
|
.background(rect(UiColor::new(40, 40, 46, 255)))
|
|
.add(rsc);
|
|
|
|
let outer_pad: WeakWidget<Pad> = content.pad(Padding::ZERO).add(rsc);
|
|
|
|
(Composer { field, outer_pad }, outer_pad)
|
|
}
|