diff --git a/crates/benilla-ui/src/script/editbox.rs b/crates/benilla-ui/src/script/editbox.rs index f43166748..da201cb12 100644 --- a/crates/benilla-ui/src/script/editbox.rs +++ b/crates/benilla-ui/src/script/editbox.rs @@ -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::().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 { diff --git a/crates/benilla-ui/src/script/editbox/flush.rs b/crates/benilla-ui/src/script/editbox/flush.rs index 85ca3e9b1..80d042c0c 100644 --- a/crates/benilla-ui/src/script/editbox/flush.rs +++ b/crates/benilla-ui/src/script/editbox/flush.rs @@ -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::() + .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, diff --git a/crates/benilla-ui/src/script/editbox/methods.rs b/crates/benilla-ui/src/script/editbox/methods.rs index 07564045b..77aa54982 100644 --- a/crates/benilla-ui/src/script/editbox/methods.rs +++ b/crates/benilla-ui/src/script/editbox/methods.rs @@ -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::().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::().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)) })?, )?; } diff --git a/crates/benilla-ui/src/script/editbox/relayout_tests.rs b/crates/benilla-ui/src/script/editbox/relayout_tests.rs index 593abf8fc..b674ca1f9 100644 --- a/crates/benilla-ui/src/script/editbox/relayout_tests.rs +++ b/crates/benilla-ui/src/script/editbox/relayout_tests.rs @@ -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 `` 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`). diff --git a/crates/benilla-ui/src/script/font_block.rs b/crates/benilla-ui/src/script/font_block.rs index 8d550d116..b046d769f 100644 --- a/crates/benilla-ui/src/script/font_block.rs +++ b/crates/benilla-ui/src/script/font_block.rs @@ -30,7 +30,11 @@ pub(crate) fn own_font(model: &mut Model, h: FrameHandle) -> Option { 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)? } diff --git a/crates/benilla-ui/src/widget/kinds/editbox.rs b/crates/benilla-ui/src/widget/kinds/editbox.rs index c42c6fcb3..0177ea507 100644 --- a/crates/benilla-ui/src/widget/kinds/editbox.rs +++ b/crates/benilla-ui/src/widget/kinds/editbox.rs @@ -140,18 +140,9 @@ pub struct EditBoxState { /// `` 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 { - 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, } } } diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md index f969829c4..d565fdfa8 100644 --- a/docs/DEVIATIONS.md +++ b/docs/DEVIATIONS.md @@ -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))