Skip to main content

compose_rust/
measure.rs

1//! Asking the Renderer how big something will be, while the Host is laying it out.
2//!
3//! A Host that lays out a page itself (a flexbox, a grid, text flowing around a picture)
4//! needs to know how wide a run of text is before it can decide where anything goes, and
5//! only the side that draws the letters can say: the font, the density, the system's font
6//! scale and the design system's type scale are all the Renderer's. So the Host asks, in
7//! the middle of its own work, and the Renderer answers on the same call stack.
8//!
9//! One call carries many questions. A sidebar lays out a couple of hundred labels, and a
10//! flexbox asks each of them for its narrowest width, its widest width and its height at a
11//! given width, so one call per question would pay the crossing six hundred times.
12//!
13//! The questions travel the way a batch does: fixed-length records first, then the strings
14//! and run lists they point at. The answers come back as fixed-length records in an array
15//! the Host owns. Nothing is serialised and nothing is copied on the way across.
16//!
17//! Application code does not call the boundary. A layout engine asks [`measurer`] for a
18//! [`Measurer`], which it gets only while the Host is inside one of its own calls, which is
19//! the only time the Renderer is standing still on the other end of the same thread.
20
21use crate::fonts::{FONT_REF_LEN, FONT_REF_SYSTEM, FontRefs};
22use crate::protocol::ProtocolError;
23use crate::schema::{OverflowWrap, TypeRole, WordBreak};
24use crate::spans::{SPAN_FONT_ENTRY_LEN, SPAN_FONT_OFFSET_AT, SPAN_LEN, TextSpans};
25use std::marker::PhantomData;
26
27/// Bytes of one request record, whichever kind it is.
28pub const MEASURE_RECORD_LEN: usize = 72;
29/// Bytes of one result record.
30pub const MEASURE_RESULT_LEN: usize = 32;
31
32/// A run of text, its style and a width constraint.
33pub const MEASURE_TEXT: u16 = 1;
34/// A node the Renderer has already applied, and size constraints.
35pub const MEASURE_NODE: u16 = 2;
36
37// Where each field of a request record starts. Both kinds start with the kind.
38pub const MEASURE_KIND_AT: usize = 0;
39
40pub const TEXT_TYPE_ROLE_AT: usize = 2;
41pub const TEXT_TEXT_OFFSET_AT: usize = 4;
42pub const TEXT_TEXT_LENGTH_AT: usize = 8;
43pub const TEXT_SPANS_OFFSET_AT: usize = 12;
44pub const TEXT_SPANS_COUNT_AT: usize = 16;
45pub const TEXT_SPAN_FONTS_OFFSET_AT: usize = 20;
46pub const TEXT_SPAN_FONTS_COUNT_AT: usize = 24;
47pub const TEXT_FONT_OFFSET_AT: usize = 28;
48pub const TEXT_FONT_COUNT_AT: usize = 32;
49pub const TEXT_FONT_SIZE_AT: usize = 36;
50pub const TEXT_FONT_WEIGHT_AT: usize = 40;
51pub const TEXT_ITALIC_AT: usize = 42;
52pub const TEXT_WRAP_AT: usize = 43;
53pub const TEXT_LETTER_SPACING_AT: usize = 44;
54pub const TEXT_LINE_HEIGHT_AT: usize = 48;
55pub const TEXT_MAX_LINES_AT: usize = 52;
56pub const TEXT_TAB_SIZE_AT: usize = 56;
57pub const TEXT_WORD_BREAK_AT: usize = 57;
58pub const TEXT_OVERFLOW_WRAP_AT: usize = 58;
59pub const TEXT_ABSOLUTE_SIZE_AT: usize = 59;
60pub const TEXT_CONSTRAINT_AT: usize = 60;
61pub const TEXT_WIDTH_AT: usize = 64;
62pub const TEXT_ZOOM_AT: usize = 68;
63
64pub const NODE_ID_AT: usize = 4;
65pub const NODE_MIN_WIDTH_AT: usize = 8;
66pub const NODE_MAX_WIDTH_AT: usize = 12;
67pub const NODE_MIN_HEIGHT_AT: usize = 16;
68pub const NODE_MAX_HEIGHT_AT: usize = 20;
69pub const NODE_ZOOM_AT: usize = 24;
70
71/// The narrowest the text can be: its longest piece that cannot be broken.
72pub const CONSTRAINT_MIN_CONTENT: u32 = 1;
73/// The text with no line broken except where it says so.
74pub const CONSTRAINT_MAX_CONTENT: u32 = 2;
75/// The text broken into lines no wider than the given width.
76pub const CONSTRAINT_AT_MOST: u32 = 3;
77
78// Where each field of a result record starts.
79pub const RESULT_WIDTH_AT: usize = 0;
80pub const RESULT_HEIGHT_AT: usize = 4;
81pub const RESULT_FIRST_BASELINE_AT: usize = 8;
82pub const RESULT_LAST_BASELINE_AT: usize = 12;
83pub const RESULT_LAST_LINE_WIDTH_AT: usize = 16;
84pub const RESULT_LINE_COUNT_AT: usize = 20;
85pub const RESULT_FLAGS_AT: usize = 24;
86pub const RESULT_STATUS_AT: usize = 28;
87
88/// The text had more lines than `max_lines` allowed, and some were not shown.
89pub const FLAG_TRUNCATED: u32 = 1;
90
91pub const RESULT_OK: u32 = 0;
92/// The node is not in the Renderer's table: never sent, removed, or sent in the batch the
93/// Host is computing right now, which the Renderer applies only once the call returns.
94pub const RESULT_UNKNOWN_NODE: u32 = 1;
95/// The record points outside the buffer, or is a kind or value the Renderer does not know.
96pub const RESULT_MALFORMED: u32 = 2;
97
98/// The request buffer as a whole cannot be read: shorter than its records, or null.
99pub const MEASURE_UNREADABLE: i32 = -1;
100/// The call came from a thread other than the Renderer's UI thread, and nothing was
101/// measured. The same number `run` gives for the same mistake.
102pub const MEASURE_OFF_UI_THREAD: i32 = -4;
103/// There is nothing to measure with yet: the Renderer has not composed its first frame,
104/// so it has no density, no fonts and no design system. Also what a build with no
105/// renderer answers.
106pub const MEASURE_UNAVAILABLE: i32 = -5;
107
108/// Hash input for the record layouts above, so a Renderer generated from another layout
109/// is refused at the handshake rather than reading the wrong bytes.
110pub(crate) const SCHEMA_DESCRIPTOR: &str = concat!(
111    "measure=v1;record=72;result=32;",
112    "text=kind@0,type_role@2,text@4,spans@12,span_fonts@20,font@28,font_size@36,",
113    "font_weight@40,italic@42,wrap@43,letter_spacing@44,line_height@48,max_lines@52,",
114    "tab_size@56,word_break@57,overflow_wrap@58,absolute_size@59,constraint@60,width@64,",
115    "zoom@68;",
116    "node=kind@0,node@4,min_width@8,max_width@12,min_height@16,max_height@20,zoom@24;",
117    "result=width@0,height@4,first_baseline@8,last_baseline@12,last_line_width@16,",
118    "line_count@20,flags@24,status@28;",
119    "fontref=12,kind@0,value@4,length@8;spanfont=12,span@0,offset@4,count@8;",
120);
121
122/// The Renderer's answer to one request, in dp.
123#[repr(C)]
124#[derive(Clone, Copy, Debug, Default, PartialEq)]
125pub struct MeasureResult {
126    pub width: f32,
127    pub height: f32,
128    /// NaN where there is no baseline.
129    pub first_baseline: f32,
130    pub last_baseline: f32,
131    /// How wide the last line is, which is where inline content after the text starts.
132    /// NaN for a node.
133    pub last_line_width: f32,
134    /// Zero for a node.
135    pub line_count: u32,
136    pub flags: u32,
137    pub status: u32,
138}
139
140const _: () = assert!(std::mem::size_of::<MeasureResult>() == MEASURE_RESULT_LEN);
141
142impl MeasureResult {
143    /// What a refused call leaves in every slot: no size, and a status that says it was
144    /// not measured.
145    pub const REFUSED: Self = Self {
146        width: 0.0,
147        height: 0.0,
148        first_baseline: f32::NAN,
149        last_baseline: f32::NAN,
150        last_line_width: f32::NAN,
151        line_count: 0,
152        flags: 0,
153        status: RESULT_MALFORMED,
154    };
155
156    pub fn status(&self) -> MeasureStatus {
157        match self.status {
158            RESULT_OK => MeasureStatus::Ok,
159            RESULT_UNKNOWN_NODE => MeasureStatus::UnknownNode,
160            RESULT_MALFORMED => MeasureStatus::Malformed,
161            other => MeasureStatus::Other(other),
162        }
163    }
164
165    /// Whether `max_lines` cut the text short.
166    pub fn truncated(&self) -> bool {
167        self.flags & FLAG_TRUNCATED != 0
168    }
169}
170
171/// How one request went.
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum MeasureStatus {
174    Ok,
175    UnknownNode,
176    Malformed,
177    /// A status this build does not know, from a Renderer newer than it.
178    Other(u32),
179}
180
181/// How wide the text may be.
182#[derive(Clone, Copy, Debug, PartialEq)]
183pub enum TextConstraint {
184    /// The narrowest it can be: CSS `min-content`.
185    MinContent,
186    /// As wide as it is with no line broken except where it says so: CSS `max-content`.
187    MaxContent,
188    /// Lines broken to fit this many dp.
189    AtMost(f32),
190}
191
192/// How a run of text is set, as the Renderer reads it.
193///
194/// The same fields a `Text` carries, meaning the same things: the Renderer resolves both
195/// with one function, so text measured with a style is the size the same text drawn with
196/// the same properties turns out to be.
197#[derive(Clone, Debug, PartialEq)]
198pub struct TextStyle {
199    pub type_role: TypeRole,
200    /// The fonts to try, for text with no role. Ignored where there is a role.
201    pub font: FontRefs,
202    /// `None` takes the role's size, or 14 for text with no role.
203    pub font_size: Option<f32>,
204    /// `None` takes the role's weight, or 400 for text with no role.
205    pub font_weight: Option<u16>,
206    pub italic: bool,
207    /// `None` takes the role's spacing, or none for text with no role.
208    pub letter_spacing: Option<f32>,
209    /// `None` is the font's own line height.
210    pub line_height: Option<f32>,
211    /// `None` is no limit.
212    pub max_lines: Option<u32>,
213    /// Whether lines break at the width given. `false` is CSS `nowrap` and `pre`.
214    pub wrap: bool,
215    /// How many spaces' width apart the tab stops are.
216    pub tab_size: u8,
217    pub word_break: Option<WordBreak>,
218    pub overflow_wrap: Option<OverflowWrap>,
219    /// Sizes are CSS pixels: the system's font scale does not enlarge them.
220    pub absolute_size: bool,
221}
222
223impl Default for TextStyle {
224    fn default() -> Self {
225        Self {
226            type_role: TypeRole::Body,
227            font: FontRefs::default(),
228            font_size: None,
229            font_weight: None,
230            italic: false,
231            letter_spacing: None,
232            line_height: None,
233            max_lines: None,
234            wrap: true,
235            tab_size: 8,
236            word_break: None,
237            overflow_wrap: None,
238            absolute_size: false,
239        }
240    }
241}
242
243impl TextStyle {
244    /// Text set by the design system's rung `role`.
245    pub fn role(role: TypeRole) -> Self {
246        Self {
247            type_role: role,
248            ..Self::default()
249        }
250    }
251}
252
253/// The size a node may take, in dp. `f32::INFINITY` is no limit.
254#[derive(Clone, Copy, Debug, PartialEq)]
255pub struct NodeConstraints {
256    pub min_width: f32,
257    pub max_width: f32,
258    pub min_height: f32,
259    pub max_height: f32,
260}
261
262impl NodeConstraints {
263    /// Any size at all.
264    pub const UNBOUNDED: Self = Self {
265        min_width: 0.0,
266        max_width: f32::INFINITY,
267        min_height: 0.0,
268        max_height: f32::INFINITY,
269    };
270
271    /// Exactly this width, any height.
272    pub const fn width(width: f32) -> Self {
273        Self {
274            min_width: width,
275            max_width: width,
276            min_height: 0.0,
277            max_height: f32::INFINITY,
278        }
279    }
280}
281
282/// The fields of one record, kept until the buffer is laid out.
283#[derive(Clone, Copy, Debug)]
284struct PendingRecord {
285    bytes: [u8; MEASURE_RECORD_LEN],
286}
287
288/// A set of questions for one call, built up and then asked at once.
289///
290/// Reused across frames: clearing keeps every buffer's capacity, so once warm, asking the
291/// same number of questions allocates nothing.
292#[derive(Debug, Default)]
293pub struct MeasureRequests {
294    records: Vec<PendingRecord>,
295    /// The strings and lists the records point at, counted from the start of this area
296    /// until the buffer is laid out.
297    payload: Vec<u8>,
298    /// Payload positions holding an offset that is counted from the start of the payload
299    /// and has to be counted from the start of the buffer instead.
300    payload_patches: Vec<u32>,
301    /// The laid out buffer.
302    buffer: Vec<u8>,
303    /// The zoom the requests asked from here on are measured at, where it was set.
304    zoom: Option<f32>,
305}
306
307impl MeasureRequests {
308    pub fn new() -> Self {
309        Self::default()
310    }
311
312    /// Measures the requests asked from here on at zoom `zoom`: the zoom of the page
313    /// region they belong to, which is the operating system's text size times the
314    /// application's own zoom. The Renderer measures them at its density times `zoom`,
315    /// with no font scale of its own, and answers in the region's CSS pixels.
316    ///
317    /// A Host can hold several regions at different zooms, which is why this travels with
318    /// every request rather than being the Renderer's state. Never set, it is one.
319    pub fn set_zoom(&mut self, zoom: f32) {
320        self.zoom = Some(zoom);
321    }
322
323    /// Forgets every question, keeping the room they took.
324    pub fn clear(&mut self) {
325        self.records.clear();
326        self.payload.clear();
327        self.payload_patches.clear();
328        self.buffer.clear();
329    }
330
331    /// How many questions there are.
332    pub fn len(&self) -> usize {
333        self.records.len()
334    }
335
336    pub fn is_empty(&self) -> bool {
337        self.records.is_empty()
338    }
339
340    /// Asks how big `text`, with `spans` laid over it and set in `style`, is under
341    /// `constraint`. Returns the index its answer will have.
342    pub fn text(
343        &mut self,
344        text: &str,
345        spans: &TextSpans,
346        style: &TextStyle,
347        constraint: TextConstraint,
348    ) -> usize {
349        let mut record = [0u8; MEASURE_RECORD_LEN];
350        put_u16(&mut record, MEASURE_KIND_AT, MEASURE_TEXT);
351        put_u16(&mut record, TEXT_TYPE_ROLE_AT, style.type_role as u16);
352
353        let text_at = self.payload.len() as u32;
354        self.payload.extend_from_slice(text.as_bytes());
355        put_u32(&mut record, TEXT_TEXT_OFFSET_AT, text_at);
356        put_u32(&mut record, TEXT_TEXT_LENGTH_AT, text.len() as u32);
357
358        let runs = spans.as_bytes();
359        let spans_at = self.payload.len() as u32;
360        self.payload.extend_from_slice(runs);
361        put_u32(&mut record, TEXT_SPANS_OFFSET_AT, spans_at);
362        put_u32(
363            &mut record,
364            TEXT_SPANS_COUNT_AT,
365            (runs.len() / SPAN_LEN) as u32,
366        );
367
368        // Both font tables are copied as they are, records and names together, and only the
369        // offsets inside them are moved: a request costs no allocation once warm.
370        let (entries_at, entries) = self.copy_span_font_table(spans.font_table());
371        put_u32(&mut record, TEXT_SPAN_FONTS_OFFSET_AT, entries_at);
372        put_u32(&mut record, TEXT_SPAN_FONTS_COUNT_AT, entries);
373        let (font_at, fonts) = self.copy_font_list(style.font.as_bytes());
374        put_u32(&mut record, TEXT_FONT_OFFSET_AT, font_at);
375        put_u32(&mut record, TEXT_FONT_COUNT_AT, fonts);
376
377        put_f32(
378            &mut record,
379            TEXT_FONT_SIZE_AT,
380            style.font_size.unwrap_or(0.0),
381        );
382        put_u16(
383            &mut record,
384            TEXT_FONT_WEIGHT_AT,
385            style.font_weight.unwrap_or(0),
386        );
387        record[TEXT_ITALIC_AT] = u8::from(style.italic);
388        record[TEXT_WRAP_AT] = u8::from(style.wrap);
389        put_f32(
390            &mut record,
391            TEXT_LETTER_SPACING_AT,
392            style.letter_spacing.unwrap_or(f32::NAN),
393        );
394        put_f32(
395            &mut record,
396            TEXT_LINE_HEIGHT_AT,
397            style.line_height.unwrap_or(f32::NAN),
398        );
399        put_u32(&mut record, TEXT_MAX_LINES_AT, style.max_lines.unwrap_or(0));
400        record[TEXT_TAB_SIZE_AT] = style.tab_size;
401        record[TEXT_WORD_BREAK_AT] = style.word_break.map_or(0, |value| value as u8);
402        record[TEXT_OVERFLOW_WRAP_AT] = style.overflow_wrap.map_or(0, |value| value as u8);
403        record[TEXT_ABSOLUTE_SIZE_AT] = u8::from(style.absolute_size);
404        let (constraint, width) = match constraint {
405            TextConstraint::MinContent => (CONSTRAINT_MIN_CONTENT, 0.0),
406            TextConstraint::MaxContent => (CONSTRAINT_MAX_CONTENT, 0.0),
407            TextConstraint::AtMost(width) => (CONSTRAINT_AT_MOST, width),
408        };
409        put_u32(&mut record, TEXT_CONSTRAINT_AT, constraint);
410        put_f32(&mut record, TEXT_WIDTH_AT, width);
411        put_f32(&mut record, TEXT_ZOOM_AT, self.zoom.unwrap_or(0.0));
412        self.records.push(PendingRecord { bytes: record });
413        self.records.len() - 1
414    }
415
416    /// Asks how big the node `node_id`, which the Renderer has already applied, is under
417    /// `constraints`. Returns the index its answer will have.
418    pub fn node(&mut self, node_id: u32, constraints: NodeConstraints) -> usize {
419        let mut record = [0u8; MEASURE_RECORD_LEN];
420        put_u16(&mut record, MEASURE_KIND_AT, MEASURE_NODE);
421        put_u32(&mut record, NODE_ID_AT, node_id);
422        put_f32(&mut record, NODE_MIN_WIDTH_AT, constraints.min_width);
423        put_f32(&mut record, NODE_MAX_WIDTH_AT, constraints.max_width);
424        put_f32(&mut record, NODE_MIN_HEIGHT_AT, constraints.min_height);
425        put_f32(&mut record, NODE_MAX_HEIGHT_AT, constraints.max_height);
426        put_f32(&mut record, NODE_ZOOM_AT, self.zoom.unwrap_or(0.0));
427        self.records.push(PendingRecord { bytes: record });
428        self.records.len() - 1
429    }
430
431    /// Copies a `Font` property blob into the payload without its count, moving every
432    /// name it points at to where it now sits, and answers where the records start and how
433    /// many there are.
434    fn copy_font_list(&mut self, blob: &[u8]) -> (u32, u32) {
435        let at = self.payload.len();
436        if blob.len() < 4 {
437            return (at as u32, 0);
438        }
439        let count = read_u32(blob, 0);
440        self.payload.extend_from_slice(&blob[4..]);
441        // Everything in the blob moved from `offset` to `at - 4 + offset`.
442        let moved = (at as u32).wrapping_sub(4);
443        self.move_font_records(at, count, moved);
444        (at as u32, count)
445    }
446
447    /// Copies a `SpanFonts` table without its count, moving every offset inside it, and
448    /// answers where the entries start and how many there are.
449    fn copy_span_font_table(&mut self, table: &[u8]) -> (u32, u32) {
450        let at = self.payload.len();
451        if table.len() < 4 {
452            return (at as u32, 0);
453        }
454        let entries = read_u32(table, 0);
455        self.payload.extend_from_slice(&table[4..]);
456        let moved = (at as u32).wrapping_sub(4);
457        for entry in 0..entries as usize {
458            let field = at + entry * SPAN_FONT_ENTRY_LEN + SPAN_FONT_OFFSET_AT;
459            if field + 4 > self.payload.len() {
460                break;
461            }
462            let list = read_u32(&self.payload, field).wrapping_add(moved);
463            self.payload[field..field + 4].copy_from_slice(&list.to_le_bytes());
464            self.payload_patches.push(field as u32);
465            let count = read_u32(&self.payload, at + entry * SPAN_FONT_ENTRY_LEN + 8);
466            self.move_font_records(list as usize, count, moved);
467        }
468        (at as u32, entries)
469    }
470
471    /// Moves the name offset of every family named by name among the `count` records at
472    /// `at`, and marks it to be counted from the start of the buffer.
473    fn move_font_records(&mut self, at: usize, count: u32, moved: u32) {
474        for index in 0..count as usize {
475            let record = at + index * FONT_REF_LEN;
476            if record + FONT_REF_LEN > self.payload.len() {
477                break;
478            }
479            if read_u32(&self.payload, record) != FONT_REF_SYSTEM {
480                continue;
481            }
482            let field = record + 4;
483            let name = read_u32(&self.payload, field).wrapping_add(moved);
484            self.payload[field..field + 4].copy_from_slice(&name.to_le_bytes());
485            self.payload_patches.push(field as u32);
486        }
487    }
488
489    /// The buffer these requests are sent as: the records, then the payload, every offset
490    /// counted from its start.
491    pub fn encoded(&mut self) -> &[u8] {
492        self.finish().0
493    }
494
495    /// Lays the records and the payload out as one buffer, every offset counted from its
496    /// start, and answers with it and the number of records.
497    fn finish(&mut self) -> (&[u8], u32) {
498        let base = (self.records.len() * MEASURE_RECORD_LEN) as u32;
499        self.buffer.clear();
500        self.buffer.reserve(base as usize + self.payload.len());
501        for record in &self.records {
502            let mut bytes = record.bytes;
503            if u16::from_le_bytes([bytes[0], bytes[1]]) == MEASURE_TEXT {
504                for field in [
505                    TEXT_TEXT_OFFSET_AT,
506                    TEXT_SPANS_OFFSET_AT,
507                    TEXT_SPAN_FONTS_OFFSET_AT,
508                    TEXT_FONT_OFFSET_AT,
509                ] {
510                    let value = read_u32(&bytes, field) + base;
511                    put_u32(&mut bytes, field, value);
512                }
513            }
514            self.buffer.extend_from_slice(&bytes);
515        }
516        let payload_at = self.buffer.len();
517        self.buffer.extend_from_slice(&self.payload);
518        for &patch in &self.payload_patches {
519            let at = payload_at + patch as usize;
520            let value = read_u32(&self.buffer, at) + base;
521            self.buffer[at..at + 4].copy_from_slice(&value.to_le_bytes());
522        }
523        (&self.buffer, self.records.len() as u32)
524    }
525}
526
527/// The right to ask, for the length of one Host call.
528///
529/// Not `Send`: the Renderer answers on its UI thread only, and the Host call this was
530/// handed out in is on that thread.
531pub struct Measurer {
532    _on_this_thread: PhantomData<*const ()>,
533}
534
535/// Why nothing was measured.
536#[derive(Clone, Debug, Eq, PartialEq)]
537pub enum MeasureError {
538    /// Not inside a Host call, so the Renderer is not waiting on this thread to answer.
539    OutsideHostCall,
540    /// The Renderer refused the whole call. The answers were left at zero size, and the
541    /// layout carries on with them.
542    Refused(ProtocolError),
543}
544
545/// A measurer, if the Host is inside one of its own calls on this thread, which is the
546/// only time there is a Renderer standing still to answer.
547pub fn measurer() -> Result<Measurer, MeasureError> {
548    if crate::boundary::in_host_call() {
549        Ok(Measurer {
550            _on_this_thread: PhantomData,
551        })
552    } else {
553        Err(MeasureError::OutsideHostCall)
554    }
555}
556
557impl Measurer {
558    /// Asks every question in `requests` in one call and puts the answers in `results`, in
559    /// the order they were asked.
560    ///
561    /// A request the Renderer could not answer says so in its own status, and the rest are
562    /// answered. A call the Renderer refused as a whole leaves every answer at zero size
563    /// and comes back as an error, which the layout is free to carry on past: nothing
564    /// stops, and the next frame asks again.
565    pub fn measure(
566        &mut self,
567        requests: &mut MeasureRequests,
568        results: &mut Vec<MeasureResult>,
569    ) -> Result<(), MeasureError> {
570        let (buffer, count) = requests.finish();
571        results.clear();
572        results.resize(count as usize, MeasureResult::default());
573        self.measure_encoded(buffer, count, results)
574    }
575
576    /// The same, for a request buffer encoded by hand. `results` must have one slot per
577    /// record.
578    pub fn measure_encoded(
579        &mut self,
580        buffer: &[u8],
581        count: u32,
582        results: &mut [MeasureResult],
583    ) -> Result<(), MeasureError> {
584        if results.len() < count as usize {
585            return Err(MeasureError::Refused(ProtocolError::LengthOverflow));
586        }
587        if count == 0 {
588            return Ok(());
589        }
590        let length = u32::try_from(buffer.len())
591            .map_err(|_| MeasureError::Refused(ProtocolError::LengthOverflow))?;
592        let status =
593            crate::boundary::renderer_measure(buffer.as_ptr(), length, count, results.as_mut_ptr());
594        if status < 0 {
595            for result in results.iter_mut().take(count as usize) {
596                *result = MeasureResult::REFUSED;
597            }
598            report_refusal(status);
599            return Err(MeasureError::Refused(ProtocolError::MeasureRefused(status)));
600        }
601        Ok(())
602    }
603}
604
605/// Says once per kind of refusal what went wrong, on standard error. A refusal is the
606/// Renderer's or the Host's bug rather than the application's, and a layout that silently
607/// had every size at zero is the hardest kind of wrong to find.
608fn report_refusal(status: i32) {
609    use std::sync::atomic::{AtomicU32, Ordering};
610    static SAID: AtomicU32 = AtomicU32::new(0);
611    let bit = 1u32 << (status.unsigned_abs().min(31));
612    if SAID.fetch_or(bit, Ordering::Relaxed) & bit != 0 {
613        return;
614    }
615    let reason = match status {
616        MEASURE_UNREADABLE => "the request buffer could not be read",
617        MEASURE_OFF_UI_THREAD => "the call was made off the Renderer's UI thread",
618        MEASURE_UNAVAILABLE => {
619            "the Renderer has nothing to measure with yet (no first frame, or no renderer)"
620        }
621        _ => "the Renderer gave no reason",
622    };
623    eprintln!(
624        "compose-rust: a measure call was refused with status {status}: {reason}. Every \
625         size in it is zero and the layout carried on."
626    );
627}
628
629fn put_u16(bytes: &mut [u8], at: usize, value: u16) {
630    bytes[at..at + 2].copy_from_slice(&value.to_le_bytes());
631}
632
633fn put_u32(bytes: &mut [u8], at: usize, value: u32) {
634    bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
635}
636
637fn put_f32(bytes: &mut [u8], at: usize, value: f32) {
638    bytes[at..at + 4].copy_from_slice(&value.to_bits().to_le_bytes());
639}
640
641fn read_u32(bytes: &[u8], at: usize) -> u32 {
642    u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
643}
644
645// ---------------------------------------------------------------------------------------
646// The mock renderer's answers.
647//
648// A test of the Host has no Renderer to ask, and wants answers it can predict. These are
649// the same rules the stand-in renderer's C answers by: every character is half the font
650// size wide, a line is one and a quarter font sizes tall, the first baseline sits one font
651// size down, and lines break at spaces. No node is known, because nothing applies a tree.
652// ---------------------------------------------------------------------------------------
653
654/// The font size the mock takes for text that does not give one.
655pub const FAKE_FONT_SIZE: f32 = 14.0;
656
657/// Answers a measure call the way the mock renderer does. Refuses a call from a thread
658/// that is not inside a Host call, which is what "off the UI thread" means to a Renderer
659/// that has no UI thread of its own.
660///
661/// # Safety
662/// `requests` must address `length` readable bytes, and `results` writable room for
663/// `count` results.
664#[doc(hidden)]
665pub unsafe extern "C" fn fake_measure(
666    requests: *const u8,
667    length: u32,
668    count: u32,
669    results: *mut MeasureResult,
670) -> i32 {
671    if !crate::boundary::in_host_call() {
672        return MEASURE_OFF_UI_THREAD;
673    }
674    if requests.is_null() || results.is_null() {
675        return MEASURE_UNREADABLE;
676    }
677    // SAFETY: the caller promises `length` readable bytes.
678    let buffer = unsafe { std::slice::from_raw_parts(requests, length as usize) };
679    // SAFETY: the caller promises room for `count` results.
680    let results = unsafe { std::slice::from_raw_parts_mut(results, count as usize) };
681    fake_measure_slices(buffer, results)
682}
683
684/// The mock's rules over slices, for whoever already has them.
685#[doc(hidden)]
686pub fn fake_measure_slices(buffer: &[u8], results: &mut [MeasureResult]) -> i32 {
687    let count = results.len();
688    if buffer.len() < count * MEASURE_RECORD_LEN {
689        return MEASURE_UNREADABLE;
690    }
691    for (index, result) in results.iter_mut().enumerate() {
692        let record = &buffer[index * MEASURE_RECORD_LEN..(index + 1) * MEASURE_RECORD_LEN];
693        *result = fake_one(buffer, record);
694    }
695    0
696}
697
698fn fake_one(buffer: &[u8], record: &[u8]) -> MeasureResult {
699    let malformed = MeasureResult {
700        status: RESULT_MALFORMED,
701        ..MeasureResult::REFUSED
702    };
703    match u16::from_le_bytes([record[0], record[1]]) {
704        MEASURE_NODE => MeasureResult {
705            status: RESULT_UNKNOWN_NODE,
706            ..MeasureResult::REFUSED
707        },
708        MEASURE_TEXT => {
709            let at = read_u32(record, TEXT_TEXT_OFFSET_AT) as usize;
710            let length = read_u32(record, TEXT_TEXT_LENGTH_AT) as usize;
711            let Some(text) = at
712                .checked_add(length)
713                .and_then(|end| buffer.get(at..end))
714                .and_then(|bytes| std::str::from_utf8(bytes).ok())
715            else {
716                return malformed;
717            };
718            let size = f32::from_bits(read_u32(record, TEXT_FONT_SIZE_AT));
719            let size = if size > 0.0 { size } else { FAKE_FONT_SIZE };
720            let line_height = f32::from_bits(read_u32(record, TEXT_LINE_HEIGHT_AT));
721            let line_height = if line_height.is_nan() || line_height <= 0.0 {
722                size * 1.25
723            } else {
724                line_height
725            };
726            let max_lines = read_u32(record, TEXT_MAX_LINES_AT);
727            let wrap = record[TEXT_WRAP_AT] != 0;
728            let advance = size * 0.5;
729            let width = f32::from_bits(read_u32(record, TEXT_WIDTH_AT));
730            let lines = match read_u32(record, TEXT_CONSTRAINT_AT) {
731                CONSTRAINT_MIN_CONTENT => {
732                    fake_lines(text, if wrap { 0.0 } else { f32::INFINITY }, advance)
733                }
734                CONSTRAINT_MAX_CONTENT => fake_lines(text, f32::INFINITY, advance),
735                CONSTRAINT_AT_MOST if width >= 0.0 => {
736                    fake_lines(text, if wrap { width } else { f32::INFINITY }, advance)
737                }
738                _ => return malformed,
739            };
740            let shown = if max_lines == 0 {
741                lines.len()
742            } else {
743                lines.len().min(max_lines as usize)
744            };
745            let widest = lines[..shown].iter().copied().fold(0.0f32, f32::max);
746            MeasureResult {
747                width: widest,
748                height: shown as f32 * line_height,
749                first_baseline: size,
750                last_baseline: (shown.max(1) - 1) as f32 * line_height + size,
751                last_line_width: lines[shown.max(1) - 1],
752                line_count: shown as u32,
753                flags: if shown < lines.len() {
754                    FLAG_TRUNCATED
755                } else {
756                    0
757                },
758                status: RESULT_OK,
759            }
760        }
761        _ => malformed,
762    }
763}
764
765/// The widths of the lines `text` breaks into at `width`, breaking at spaces and at
766/// newlines. A width of zero puts every word on its own line.
767fn fake_lines(text: &str, width: f32, advance: f32) -> Vec<f32> {
768    let mut lines = Vec::new();
769    for paragraph in text.split('\n') {
770        let mut line = 0usize;
771        for word in paragraph.split(' ') {
772            let word_length = word.chars().count();
773            let joined = if line == 0 {
774                word_length
775            } else {
776                line + 1 + word_length
777            };
778            if line != 0 && joined as f32 * advance > width {
779                lines.push(line as f32 * advance);
780                line = word_length;
781            } else {
782                line = joined;
783            }
784        }
785        lines.push(line as f32 * advance);
786    }
787    lines
788}