1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-10 15:30:55 +00:00

refactor(core/rust): support for StrBuffer slicing

[no changelog]
This commit is contained in:
Martin Milata 2022-12-01 00:37:07 +01:00
parent 452857757a
commit 4135b00708
8 changed files with 231 additions and 200 deletions

View File

@ -16,9 +16,14 @@ use super::ffi;
/// ///
/// Given the above assumptions about MicroPython strings, working with /// Given the above assumptions about MicroPython strings, working with
/// StrBuffers in Rust is safe. /// StrBuffers in Rust is safe.
///
/// The `off` field represents offset from the `ptr` and allows us to do
/// substring slices while keeping the head pointer as required by GC.
#[repr(C)]
pub struct StrBuffer { pub struct StrBuffer {
ptr: *const u8, ptr: *const u8,
len: usize, len: u16,
off: u16,
} }
impl StrBuffer { impl StrBuffer {
@ -27,22 +32,41 @@ impl StrBuffer {
} }
pub fn alloc(val: &str) -> Result<Self, Error> { pub fn alloc(val: &str) -> Result<Self, Error> {
unsafe {
Self::alloc_with(val.len(), |buffer| {
// SAFETY: Memory should be freshly allocated and as such cannot overlap.
ptr::copy_nonoverlapping(val.as_ptr(), buffer.as_mut_ptr(), buffer.len())
})
}
}
pub fn alloc_with(len: usize, func: impl FnOnce(&mut [u8])) -> Result<Self, Error> {
// SAFETY: // SAFETY:
// We assume that if `gc_alloc` returns successfully, the result is a valid // We assume that if `gc_alloc` returns successfully, the result is a valid
// pointer to GC-controlled memory of at least `val.len() + 1` bytes. // pointer to GC-controlled memory of at least `val.len() + 1` bytes.
unsafe { unsafe {
let raw = ffi::gc_alloc(val.len() + 1, 0) as *mut u8; let raw = ffi::gc_alloc(len + 1, 0) as *mut u8;
if raw.is_null() { if raw.is_null() {
return Err(Error::AllocationFailed); return Err(Error::AllocationFailed);
} }
// SAFETY: Memory should be freshly allocated and as such cannot overlap.
ptr::copy_nonoverlapping(val.as_ptr(), raw, val.len()); // SAFETY: GC returns valid pointers, slice is discarded after `func`.
let bytes = slice::from_raw_parts_mut(raw, len);
// GC returns uninitialized memory which we must make sure to overwrite,
// otherwise leftover references may keep alive otherwise dead
// objects. Zero the entire buffer so we don't have to rely on
// `func` doing it.
bytes.fill(0);
func(bytes);
str::from_utf8(bytes).map_err(|_| Error::OutOfRange)?;
// Null-terminate the string for C ASCIIZ compatibility. This will not be // Null-terminate the string for C ASCIIZ compatibility. This will not be
// reflected in Rust-visible slice, the zero byte is after the end. // reflected in Rust-visible slice, the zero byte is after the end.
raw.add(val.len()).write(0); raw.add(len).write(0);
Ok(Self { Ok(Self {
ptr: raw, ptr: raw,
len: val.len(), len: unwrap!(len.try_into()),
off: 0,
}) })
} }
} }
@ -51,7 +75,22 @@ impl StrBuffer {
if self.ptr.is_null() { if self.ptr.is_null() {
&[] &[]
} else { } else {
unsafe { slice::from_raw_parts(self.ptr, self.len) } unsafe { slice::from_raw_parts(self.ptr.add(self.off.into()), self.len.into()) }
}
}
pub fn offset(&self, skip_bytes: usize) -> Self {
let off: u16 = unwrap!(skip_bytes.try_into());
assert!(off <= self.len);
assert!(self.as_ref().is_char_boundary(skip_bytes));
Self {
ptr: self.ptr,
// Does not overflow because `off <= self.len`.
len: self.len - off,
// `self.off + off` could only overflow if `self.off + self.len` could overflow, and
// given that `off` only advances by as much as `len` decreases, that should not be
// possible either.
off: self.off + off,
} }
} }
} }
@ -70,7 +109,8 @@ impl TryFrom<Obj> for StrBuffer {
let bufinfo = get_buffer_info(obj, ffi::MP_BUFFER_READ)?; let bufinfo = get_buffer_info(obj, ffi::MP_BUFFER_READ)?;
let new = Self { let new = Self {
ptr: bufinfo.buf as _, ptr: bufinfo.buf as _,
len: bufinfo.len as _, len: bufinfo.len.try_into()?,
off: 0,
}; };
// MicroPython _should_ ensure that values of type `str` are UTF-8. // MicroPython _should_ ensure that values of type `str` are UTF-8.
@ -103,6 +143,8 @@ impl AsRef<str> for StrBuffer {
// - If constructed from a MicroPython string, we check validity of UTF-8 at // - If constructed from a MicroPython string, we check validity of UTF-8 at
// construction time. Python semantics promise not to mutate the underlying // construction time. Python semantics promise not to mutate the underlying
// data from under us. // data from under us.
// - If constructed by `offset()`, we expect the input to be UTF-8 and check
// that we split the string at character boundary.
unsafe { str::from_utf8_unchecked(self.as_bytes()) } unsafe { str::from_utf8_unchecked(self.as_bytes()) }
} }
} }
@ -111,7 +153,8 @@ impl From<&'static str> for StrBuffer {
fn from(val: &'static str) -> Self { fn from(val: &'static str) -> Self {
Self { Self {
ptr: val.as_ptr(), ptr: val.as_ptr(),
len: val.len(), len: unwrap!(val.len().try_into()),
off: 0,
} }
} }
} }
@ -183,31 +226,43 @@ pub unsafe fn get_buffer_mut<'a>(obj: Obj) -> Result<&'a mut [u8], Error> {
} }
} }
fn hexlify(data: &[u8], buffer: &mut [u8]) {
const HEX_LOWER: [u8; 16] = *b"0123456789abcdef";
let mut i: usize = 0;
for b in data.iter().take(buffer.len() / 2) {
let hi: usize = ((b & 0xf0) >> 4).into();
let lo: usize = (b & 0x0f).into();
buffer[i] = HEX_LOWER[hi];
buffer[i + 1] = HEX_LOWER[lo];
i += 2;
}
}
pub fn hexlify_bytes(obj: Obj, offset: usize, max_len: usize) -> Result<StrBuffer, Error> {
if !obj.is_bytes() {
return Err(Error::TypeError);
}
// Convert offset to byte representation, handle case where it points in the
// middle of a byte.
let bin_off = offset / 2;
let hex_off = offset % 2;
// SAFETY:
// (a) only immutable references are taken
// (b) reference is discarded before returning to micropython
let bin_slice = unsafe { get_buffer(obj)? };
let bin_slice = &bin_slice[bin_off..];
let max_len = max_len & !1;
let hex_len = (bin_slice.len() * 2).min(max_len);
let result = StrBuffer::alloc_with(hex_len, move |buffer| hexlify(bin_slice, buffer))?;
Ok(result.offset(hex_off))
}
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl crate::trace::Trace for StrBuffer { impl crate::trace::Trace for StrBuffer {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.as_ref().trace(t) self.as_ref().trace(t)
} }
} }
/// Version of `get_buffer` for strings that ties the string lifetime with
/// another object that is expected to be placed on stack or in micropython heap
/// and be the beginning of a chain of references that lead to the `obj`.
///
/// SAFETY:
/// The caller must ensure that:
/// (a) the `_owner` is an object visible to the micropython GC,
/// (b) the `_owner` contains a reference that leads to `obj`, possibly through
/// other objects,
/// (c) that path is not broken as long as the returned reference lives.
pub unsafe fn get_str_owner<T: ?Sized>(_owner: &T, obj: Obj) -> Result<&str, Error> {
if !obj.is_str() {
return Err(Error::TypeError);
}
// SAFETY:
// (a) only immutable references are taken.
// (b) micropython guarantees immutability and the buffer is not freed/moved as
// long as _owner satisfies precondition.
let buffer = unsafe { get_buffer(obj)? };
str::from_utf8(buffer).map_err(|_| Error::TypeError)
}

