editbox: draw the text by SetJustifyH (#2999)

An edit box always drew its text left-aligned: `SetJustifyH("RIGHT")` or `("CENTER")` moved only the `OnCursorChanged` x.

In 1.12.1 the box's `SetJustifyH` writes the box's own font (`0x797a17`, `+0x324`) through the shared setter `0x79fc20`, which pushes the change to the label linked to that font (`0x779c27`). `SetMultiLine` cuts off only the label's vertical bits (`0x77a509`, `0x77a599`), so the horizontal justify draws.

The box's font now exists with its label, justified `LEFT` as the constructor leaves it (`0x779bd2`). The four justify methods act on that font, the label no longer pins H, and the caret reads the label's justify. The separate justify word on the box is gone.

Test: `the_text_draws_by_the_boxs_justify_h_and_its_own_justify_v` checks the drawn H follows `SetJustifyH` while V stays the box's own. Without the fix, RIGHT draws LEFT.

Fixes #2977

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-11-06 22:09:19 +07:00
committed by GitHub
parent f9889a3c9d
commit e748780b9e
7 changed files with 119 additions and 55 deletions
+21 -6
View File
@@ -540,6 +540,19 @@ pub(super) fn ensure_text_region_in(model: &mut Model, h: FrameHandle) -> Option
slot.insert(data);
model.touch_layout_region(rh);
}
if !model.own_fonts.contains_key(&h) {
// The ctor's font (`0x779bc7`), justified `LEFT` with the group marked set and still
// following (`0x779bd2`-`0x779be7`), which the label links to (`0x779c27`).
let id = model.fonts.anonymous();
let mut block = model.fonts.get(id).block.clone();
block
.justify
.get_or_insert_with(Default::default)
.set_h(crate::script::JustifyH::Left);
super::font::replace_block(model, id, block);
model.own_fonts.insert(h, id);
super::font::link_region(model, rh, Some(id));
}
if let Some(KindState::EditBox(eb)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
eb.text_region = Some(rh);
}
@@ -552,21 +565,23 @@ pub(super) fn ensure_text_region_in(model: &mut Model, h: FrameHandle) -> Option
pub(super) fn refresh_text_region(lua: &Lua, this: &Table) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let Some(rh) = ensure_text_region_in(&mut model, h) else {
return Ok(());
};
let Some(KindState::EditBox(eb)) = model.arena.frame(h).map(|f| &f.kind_state) else {
return Ok(());
};
let (Some(rh), multi_line) = (eb.text_region, eb.multi_line) else {
return Ok(());
};
let multi_line = eb.multi_line;
apply_text_region_justify(model.region_data.entry(rh).or_default(), multi_line);
relayout::seat_text_region(&mut model, h);
Ok(())
}
/// Left-justified whatever the font string declares (the draw `0x77da80` anchors left at the
/// insets), from the top in a multiLine box and vertically centered otherwise.
/// The label's vertical justify, cut off from the box's font for good (`0x77a509`, `0x77a599`):
/// from the top in a multiLine box and centered otherwise (`0x77a517`, `0x77a5a7`). Its horizontal
/// justify still follows the font, so `SetJustifyH` draws.
fn apply_text_region_justify(data: &mut RegionData, multi_line: bool) {
data.justify.set_h(crate::script::JustifyH::Left);
data.justify_mask.0 &= !crate::justify::V_MASK;
data.justify.set_v(if multi_line {
crate::script::JustifyV::Top
} else {
+11 -4
View File
@@ -89,11 +89,18 @@ fn cursor_changed(lua: &Lua, h: FrameHandle) -> bool {
return false;
}
// Already in the box's own units, as the measure answers.
let Some(line_h) =
super::ensure_text_region(lua, h).and_then(|rh| relayout::line_height(lua, h, rh))
else {
let Some(rh) = super::ensure_text_region(lua, h) else {
return false;
};
let Some(line_h) = relayout::line_height(lua, h, rh) else {
return false;
};
let justify_h = super::super::region::justify_word(
&lua.app_data_ref::<crate::script::Model>()
.expect("model app_data"),
rh,
)
.0 & crate::justify::H_MASK;
let args = with_eb(lua, h, |eb| {
let display = eb.display();
if eb.advances.len() != display.len() + 1 {
@@ -122,7 +129,7 @@ fn cursor_changed(lua: &Lua, h: FrameHandle) -> bool {
} else {
a[cursor_d] - a[line_start]
};
let x = match eb.justify & crate::justify::H_MASK {
let x = match justify_h {
0x01 => along,
0x02 => along - (a[line_end] - a[line_start]) * 0.5,
0x04 if cursor_d == line_end => 0.0,
@@ -3,8 +3,11 @@
use mlua::{Lua, Table, Value};
use crate::justify;
use crate::script::binding_abi;
use crate::script::font_block::own_font;
use crate::script::object::frame_handle_of;
use crate::script::{FontMask, Model};
use crate::widget::EditBoxState;
use super::{
@@ -358,36 +361,50 @@ fn install_font_block(lua: &Lua, m: &Table) -> mlua::Result<()> {
}),
)?;
// The justify four act on the box's font (`[this+0x324]`, `0x797a17`) through the shared
// setter (`0x79fc20`), which pushes to the label: its H draws, its V is cut off (`0x77a4a0`).
// SetJustifyH/SetJustifyV return nothing. An unknown token raises `Usage:` (`0x87c77c`); a
// token of the other axis parses and masks to nothing, so `SetJustifyH("TOP")` clears justifyH
// and `GetJustifyH()` answers "UNKNOWN".
for (name, mask) in [
("SetJustifyH", EditBoxState::JUSTIFY_H_MASK),
("SetJustifyV", EditBoxState::JUSTIFY_V_MASK),
("SetJustifyH", justify::H_MASK),
("SetJustifyV", justify::V_MASK),
] {
m.set(
name,
lua.create_function(move |lua, (this, token): (Table, Value)| {
frame_handle_of(lua, &this)?;
let h = frame_handle_of(lua, &this)?;
let bits = binding_abi::optional_string(lua, &token)
.and_then(|t| EditBoxState::justify_bit(&t))
.and_then(|t| justify::parse_bits(&t))
.ok_or_else(|| {
binding_abi::usage(lua, &this, &format!("{name}(\"justify\")"))
})?;
with_editbox(lua, &this, |eb| eb.set_justify_axis(mask, bits))
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let id = own_font(&mut model, h)
.ok_or_else(|| mlua::Error::runtime("not an EditBox"))?;
crate::script::font::set(&mut model, id, FontMask::JUSTIFY, |fo| {
let word = fo.justify.get_or_insert_with(Default::default);
word.0 = justify::set_axis(word.0, mask, bits);
});
Ok(())
})?,
)?;
}
// GetJustifyH/GetJustifyV: one string, the first set bit's token in the reference's table
// order, else "UNKNOWN".
for (name, mask) in [
("GetJustifyH", EditBoxState::JUSTIFY_H_MASK),
("GetJustifyV", EditBoxState::JUSTIFY_V_MASK),
("GetJustifyH", justify::H_MASK),
("GetJustifyV", justify::V_MASK),
] {
m.set(
name,
lua.create_function(move |lua, this: Table| {
with_editbox(lua, &this, |eb| eb.justify_token(mask))
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let id = own_font(&mut model, h)
.ok_or_else(|| mlua::Error::runtime("not an EditBox"))?;
let word = model.fonts.get(id).block.justify.unwrap_or_default();
Ok(justify::name_of(word.0, mask))
})?,
)?;
}
@@ -1,7 +1,7 @@
//! The text region's rect (`0x77b8c0`, re-seated on every resize by `0x77a8d0`) and a multi-line
//! box's own height (`0x77d4d0` @`0x77d8ad`), driven through the Lua methods and the tick.
use crate::script::{MeasureRequest, QuadContent, TextMeasure, UiScript};
use crate::script::{JustifyH, JustifyV, MeasureRequest, QuadContent, TextMeasure, UiScript};
/// A stand-in font engine: each character [`CW`] wide and each line `.0` tall. Hard breaks split
/// lines, a trailing one opening none as in the host's line law, and each line wraps by
@@ -330,6 +330,56 @@ fn text_rect(s: &UiScript, text: &str) -> Option<(f32, f32, f32, f32)> {
})
}
/// The text quad's drawn justify as `(h, v)`.
fn text_justify(s: &UiScript, text: &str) -> Option<(JustifyH, JustifyV)> {
s.extract().into_iter().find_map(|q| match &q.content {
QuadContent::Text {
text: Some(t),
justify_h,
justify_v,
..
} if t == text => Some((*justify_h, *justify_v)),
_ => None,
})
}
/// The label follows the box's font horizontally (`0x779c27`, `0x797a17` → `0x79fc20`), so
/// `SetJustifyH` draws; its vertical justify is the box's own (`0x77a4a0`), whatever
/// `SetJustifyV` says.
#[test]
fn the_text_draws_by_the_boxs_justify_h_and_its_own_justify_v() {
let mut s = script();
s.run(
r#"
E = CreateFrame("EditBox", "E")
E:SetAutoFocus(false)
E:SetPoint("BOTTOMLEFT", 100, 50)
E:SetWidth(200); E:SetHeight(32)
E:SetFont("Fonts\\FRIZQT__.TTF", 14)
E:SetText("1")
"#,
)
.unwrap();
s.resolve();
assert_eq!(
text_justify(&s, "1"),
Some((JustifyH::Left, JustifyV::Middle))
);
for (h, want) in [("RIGHT", JustifyH::Right), ("CENTER", JustifyH::Center)] {
s.run(&format!(r#"E:SetJustifyH("{h}") E:SetJustifyV("BOTTOM")"#))
.unwrap();
s.resolve();
assert_eq!(text_justify(&s, "1"), Some((want, JustifyV::Middle)), "{h}");
}
s.run("E:SetMultiLine(true)").unwrap();
s.resolve();
assert_eq!(
text_justify(&s, "1"),
Some((JustifyH::Center, JustifyV::Top))
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A box built in Lua, with no `<FontString>` and no `SetTextInsets`, has its text seated at its
/// first resolve: `ApplyRect` (`0x76b580`) sees it resized from the ctor's zero rect and the
/// override `0x77a8d0` runs `0x77b8c0` (`0x77a8e7`).
+5 -1
View File
@@ -30,7 +30,11 @@ pub(crate) fn own_font(model: &mut Model, h: FrameHandle) -> Option<FontId> {
return Some(id);
}
let rh = match model.arena.frame(h)?.kind_state {
KindState::EditBox(_) => super::editbox::ensure_text_region_in(model, h)?,
// A box's font comes with its label.
KindState::EditBox(_) => {
super::editbox::ensure_text_region_in(model, h)?;
return model.own_fonts.get(&h).copied();
}
KindState::Message(_) | KindState::ScrollingMessage(_) => {
super::messageframe::ensure_font_region_in(model, h)?
}
@@ -140,18 +140,9 @@ pub struct EditBoxState {
/// `<HighlightColor>` through `SetSelectionColor` (`0x77e330`), which no Lua method reaches;
/// the ctor's is `0xFF606060` (`0x779c7a`).
pub highlight_color: crate::color::Rgba8,
/// The bits of `Set/GetJustifyH` and `Set/GetJustifyV` (`0x797990`-`0x797bd0`), kept on the
/// box and not drawn. For V that is the reference: `SetMultiLine` (`0x77a4a0`) makes the
/// text's V bits local, so a `SetJustifyV` is masked out (`0x77086e`) and only the getter sees
/// it. Whether the draw (`0x77da80`) reads H is untraced.
pub justify: u32,
}
impl EditBoxState {
/// The horizontal justify bits (0-2).
pub const JUSTIFY_H_MASK: u32 = crate::justify::H_MASK;
/// The vertical justify bits (3-5).
pub const JUSTIFY_V_MASK: u32 = crate::justify::V_MASK;
/// Dirty bit 0, the text changed: every edit raises it, and the flush relayouts (`0x77d447`)
/// and fires `OnTextChanged` (`0x77d498`).
pub const DIRTY_TEXT: u8 = 1;
@@ -160,23 +151,6 @@ impl EditBoxState {
/// leg (`0x77d475` → `0x77da80`), which fires `OnCursorChanged`. Bit 1, the
/// highlight's (`0x77d950`), has no counterpart: the host paints the selection each frame.
pub const DIRTY_CURSOR: u8 = 4;
/// A justify token's bit; `None` makes the caller raise the reference's
/// `Usage: %s:SetJustifyH("justify")`.
pub fn justify_bit(token: &str) -> Option<u32> {
crate::justify::parse_bits(token)
}
/// Replace one axis's bits with `parsed`'s. `SetJustifyH("TOP")` parses but clears the H bits,
/// so `GetJustifyH()` then answers `"UNKNOWN"`, with no error.
pub fn set_justify_axis(&mut self, mask: u32, parsed: u32) {
self.justify = crate::justify::set_axis(self.justify, mask, parsed);
}
/// The token for one axis.
pub fn justify_token(&self, mask: u32) -> &'static str {
crate::justify::name_of(self.justify, mask)
}
}
impl Default for EditBoxState {
@@ -219,9 +193,6 @@ impl Default for EditBoxState {
selection_regions: [None; 3],
caret_region: None,
highlight_color: crate::color::Rgba8::rgba(0x60, 0x60, 0x60, 0xff),
// The ctor's `0x211` less the unread bit 0x200: LEFT | MIDDLE, since the ctor
// overrides the font default's CENTER (`0x779be4`).
justify: 0x01 | 0x10,
}
}
}
+6 -6
View File
@@ -851,12 +851,12 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
#### widget
- The history browses as a shell's does because it is a small everyday gain: the first UP stashes the line being typed and DOWN past the newest line brings it back, UP holds at the oldest, DOWN does nothing when not browsing, typing or a focus gain ends the browse, and an empty line is not kept. The reference's history is a ring of `historyLines` slots that keeps any line (`0x77cf40`); UP scans back for the previous filled slot, wrapping (`0x77d030`), DOWN forward with `idiv` (`0x77cfd0`), each a `SetText` (`0x77be00`), with no draft and no stop at either end. ([`src/widget/kinds/editbox.rs:247`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L247))
- A plain Left or Right (or Ctrl+B or F) with a selection collapses it to that edge, because every OS's own text fields do; the reference clears the selection and steps once from the caret (`0x77c6b0` from `0x77c70b`). ([`src/widget/kinds/editbox.rs:818`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L818))
- Left reads the whole character before the caret, not the lone byte `caret - 1` the client reads (`0x77c8da`), because that takes a multi-byte space for a non-space. ([`src/widget/kinds/editbox.rs:837`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L837))
- On the last row a walk may end at the text's end, because the end starts no row; the reference steps back from it as from a row's start (its line table ends with the text's length, `0x77d6ef`), so DOWN onto a short last line stops before its last letter. ([`src/widget/kinds/editbox.rs:888`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L888))
- The drag selects from its anchor, because every OS's own text fields do, where the reference's applies `0x77cd10` to each mouse move's whole delta (`0x77a8a1`). The two part in two cases. One move that carries the caret across the anchor keeps the far end selected too in the reference. And a drag that starts with the caret inside a selection, as after a focus that selects all, gives `(0, t)` here dragging right to `t`, where the reference gives `(t, len)`. ([`src/widget/kinds/editbox.rs:1001`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L1001))
- A code point past U+FFFF is no space, because the client tests its low 16 bits, so U+10020 counts as a space there. ([`src/widget/kinds/editbox.rs:1131`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L1131))
- The history browses as a shell's does because it is a small everyday gain: the first UP stashes the line being typed and DOWN past the newest line brings it back, UP holds at the oldest, DOWN does nothing when not browsing, typing or a focus gain ends the browse, and an empty line is not kept. The reference's history is a ring of `historyLines` slots that keeps any line (`0x77cf40`); UP scans back for the previous filled slot, wrapping (`0x77d030`), DOWN forward with `idiv` (`0x77cfd0`), each a `SetText` (`0x77be00`), with no draft and no stop at either end. ([`src/widget/kinds/editbox.rs:218`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L218))
- A plain Left or Right (or Ctrl+B or F) with a selection collapses it to that edge, because every OS's own text fields do; the reference clears the selection and steps once from the caret (`0x77c6b0` from `0x77c70b`). ([`src/widget/kinds/editbox.rs:789`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L789))
- Left reads the whole character before the caret, not the lone byte `caret - 1` the client reads (`0x77c8da`), because that takes a multi-byte space for a non-space. ([`src/widget/kinds/editbox.rs:808`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L808))
- On the last row a walk may end at the text's end, because the end starts no row; the reference steps back from it as from a row's start (its line table ends with the text's length, `0x77d6ef`), so DOWN onto a short last line stops before its last letter. ([`src/widget/kinds/editbox.rs:859`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L859))
- The drag selects from its anchor, because every OS's own text fields do, where the reference's applies `0x77cd10` to each mouse move's whole delta (`0x77a8a1`). The two part in two cases. One move that carries the caret across the anchor keeps the far end selected too in the reference. And a drag that starts with the caret inside a selection, as after a focus that selects all, gives `(0, t)` here dragging right to `t`, where the reference gives `(t, len)`. ([`src/widget/kinds/editbox.rs:972`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L972))
- A code point past U+FFFF is no space, because the client tests its low 16 bits, so U+10020 counts as a space there. ([`src/widget/kinds/editbox.rs:1102`](../crates/benilla-ui/src/widget/kinds/editbox.rs#L1102))
- The cut backs up to a character start, where the reference cuts at the byte, because a Rust string holds whole characters. ([`src/widget/kinds/messageframe.rs:120`](../crates/benilla-ui/src/widget/kinds/messageframe.rs#L120))
- N of -2 or less is an empty line too, because the reference asks `SMemAlloc` for about 4 GB and fails. ([`src/widget/kinds/messageframe.rs:122`](../crates/benilla-ui/src/widget/kinds/messageframe.rs#L122))
- An index past 5 reads the last entry, because 1.12.1 reads past both six-entry tables (`0x6da9d5`, `0x6da9c4`), a client fault a hand-edited CVar reaches. ([`src/widget/kinds/mod.rs:973`](../crates/benilla-ui/src/widget/kinds/mod.rs#L973))