1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 22:38:08 +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
/// 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<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:
// 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<Obj> 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<str> 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<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")]
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<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 ParagraphVecShort<T> = Vec<Paragraph<T>, 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<str> {
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<Self::StrType>;
/// Number of paragraphs.
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> {
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<S: ParagraphStrType>(
mut area: Rect,
mut offset: PageOffset,
source: &dyn ParagraphSource,
source: &dyn ParagraphSource<StrType = S>,
visible: &mut Vec<TextLayout, MAX_LINES>,
) {
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<StrType = S>,
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<T> Paragraph<T> {
}
/// 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 {
content,
content: func(&self.content),
style: self.style,
align: self.align,
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 {
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<S: ParagraphStrType>(
mut self,
area: Rect,
source: &dyn ParagraphSource,
source: &dyn ParagraphSource<StrType = S>,
full_height: i16,
) -> (PageOffset, Option<Rect>, Option<TextLayout>) {
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(
&paragraph,
@ -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<S: ParagraphStrType>(
this_paragraph: &Paragraph<S>,
next_paragraph: &Paragraph<S>,
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<T: ParagraphSource> PageBreakIterator<'_, T> {
fn dyn_next(
fn dyn_next<S: ParagraphStrType>(
mut area: Rect,
paragraphs: &dyn ParagraphSource,
paragraphs: &dyn ParagraphSource<StrType = S>,
mut offset: PageOffset,
) -> Option<PageOffset> {
let full_height = area.height();

View File

@ -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<Obj> 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<Self::StrType> {
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<Self::StrType> {
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<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,
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<S> {
area: Rect,
pad: Pad,
result_anim: Child<ResultAnim>,
headline_baseline: Point,
headline: Option<Label<&'static str>>,
text: Child<Paragraphs<Paragraph<&'static str>>>,
text: Child<Paragraphs<Paragraph<S>>>,
button: Option<Child<Button<&'static str>>>,
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<S: ParagraphStrType> ResultPopup<S> {
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<S: ParagraphStrType> Component for ResultPopup<S> {
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<S: ParagraphStrType> crate::trace::Trace for ResultPopup<S> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("ResultPopup");
self.text.trace(d);

View File

@ -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<T, U> {
impl<T, U> IconDialog<T, U>
where
T: AsRef<str>,
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<T, U> Component for IconDialog<T, U>
where
T: AsRef<str>,
T: ParagraphStrType,
U: Component,
{
type Msg = DialogMsg<Never, U::Msg>;
@ -193,7 +194,7 @@ where
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for IconDialog<T, U>
where
T: AsRef<str>,
T: ParagraphStrType,
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -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<T, F> NumberInputDialog<T, F>
where
F: Fn(u32) -> T,
T: AsRef<str>,
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<T, F> Component for NumberInputDialog<T, F>
where
T: AsRef<str>,
T: ParagraphStrType,
F: Fn(u32) -> T,
{
type Msg = NumberInputDialogMsg;
@ -131,7 +131,7 @@ where
#[cfg(feature = "ui_debug")]
impl<T, F> crate::trace::Trace for NumberInputDialog<T, F>
where
T: AsRef<str>,
T: ParagraphStrType,
F: Fn(u32) -> T,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -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);

View File

@ -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<T, U> ComponentMsgObj for IconDialog<T, U>
where
T: AsRef<str>,
T: ParagraphStrType,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
@ -263,7 +263,7 @@ where
impl<T, F> ComponentMsgObj for NumberInputDialog<T, F>
where
T: AsRef<str>,
T: ParagraphStrType,
F: Fn(u32) -> T,
{
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"
};
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()))