View File

@ -24,21 +24,25 @@ pub const PARAGRAPH_BOTTOM_SPACE: i16 = 5;
pub type ParagraphVecLong<T> = Vec<Paragraph<T>, 32>; pub type ParagraphVecLong<T> = Vec<Paragraph<T>, 32>;
pub type ParagraphVecShort<T> = Vec<Paragraph<T>, 8>; pub type ParagraphVecShort<T> = Vec<Paragraph<T>, 8>;
/// Maximum number of characters that can be displayed on screen at once. Used /// Trait for internal representation of strings, which need to support
/// for on-the-fly conversion of binary data to hexadecimal representation. /// converting to short-lived &str reference as well as creating a new string by
/// NOTE: can be fine-tuned for particular model screen to decrease memory /// skipping some number of bytes. Exists so that we can support `StrBuffer` as
/// consumption and conversion time. /// well as `&'static str`.
pub const SCRATCH_BUFFER_LEN: usize = 256; ///
/// NOTE: do not implement this trait for `&'static str` in firmware. We always
/// use StrBuffer because using multiple internal representations results in
/// multiple copies of the code in flash memory.
pub trait ParagraphStrType: AsRef<str> {
fn skip_prefix(&self, bytes: usize) -> Self;
}
pub trait ParagraphSource { pub trait ParagraphSource {
/// Determines the output type produced.
type StrType: ParagraphStrType;
/// Return text and associated style for given paragraph index and character /// Return text and associated style for given paragraph index and character
/// offset within the paragraph. /// offset within the paragraph.
/// fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType>;
/// Implementations can use the provided buffer to perform some kind of
/// conversion (currently used for displaying binary data in
/// hexadecimal) and return a reference to this buffer. Caller needs to
/// make sure the buffer is large enough to fit screenfull of characters.
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str>;
/// Number of paragraphs. /// Number of paragraphs.
fn size(&self) -> usize; fn size(&self) -> usize;
@ -51,46 +55,6 @@ pub trait ParagraphSource {
} }
} }
impl<T, const N: usize> ParagraphSource for Vec<Paragraph<T>, N>
where
T: AsRef<str>,
{
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
self[index].offset_as_ref(offset)
}
fn size(&self) -> usize {
self.len()
}
}
impl<T, const N: usize> ParagraphSource for [Paragraph<T>; N]
where
T: AsRef<str>,
{
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
self[index].offset_as_ref(offset)
}
fn size(&self) -> usize {
self.len()
}
}
impl<T> ParagraphSource for Paragraph<T>
where
T: AsRef<str>,
{
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
assert_eq!(index, 0);
self.offset_as_ref(offset)
}
fn size(&self) -> usize {
1
}
}
pub struct Paragraphs<T> { pub struct Paragraphs<T> {
area: Rect, area: Rect,
placement: LinearPlacement, placement: LinearPlacement,
@ -140,10 +104,10 @@ where
/// Helper for `change_offset` which should not get monomorphized as it /// Helper for `change_offset` which should not get monomorphized as it
/// doesn't refer to T or Self. /// doesn't refer to T or Self.
fn dyn_change_offset( fn dyn_change_offset<S: ParagraphStrType>(
mut area: Rect, mut area: Rect,
mut offset: PageOffset, mut offset: PageOffset,
source: &dyn ParagraphSource, source: &dyn ParagraphSource<StrType = S>,
visible: &mut Vec<TextLayout, MAX_LINES>, visible: &mut Vec<TextLayout, MAX_LINES>,
) { ) {
visible.clear(); visible.clear();
@ -174,24 +138,23 @@ where
/// Iterate over visible layouts (bounding box, style) together /// Iterate over visible layouts (bounding box, style) together
/// with corresponding string content. Should not get monomorphized. /// with corresponding string content. Should not get monomorphized.
fn foreach_visible<'a, 'b>( fn foreach_visible<'a, 'b, S: ParagraphStrType>(
source: &'a dyn ParagraphSource, source: &'a dyn ParagraphSource<StrType = S>,
visible: &'a [TextLayout], visible: &'a [TextLayout],
offset: PageOffset, offset: PageOffset,
func: &'b mut dyn FnMut(&TextLayout, &str), func: &'b mut dyn FnMut(&TextLayout, &str),
) { ) {
let mut buffer = [0; SCRATCH_BUFFER_LEN];
let mut vis_iter = visible.iter(); let mut vis_iter = visible.iter();
let mut chr = offset.chr; let mut chr = offset.chr;
for par in offset.par..source.size() { for par in offset.par..source.size() {
let s = source.at(par, chr, &mut buffer).content; let s = source.at(par, chr).content;
if s.is_empty() { if s.as_ref().is_empty() {
chr = 0; chr = 0;
continue; continue;
} }
if let Some(layout) = vis_iter.next() { if let Some(layout) = vis_iter.next() {
func(layout, s); func(layout, s.as_ref());
} else { } else {
break; break;
} }
@ -328,9 +291,9 @@ impl<T> Paragraph<T> {
} }
/// Copy style and replace content. /// Copy style and replace content.
pub fn with_content<U>(&self, content: U) -> Paragraph<U> { pub fn map<U>(&self, func: impl FnOnce(&T) -> U) -> Paragraph<U> {
Paragraph { Paragraph {
content, content: func(&self.content),
style: self.style, style: self.style,
align: self.align, align: self.align,
break_after: self.break_after, break_after: self.break_after,
@ -338,13 +301,6 @@ impl<T> Paragraph<T> {
} }
} }
pub fn offset_as_ref(&self, offset: usize) -> Paragraph<&str>
where
T: AsRef<str>,
{
self.with_content(&self.content.as_ref()[offset..])
}
fn layout(&self, area: Rect) -> TextLayout { fn layout(&self, area: Rect) -> TextLayout {
TextLayout { TextLayout {
padding_top: PARAGRAPH_TOP_SPACE, padding_top: PARAGRAPH_TOP_SPACE,
@ -361,7 +317,7 @@ struct PageOffset {
/// Index of paragraph. /// Index of paragraph.
par: usize, par: usize,
/// Index of character in the paragraph. /// Index of (the first byte of) the character in the paragraph.
chr: usize, chr: usize,
} }
@ -376,17 +332,16 @@ impl PageOffset {
/// ///
/// If the returned remaining area is not None then it holds that /// If the returned remaining area is not None then it holds that
/// `next_offset.par == self.par + 1`. /// `next_offset.par == self.par + 1`.
fn advance( fn advance<S: ParagraphStrType>(
mut self, mut self,
area: Rect, area: Rect,
source: &dyn ParagraphSource, source: &dyn ParagraphSource<StrType = S>,
full_height: i16, full_height: i16,
) -> (PageOffset, Option<Rect>, Option<TextLayout>) { ) -> (PageOffset, Option<Rect>, Option<TextLayout>) {
let mut buffer = [0; SCRATCH_BUFFER_LEN]; let paragraph = source.at(self.par, self.chr);
let paragraph = source.at(self.par, self.chr, &mut buffer);
// Skip empty paragraphs. // Skip empty paragraphs.
if paragraph.content.is_empty() { if paragraph.content.as_ref().is_empty() {
self.par += 1; self.par += 1;
self.chr = 0; self.chr = 0;
return (self, Some(area), None); return (self, Some(area), None);
@ -394,9 +349,8 @@ impl PageOffset {
// Handle the `no_break` flag used to keep key-value pair on the same page. // Handle the `no_break` flag used to keep key-value pair on the same page.
if paragraph.no_break && self.chr == 0 { if paragraph.no_break && self.chr == 0 {
let mut next_buffer = [0; SCRATCH_BUFFER_LEN];
if let Some(next_paragraph) = if let Some(next_paragraph) =
(self.par + 1 < source.size()).then(|| source.at(self.par + 1, 0, &mut next_buffer)) (self.par + 1 < source.size()).then(|| source.at(self.par + 1, 0))
{ {
if Self::should_place_pair_on_next_page( if Self::should_place_pair_on_next_page(
&paragraph, &paragraph,
@ -411,7 +365,7 @@ impl PageOffset {
// Find out the dimensions of the paragraph at given char offset. // Find out the dimensions of the paragraph at given char offset.
let mut layout = paragraph.layout(area); let mut layout = paragraph.layout(area);
let fit = layout.fit_text(paragraph.content); let fit = layout.fit_text(paragraph.content.as_ref());
let (used, remaining_area) = area.split_top(fit.height()); let (used, remaining_area) = area.split_top(fit.height());
layout.bounds = used; layout.bounds = used;
@ -441,9 +395,9 @@ impl PageOffset {
) )
} }
fn should_place_pair_on_next_page( fn should_place_pair_on_next_page<S: ParagraphStrType>(
this_paragraph: &Paragraph<&str>, this_paragraph: &Paragraph<S>,
next_paragraph: &Paragraph<&str>, next_paragraph: &Paragraph<S>,
area: Rect, area: Rect,
full_height: i16, full_height: i16,
) -> bool { ) -> bool {
@ -456,11 +410,11 @@ impl PageOffset {
let full_area = area.with_height(full_height); let full_area = area.with_height(full_height);
let key_height = this_paragraph let key_height = this_paragraph
.layout(full_area) .layout(full_area)
.fit_text(this_paragraph.content) .fit_text(this_paragraph.content.as_ref())
.height(); .height();
let val_height = next_paragraph let val_height = next_paragraph
.layout(full_area) .layout(full_area)
.fit_text(next_paragraph.content) .fit_text(next_paragraph.content.as_ref())
.height(); .height();
let screen_full_threshold = this_paragraph.style.text_font.line_height() let screen_full_threshold = this_paragraph.style.text_font.line_height()
+ next_paragraph.style.text_font.line_height(); + next_paragraph.style.text_font.line_height();
@ -492,9 +446,9 @@ struct PageBreakIterator<'a, T> {
} }
impl<T: ParagraphSource> PageBreakIterator<'_, T> { impl<T: ParagraphSource> PageBreakIterator<'_, T> {
fn dyn_next( fn dyn_next<S: ParagraphStrType>(
mut area: Rect, mut area: Rect,
paragraphs: &dyn ParagraphSource, paragraphs: &dyn ParagraphSource<StrType = S>,
mut offset: PageOffset, mut offset: PageOffset,
) -> Option<PageOffset> { ) -> Option<PageOffset> {
let full_height = area.height(); let full_height = area.height();

View File

@ -1,18 +1,17 @@
use crate::{ use crate::{
error::Error, error::Error,
micropython::{ micropython::{
buffer::{get_buffer, get_str_owner, StrBuffer}, buffer::{hexlify_bytes, StrBuffer},
gc::Gc, gc::Gc,
iter::{Iter, IterBuf}, iter::{Iter, IterBuf},
list::List, list::List,
obj::Obj, obj::Obj,
}, },
ui::component::text::{ ui::component::text::{
paragraphs::{Paragraph, ParagraphSource}, paragraphs::{Paragraph, ParagraphSource, ParagraphStrType},
TextStyle, TextStyle,
}, },
}; };
use core::str;
use cstr_core::cstr; use cstr_core::cstr;
use heapless::Vec; use heapless::Vec;
@ -41,19 +40,11 @@ where
vec.into_array().map_err(|_| err) vec.into_array().map_err(|_| err)
} }
fn hexlify<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str { /// Maximum number of characters that can be displayed on screen at once. Used
const HEX_LOWER: [u8; 16] = *b"0123456789abcdef"; /// for on-the-fly conversion of binary data to hexadecimal representation.
let mut i: usize = 0; /// NOTE: can be fine-tuned for particular model screen to decrease memory
for b in data.iter().take(buffer.len() / 2) { /// consumption and conversion time.
let hi: usize = ((b & 0xf0) >> 4).into(); pub const MAX_HEX_CHARS_ON_SCREEN: usize = 256;
let lo: usize = (b & 0x0f).into();
buffer[i] = HEX_LOWER[hi];
buffer[i + 1] = HEX_LOWER[lo];
i += 2;
}
// SAFETY: only <0x7f bytes are used to construct the string
unsafe { str::from_utf8_unchecked(&buffer[0..i]) }
}
pub enum StrOrBytes { pub enum StrOrBytes {
Str(StrBuffer), Str(StrBuffer),
@ -61,35 +52,13 @@ pub enum StrOrBytes {
} }
impl StrOrBytes { impl StrOrBytes {
pub fn as_str_offset<'a>(&'a self, offset: usize, buffer: &'a mut [u8]) -> &'a str { pub fn as_str_offset(&self, offset: usize) -> StrBuffer {
match self { match self {
StrOrBytes::Str(x) => &x.as_ref()[offset..], StrOrBytes::Str(x) => x.skip_prefix(offset),
StrOrBytes::Bytes(x) => Self::hexlify(*x, offset, buffer), StrOrBytes::Bytes(x) => hexlify_bytes(*x, offset, MAX_HEX_CHARS_ON_SCREEN)
.unwrap_or_else(|_| StrBuffer::from("ERROR")),
} }
} }
fn hexlify(obj: Obj, offset: usize, buffer: &mut [u8]) -> &str {
if !obj.is_bytes() {
return "ERROR";
}
// Convert offset to byte representation, handle case where it points in the
// middle of a byte.
let bin_off = offset / 2;
let hex_off = offset % 2;
// SAFETY:
// (a) only immutable references are taken
// (b) reference is discarded before returning to micropython
let bin_slice = if let Ok(buf) = unsafe { get_buffer(obj) } {
&buf[bin_off..]
} else {
return "ERROR";
};
let hexadecimal = hexlify(bin_slice, buffer);
&hexadecimal[hex_off..]
}
} }
impl TryFrom<Obj> for StrOrBytes { impl TryFrom<Obj> for StrOrBytes {
@ -116,11 +85,13 @@ pub struct ConfirmBlob {
} }
impl ParagraphSource for ConfirmBlob { impl ParagraphSource for ConfirmBlob {
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str> { type StrType = StrBuffer;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
match index { match index {
0 => Paragraph::new(self.description_font, &self.description.as_ref()[offset..]), 0 => Paragraph::new(self.description_font, self.description.skip_prefix(offset)),
1 => Paragraph::new(self.extra_font, &self.extra.as_ref()[offset..]), 1 => Paragraph::new(self.extra_font, self.extra.skip_prefix(offset)),
2 => Paragraph::new(self.data_font, self.data.as_str_offset(offset, buffer)), 2 => Paragraph::new(self.data_font, self.data.as_str_offset(offset)),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -154,8 +125,10 @@ impl PropsList {
} }
impl ParagraphSource for PropsList { impl ParagraphSource for PropsList {
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str> { type StrType = StrBuffer;
let block = move |buffer| {
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
let block = move || {
let entry = self.items.get(index / 2)?; let entry = self.items.get(index / 2)?;
let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?; let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?;
let value_is_mono: bool = bool::try_from(value_is_mono)?; let value_is_mono: bool = bool::try_from(value_is_mono)?;
@ -181,19 +154,15 @@ impl ParagraphSource for PropsList {
}; };
if obj == Obj::const_none() { if obj == Obj::const_none() {
return Ok(Paragraph::new(style, "")); return Ok(Paragraph::new(style, StrBuffer::empty()));
} }
let para = if obj.is_str() { let para = if obj.is_str() {
// SAFETY: let content: StrBuffer = obj.try_into()?;
// As long as self is visible to GC, the string will be also. The paragraph Paragraph::new(style, content.skip_prefix(offset))
// rendering does not keep the returned references for long so we can reasonably
// expect for the chain of references from self to the buffer not to be broken.
let s = unsafe { get_str_owner(self, obj)? };
Paragraph::new(style, &s[offset..])
} else if obj.is_bytes() { } else if obj.is_bytes() {
let s = StrOrBytes::hexlify(obj, offset, buffer); let content = hexlify_bytes(obj, offset, MAX_HEX_CHARS_ON_SCREEN)?;
Paragraph::new(style, s) Paragraph::new(style, content)
} else { } else {
return Err(Error::TypeError); return Err(Error::TypeError);
}; };
@ -204,9 +173,9 @@ impl ParagraphSource for PropsList {
Ok(para) Ok(para)
} }
}; };
match block(buffer) { match block() {
Ok(p) => p, Ok(para) => para,
Err(_) => Paragraph::new(self.value_font, "ERROR"), Err(_) => Paragraph::new(self.value_font, StrBuffer::from("ERROR")),
} }
} }
@ -214,3 +183,48 @@ impl ParagraphSource for PropsList {
2 * self.items.len() 2 * self.items.len()
} }
} }
impl<T: ParagraphStrType, const N: usize> ParagraphSource for Vec<Paragraph<T>, N> {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
let para = &self[index];
para.map(|content| content.skip_prefix(offset))
}
fn size(&self) -> usize {
self.len()
}
}
impl<T: ParagraphStrType, const N: usize> ParagraphSource for [Paragraph<T>; N] {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
let para = &self[index];
para.map(|content| content.skip_prefix(offset))
}
fn size(&self) -> usize {
self.len()
}
}
impl<T: ParagraphStrType> ParagraphSource for Paragraph<T> {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
assert_eq!(index, 0);
self.map(|content| content.skip_prefix(offset))
}
fn size(&self) -> usize {
1
}
}
impl ParagraphStrType for StrBuffer {
fn skip_prefix(&self, chars: usize) -> Self {
self.offset(chars)
}
}

View File

@ -2,7 +2,7 @@ use crate::{
time::Instant, time::Instant,
ui::{ ui::{
component::{ component::{
text::paragraphs::{Paragraph, Paragraphs}, text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
Child, Component, ComponentExt, Event, EventCtx, Label, Pad, Child, Component, ComponentExt, Event, EventCtx, Label, Pad,
}, },
constant::screen, constant::screen,
@ -18,13 +18,13 @@ pub enum ResultPopupMsg {
Confirmed, Confirmed,
} }
pub struct ResultPopup { pub struct ResultPopup<S> {
area: Rect, area: Rect,
pad: Pad, pad: Pad,
result_anim: Child<ResultAnim>, result_anim: Child<ResultAnim>,
headline_baseline: Point, headline_baseline: Point,
headline: Option<Label<&'static str>>, headline: Option<Label<&'static str>>,
text: Child<Paragraphs<Paragraph<&'static str>>>, text: Child<Paragraphs<Paragraph<S>>>,
button: Option<Child<Button<&'static str>>>, button: Option<Child<Button<&'static str>>>,
autoclose: bool, autoclose: bool,
} }
@ -36,10 +36,10 @@ const ANIM_POS: i16 = 32;
const ANIM_POS_ADJ_HEADLINE: i16 = 10; const ANIM_POS_ADJ_HEADLINE: i16 = 10;
const ANIM_POS_ADJ_BUTTON: i16 = 6; const ANIM_POS_ADJ_BUTTON: i16 = 6;
impl ResultPopup { impl<S: ParagraphStrType> ResultPopup<S> {
pub fn new( pub fn new(
icon: &'static [u8], icon: &'static [u8],
text: &'static str, text: S,
headline: Option<&'static str>, headline: Option<&'static str>,
button_text: Option<&'static str>, button_text: Option<&'static str>,
) -> Self { ) -> Self {
@ -86,7 +86,7 @@ impl ResultPopup {
} }
} }
impl Component for ResultPopup { impl<S: ParagraphStrType> Component for ResultPopup<S> {
type Msg = ResultPopupMsg; type Msg = ResultPopupMsg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
@ -155,7 +155,7 @@ impl Component for ResultPopup {
} }
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl crate::trace::Trace for ResultPopup { impl<S: ParagraphStrType> crate::trace::Trace for ResultPopup<S> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) { fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("ResultPopup"); d.open("ResultPopup");
self.text.trace(d); self.text.trace(d);

View File

@ -1,7 +1,9 @@
use crate::ui::{ use crate::ui::{
component::{ component::{
image::BlendedImage, image::BlendedImage,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, text::paragraphs::{
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
},
Child, Component, Event, EventCtx, Never, Child, Component, Event, EventCtx, Never,
}, },
geometry::{Insets, LinearPlacement, Rect}, geometry::{Insets, LinearPlacement, Rect},
@ -93,18 +95,17 @@ pub struct IconDialog<T, U> {
impl<T, U> IconDialog<T, U> impl<T, U> IconDialog<T, U>
where where
T: AsRef<str>, T: ParagraphStrType,
U: Component, U: Component,
{ {
pub fn new(icon: BlendedImage, title: T, controls: U) -> Self { pub fn new(icon: BlendedImage, title: T, controls: U) -> Self {
Self { Self {
image: Child::new(icon), image: Child::new(icon),
paragraphs: ParagraphVecShort::from_iter([Paragraph::new( paragraphs: Paragraphs::new(ParagraphVecShort::from_iter([Paragraph::new(
&theme::TEXT_DEMIBOLD, &theme::TEXT_DEMIBOLD,
title, title,
) )
.centered()]) .centered()]))
.into_paragraphs()
.with_placement( .with_placement(
LinearPlacement::vertical() LinearPlacement::vertical()
.align_at_center() .align_at_center()
@ -152,7 +153,7 @@ where
impl<T, U> Component for IconDialog<T, U> impl<T, U> Component for IconDialog<T, U>
where where
T: AsRef<str>, T: ParagraphStrType,
U: Component, U: Component,
{ {
type Msg = DialogMsg<Never, U::Msg>; type Msg = DialogMsg<Never, U::Msg>;
@ -193,7 +194,7 @@ where
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for IconDialog<T, U> impl<T, U> crate::trace::Trace for IconDialog<T, U>
where where
T: AsRef<str>, T: ParagraphStrType,
U: crate::trace::Trace, U: crate::trace::Trace,
{ {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -2,7 +2,7 @@ use crate::ui::{
component::{ component::{
base::ComponentExt, base::ComponentExt,
paginated::Paginate, paginated::Paginate,
text::paragraphs::{Paragraph, Paragraphs}, text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
Child, Component, Event, EventCtx, Pad, Child, Component, Event, EventCtx, Pad,
}, },
display::{self, Font}, display::{self, Font},
@ -33,7 +33,7 @@ where
impl<T, F> NumberInputDialog<T, F> impl<T, F> NumberInputDialog<T, F>
where where
F: Fn(u32) -> T, F: Fn(u32) -> T,
T: AsRef<str>, T: ParagraphStrType,
{ {
pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Self { pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Self {
let text = description_func(init_value); let text = description_func(init_value);
@ -69,7 +69,7 @@ where
impl<T, F> Component for NumberInputDialog<T, F> impl<T, F> Component for NumberInputDialog<T, F>
where where
T: AsRef<str>, T: ParagraphStrType,
F: Fn(u32) -> T, F: Fn(u32) -> T,
{ {
type Msg = NumberInputDialogMsg; type Msg = NumberInputDialogMsg;
@ -131,7 +131,7 @@ where
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl<T, F> crate::trace::Trace for NumberInputDialog<T, F> impl<T, F> crate::trace::Trace for NumberInputDialog<T, F>
where where
T: AsRef<str>, T: ParagraphStrType,
F: Fn(u32) -> T, F: Fn(u32) -> T,
{ {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -385,7 +385,7 @@ mod tests {
trace::Trace, trace::Trace,
ui::{ ui::{
component::{ component::{
text::paragraphs::{Paragraph, Paragraphs}, text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
Empty, Empty,
}, },
event::TouchEvent, event::TouchEvent,
@ -398,6 +398,12 @@ mod tests {
const SCREEN: Rect = constant::screen().inset(theme::borders()); const SCREEN: Rect = constant::screen().inset(theme::borders());
impl ParagraphStrType for &'static str {
fn skip_prefix(&self, chars: usize) -> Self {
&self[chars..]
}
}
fn trace(val: &impl Trace) -> String { fn trace(val: &impl Trace) -> String {
let mut t = Vec::new(); let mut t = Vec::new();
val.trace(&mut t); val.trace(&mut t);

View File

@ -21,8 +21,8 @@ use crate::{
painter, painter,
text::{ text::{
paragraphs::{ paragraphs::{
Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Checklist, Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecLong,
Paragraphs, VecExt, ParagraphVecShort, Paragraphs, VecExt,
}, },
TextStyle, TextStyle,
}, },
@ -122,7 +122,7 @@ where
impl<T, U> ComponentMsgObj for IconDialog<T, U> impl<T, U> ComponentMsgObj for IconDialog<T, U>
where where
T: AsRef<str>, T: ParagraphStrType,
U: Component, U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>, <U as Component>::Msg: TryInto<Obj, Error = Error>,
{ {
@ -263,7 +263,7 @@ where
impl<T, F> ComponentMsgObj for NumberInputDialog<T, F> impl<T, F> ComponentMsgObj for NumberInputDialog<T, F>
where where
T: AsRef<str>, T: ParagraphStrType,
F: Fn(u32) -> T, F: Fn(u32) -> T,
{ {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
@ -1061,8 +1061,9 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu
"RECOVERY MODE" "RECOVERY MODE"
}; };
let paragraphs = let paragraphs = Paragraphs::new(
Paragraphs::new([Paragraph::new(&theme::TEXT_BOLD, "Number of words?").centered()]); Paragraph::new(&theme::TEXT_BOLD, StrBuffer::from("Number of words?")).centered(),
);
let obj = LayoutObj::new( let obj = LayoutObj::new(
Frame::new(title, Dialog::new(paragraphs, SelectWordCount::new())) Frame::new(title, Dialog::new(paragraphs, SelectWordCount::new()))