diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs index 45b202393b..8d44136651 100644 --- a/core/embed/rust/src/micropython/buffer.rs +++ b/core/embed/rust/src/micropython/buffer.rs @@ -16,9 +16,14 @@ use super::ffi; /// /// Given the above assumptions about MicroPython strings, working with /// 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 { ptr: *const u8, - len: usize, + len: u16, + off: u16, } impl StrBuffer { @@ -27,22 +32,41 @@ impl StrBuffer { } pub fn alloc(val: &str) -> Result { + 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 { // SAFETY: // 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. 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() { 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 // 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 { ptr: raw, - len: val.len(), + len: unwrap!(len.try_into()), + off: 0, }) } } @@ -51,7 +75,22 @@ impl StrBuffer { if self.ptr.is_null() { &[] } 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 for StrBuffer { let bufinfo = get_buffer_info(obj, ffi::MP_BUFFER_READ)?; let new = Self { 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. @@ -103,6 +143,8 @@ impl AsRef for StrBuffer { // - If constructed from a MicroPython string, we check validity of UTF-8 at // construction time. Python semantics promise not to mutate the underlying // 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()) } } } @@ -111,7 +153,8 @@ impl From<&'static str> for StrBuffer { fn from(val: &'static str) -> Self { Self { 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 { + 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")] impl crate::trace::Trace for StrBuffer { fn trace(&self, t: &mut dyn crate::trace::Tracer) { 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(_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) -} diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index e5681bd02b..f4e693493a 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -24,21 +24,25 @@ pub const PARAGRAPH_BOTTOM_SPACE: i16 = 5; pub type ParagraphVecLong = Vec, 32>; pub type ParagraphVecShort = Vec, 8>; -/// Maximum number of characters that can be displayed on screen at once. Used -/// for on-the-fly conversion of binary data to hexadecimal representation. -/// NOTE: can be fine-tuned for particular model screen to decrease memory -/// consumption and conversion time. -pub const SCRATCH_BUFFER_LEN: usize = 256; +/// Trait for internal representation of strings, which need to support +/// converting to short-lived &str reference as well as creating a new string by +/// skipping some number of bytes. Exists so that we can support `StrBuffer` as +/// well as `&'static str`. +/// +/// 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 { + fn skip_prefix(&self, bytes: usize) -> Self; +} pub trait ParagraphSource { + /// Determines the output type produced. + type StrType: ParagraphStrType; + /// Return text and associated style for given paragraph index and character /// offset within the paragraph. - /// - /// 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>; + fn at(&self, index: usize, offset: usize) -> Paragraph; /// Number of paragraphs. fn size(&self) -> usize; @@ -51,46 +55,6 @@ pub trait ParagraphSource { } } -impl ParagraphSource for Vec, N> -where - T: AsRef, -{ - 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 ParagraphSource for [Paragraph; N] -where - T: AsRef, -{ - 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 ParagraphSource for Paragraph -where - T: AsRef, -{ - 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 { area: Rect, placement: LinearPlacement, @@ -140,10 +104,10 @@ where /// Helper for `change_offset` which should not get monomorphized as it /// doesn't refer to T or Self. - fn dyn_change_offset( + fn dyn_change_offset( mut area: Rect, mut offset: PageOffset, - source: &dyn ParagraphSource, + source: &dyn ParagraphSource, visible: &mut Vec, ) { visible.clear(); @@ -174,24 +138,23 @@ where /// Iterate over visible layouts (bounding box, style) together /// with corresponding string content. Should not get monomorphized. - fn foreach_visible<'a, 'b>( - source: &'a dyn ParagraphSource, + fn foreach_visible<'a, 'b, S: ParagraphStrType>( + source: &'a dyn ParagraphSource, visible: &'a [TextLayout], offset: PageOffset, func: &'b mut dyn FnMut(&TextLayout, &str), ) { - let mut buffer = [0; SCRATCH_BUFFER_LEN]; let mut vis_iter = visible.iter(); let mut chr = offset.chr; for par in offset.par..source.size() { - let s = source.at(par, chr, &mut buffer).content; - if s.is_empty() { + let s = source.at(par, chr).content; + if s.as_ref().is_empty() { chr = 0; continue; } if let Some(layout) = vis_iter.next() { - func(layout, s); + func(layout, s.as_ref()); } else { break; } @@ -328,9 +291,9 @@ impl Paragraph { } /// Copy style and replace content. - pub fn with_content(&self, content: U) -> Paragraph { + pub fn map(&self, func: impl FnOnce(&T) -> U) -> Paragraph { Paragraph { - content, + content: func(&self.content), style: self.style, align: self.align, break_after: self.break_after, @@ -338,13 +301,6 @@ impl Paragraph { } } - pub fn offset_as_ref(&self, offset: usize) -> Paragraph<&str> - where - T: AsRef, - { - self.with_content(&self.content.as_ref()[offset..]) - } - fn layout(&self, area: Rect) -> TextLayout { TextLayout { padding_top: PARAGRAPH_TOP_SPACE, @@ -361,7 +317,7 @@ struct PageOffset { /// Index of paragraph. par: usize, - /// Index of character in the paragraph. + /// Index of (the first byte of) the character in the paragraph. chr: usize, } @@ -376,17 +332,16 @@ impl PageOffset { /// /// If the returned remaining area is not None then it holds that /// `next_offset.par == self.par + 1`. - fn advance( + fn advance( mut self, area: Rect, - source: &dyn ParagraphSource, + source: &dyn ParagraphSource, full_height: i16, ) -> (PageOffset, Option, Option) { - let mut buffer = [0; SCRATCH_BUFFER_LEN]; - let paragraph = source.at(self.par, self.chr, &mut buffer); + let paragraph = source.at(self.par, self.chr); // Skip empty paragraphs. - if paragraph.content.is_empty() { + if paragraph.content.as_ref().is_empty() { self.par += 1; self.chr = 0; 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. if paragraph.no_break && self.chr == 0 { - let mut next_buffer = [0; SCRATCH_BUFFER_LEN]; 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( ¶graph, @@ -411,7 +365,7 @@ impl PageOffset { // Find out the dimensions of the paragraph at given char offset. 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()); layout.bounds = used; @@ -441,9 +395,9 @@ impl PageOffset { ) } - fn should_place_pair_on_next_page( - this_paragraph: &Paragraph<&str>, - next_paragraph: &Paragraph<&str>, + fn should_place_pair_on_next_page( + this_paragraph: &Paragraph, + next_paragraph: &Paragraph, area: Rect, full_height: i16, ) -> bool { @@ -456,11 +410,11 @@ impl PageOffset { let full_area = area.with_height(full_height); let key_height = this_paragraph .layout(full_area) - .fit_text(this_paragraph.content) + .fit_text(this_paragraph.content.as_ref()) .height(); let val_height = next_paragraph .layout(full_area) - .fit_text(next_paragraph.content) + .fit_text(next_paragraph.content.as_ref()) .height(); let screen_full_threshold = this_paragraph.style.text_font.line_height() + next_paragraph.style.text_font.line_height(); @@ -492,9 +446,9 @@ struct PageBreakIterator<'a, T> { } impl PageBreakIterator<'_, T> { - fn dyn_next( + fn dyn_next( mut area: Rect, - paragraphs: &dyn ParagraphSource, + paragraphs: &dyn ParagraphSource, mut offset: PageOffset, ) -> Option { let full_height = area.height(); diff --git a/core/embed/rust/src/ui/layout/util.rs b/core/embed/rust/src/ui/layout/util.rs index a09a3aa758..c07773954b 100644 --- a/core/embed/rust/src/ui/layout/util.rs +++ b/core/embed/rust/src/ui/layout/util.rs @@ -1,18 +1,17 @@ use crate::{ error::Error, micropython::{ - buffer::{get_buffer, get_str_owner, StrBuffer}, + buffer::{hexlify_bytes, StrBuffer}, gc::Gc, iter::{Iter, IterBuf}, list::List, obj::Obj, }, ui::component::text::{ - paragraphs::{Paragraph, ParagraphSource}, + paragraphs::{Paragraph, ParagraphSource, ParagraphStrType}, TextStyle, }, }; -use core::str; use cstr_core::cstr; use heapless::Vec; @@ -41,19 +40,11 @@ where vec.into_array().map_err(|_| err) } -fn hexlify<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str { - 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; - } - // SAFETY: only <0x7f bytes are used to construct the string - unsafe { str::from_utf8_unchecked(&buffer[0..i]) } -} +/// Maximum number of characters that can be displayed on screen at once. Used +/// for on-the-fly conversion of binary data to hexadecimal representation. +/// NOTE: can be fine-tuned for particular model screen to decrease memory +/// consumption and conversion time. +pub const MAX_HEX_CHARS_ON_SCREEN: usize = 256; pub enum StrOrBytes { Str(StrBuffer), @@ -61,35 +52,13 @@ pub enum 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 { - StrOrBytes::Str(x) => &x.as_ref()[offset..], - StrOrBytes::Bytes(x) => Self::hexlify(*x, offset, buffer), + StrOrBytes::Str(x) => x.skip_prefix(offset), + 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 for StrOrBytes { @@ -116,11 +85,13 @@ pub struct 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 { match index { - 0 => Paragraph::new(self.description_font, &self.description.as_ref()[offset..]), - 1 => Paragraph::new(self.extra_font, &self.extra.as_ref()[offset..]), - 2 => Paragraph::new(self.data_font, self.data.as_str_offset(offset, buffer)), + 0 => Paragraph::new(self.description_font, self.description.skip_prefix(offset)), + 1 => Paragraph::new(self.extra_font, self.extra.skip_prefix(offset)), + 2 => Paragraph::new(self.data_font, self.data.as_str_offset(offset)), _ => unreachable!(), } } @@ -154,8 +125,10 @@ impl PropsList { } impl ParagraphSource for PropsList { - fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str> { - let block = move |buffer| { + type StrType = StrBuffer; + + fn at(&self, index: usize, offset: usize) -> Paragraph { + let block = move || { let entry = self.items.get(index / 2)?; let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?; let value_is_mono: bool = bool::try_from(value_is_mono)?; @@ -181,19 +154,15 @@ impl ParagraphSource for PropsList { }; if obj == Obj::const_none() { - return Ok(Paragraph::new(style, "")); + return Ok(Paragraph::new(style, StrBuffer::empty())); } let para = if obj.is_str() { - // SAFETY: - // As long as self is visible to GC, the string will be also. The paragraph - // 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..]) + let content: StrBuffer = obj.try_into()?; + Paragraph::new(style, content.skip_prefix(offset)) } else if obj.is_bytes() { - let s = StrOrBytes::hexlify(obj, offset, buffer); - Paragraph::new(style, s) + let content = hexlify_bytes(obj, offset, MAX_HEX_CHARS_ON_SCREEN)?; + Paragraph::new(style, content) } else { return Err(Error::TypeError); }; @@ -204,9 +173,9 @@ impl ParagraphSource for PropsList { Ok(para) } }; - match block(buffer) { - Ok(p) => p, - Err(_) => Paragraph::new(self.value_font, "ERROR"), + match block() { + Ok(para) => para, + Err(_) => Paragraph::new(self.value_font, StrBuffer::from("ERROR")), } } @@ -214,3 +183,48 @@ impl ParagraphSource for PropsList { 2 * self.items.len() } } + +impl ParagraphSource for Vec, N> { + type StrType = T; + + fn at(&self, index: usize, offset: usize) -> Paragraph { + let para = &self[index]; + para.map(|content| content.skip_prefix(offset)) + } + + fn size(&self) -> usize { + self.len() + } +} + +impl ParagraphSource for [Paragraph; N] { + type StrType = T; + + fn at(&self, index: usize, offset: usize) -> Paragraph { + let para = &self[index]; + para.map(|content| content.skip_prefix(offset)) + } + + fn size(&self) -> usize { + self.len() + } +} + +impl ParagraphSource for Paragraph { + type StrType = T; + + fn at(&self, index: usize, offset: usize) -> Paragraph { + 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) + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/result_popup.rs b/core/embed/rust/src/ui/model_tr/component/result_popup.rs index 979e2a783a..ca5cc5e6ee 100644 --- a/core/embed/rust/src/ui/model_tr/component/result_popup.rs +++ b/core/embed/rust/src/ui/model_tr/component/result_popup.rs @@ -2,7 +2,7 @@ use crate::{ time::Instant, ui::{ component::{ - text::paragraphs::{Paragraph, Paragraphs}, + text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, Child, Component, ComponentExt, Event, EventCtx, Label, Pad, }, constant::screen, @@ -18,13 +18,13 @@ pub enum ResultPopupMsg { Confirmed, } -pub struct ResultPopup { +pub struct ResultPopup { area: Rect, pad: Pad, result_anim: Child, headline_baseline: Point, headline: Option>, - text: Child>>, + text: Child>>, button: Option>>, autoclose: bool, } @@ -36,10 +36,10 @@ const ANIM_POS: i16 = 32; const ANIM_POS_ADJ_HEADLINE: i16 = 10; const ANIM_POS_ADJ_BUTTON: i16 = 6; -impl ResultPopup { +impl ResultPopup { pub fn new( icon: &'static [u8], - text: &'static str, + text: S, headline: Option<&'static str>, button_text: Option<&'static str>, ) -> Self { @@ -86,7 +86,7 @@ impl ResultPopup { } } -impl Component for ResultPopup { +impl Component for ResultPopup { type Msg = ResultPopupMsg; fn place(&mut self, bounds: Rect) -> Rect { @@ -155,7 +155,7 @@ impl Component for ResultPopup { } #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for ResultPopup { +impl crate::trace::Trace for ResultPopup { fn trace(&self, d: &mut dyn crate::trace::Tracer) { d.open("ResultPopup"); self.text.trace(d); diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index 0e296a0e77..c16ca41cdd 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -1,7 +1,9 @@ use crate::ui::{ component::{ image::BlendedImage, - text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, + text::paragraphs::{ + Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt, + }, Child, Component, Event, EventCtx, Never, }, geometry::{Insets, LinearPlacement, Rect}, @@ -93,18 +95,17 @@ pub struct IconDialog { impl IconDialog where - T: AsRef, + T: ParagraphStrType, U: Component, { pub fn new(icon: BlendedImage, title: T, controls: U) -> Self { Self { image: Child::new(icon), - paragraphs: ParagraphVecShort::from_iter([Paragraph::new( + paragraphs: Paragraphs::new(ParagraphVecShort::from_iter([Paragraph::new( &theme::TEXT_DEMIBOLD, title, ) - .centered()]) - .into_paragraphs() + .centered()])) .with_placement( LinearPlacement::vertical() .align_at_center() @@ -152,7 +153,7 @@ where impl Component for IconDialog where - T: AsRef, + T: ParagraphStrType, U: Component, { type Msg = DialogMsg; @@ -193,7 +194,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for IconDialog where - T: AsRef, + T: ParagraphStrType, U: crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_tt/component/number_input.rs b/core/embed/rust/src/ui/model_tt/component/number_input.rs index 9154270df9..f4a15cc68b 100644 --- a/core/embed/rust/src/ui/model_tt/component/number_input.rs +++ b/core/embed/rust/src/ui/model_tt/component/number_input.rs @@ -2,7 +2,7 @@ use crate::ui::{ component::{ base::ComponentExt, paginated::Paginate, - text::paragraphs::{Paragraph, Paragraphs}, + text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, Child, Component, Event, EventCtx, Pad, }, display::{self, Font}, @@ -33,7 +33,7 @@ where impl NumberInputDialog where F: Fn(u32) -> T, - T: AsRef, + T: ParagraphStrType, { pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Self { let text = description_func(init_value); @@ -69,7 +69,7 @@ where impl Component for NumberInputDialog where - T: AsRef, + T: ParagraphStrType, F: Fn(u32) -> T, { type Msg = NumberInputDialogMsg; @@ -131,7 +131,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for NumberInputDialog where - T: AsRef, + T: ParagraphStrType, F: Fn(u32) -> T, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index a370f35da5..a9e101987f 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -385,7 +385,7 @@ mod tests { trace::Trace, ui::{ component::{ - text::paragraphs::{Paragraph, Paragraphs}, + text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, Empty, }, event::TouchEvent, @@ -398,6 +398,12 @@ mod tests { 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 { let mut t = Vec::new(); val.trace(&mut t); diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 1602dbb1c4..3dee6a2013 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -21,8 +21,8 @@ use crate::{ painter, text::{ paragraphs::{ - Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, - Paragraphs, VecExt, + Checklist, Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecLong, + ParagraphVecShort, Paragraphs, VecExt, }, TextStyle, }, @@ -122,7 +122,7 @@ where impl ComponentMsgObj for IconDialog where - T: AsRef, + T: ParagraphStrType, U: Component, ::Msg: TryInto, { @@ -263,7 +263,7 @@ where impl ComponentMsgObj for NumberInputDialog where - T: AsRef, + T: ParagraphStrType, F: Fn(u32) -> T, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { @@ -1061,8 +1061,9 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu "RECOVERY MODE" }; - let paragraphs = - Paragraphs::new([Paragraph::new(&theme::TEXT_BOLD, "Number of words?").centered()]); + let paragraphs = Paragraphs::new( + Paragraph::new(&theme::TEXT_BOLD, StrBuffer::from("Number of words?")).centered(), + ); let obj = LayoutObj::new( Frame::new(title, Dialog::new(paragraphs, SelectWordCount::new()))