1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-08-02 20:08:31 +00:00

fix(core/ui): fix receive dialogs for long xpubs

This commit is contained in:
Martin Milata 2023-03-14 21:16:56 +01:00
parent a4a7cb9e6c
commit c9a5ef7cfa
6 changed files with 67 additions and 33 deletions

View File

@ -15,24 +15,30 @@ use crate::{
use super::{theme, Frame, FrameMsg}; use super::{theme, Frame, FrameMsg};
const MAX_XPUBS: usize = 16;
pub struct AddressDetails<T> { pub struct AddressDetails<T> {
qr_code: Frame<Qr, T>, qr_code: Frame<Qr, T>,
details: Frame<Paragraphs<ParagraphVecShort<T>>, T>, details: Frame<Paragraphs<ParagraphVecShort<T>>, T>,
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>, xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
xpubs: Vec<(T, T), 16>, xpubs: Vec<(T, T), MAX_XPUBS>,
xpub_pages: Vec<u8, MAX_XPUBS>,
current_page: usize, current_page: usize,
} }
impl<T> AddressDetails<T> impl<T> AddressDetails<T>
where where
T: ParagraphStrType + From<&'static str>, T: ParagraphStrType,
{ {
pub fn new( pub fn new(
qr_address: T, qr_address: T,
case_sensitive: bool, case_sensitive: bool,
account: Option<T>, account: Option<T>,
path: Option<T>, path: Option<T>,
) -> Result<Self, Error> { ) -> Result<Self, Error>
where
T: From<&'static str>,
{
let mut para = ParagraphVecShort::new(); let mut para = ParagraphVecShort::new();
if let Some(a) = account { if let Some(a) = account {
para.add(Paragraph::new(&theme::TEXT_NORMAL, "Account:".into())); para.add(Paragraph::new(&theme::TEXT_NORMAL, "Account:".into()));
@ -68,16 +74,50 @@ where
.with_cancel_button() .with_cancel_button()
.with_border(theme::borders_horizontal_scroll()), .with_border(theme::borders_horizontal_scroll()),
xpubs: Vec::new(), xpubs: Vec::new(),
xpub_pages: Vec::new(),
current_page: 0, current_page: 0,
}; };
Ok(result) Ok(result)
} }
pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> { pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> {
self.xpub_pages.push(1u8).map_err(|_| Error::OutOfRange)?;
self.xpubs self.xpubs
.push((title, xpub)) .push((title, xpub))
.map_err(|_| Error::OutOfRange) .map_err(|_| Error::OutOfRange)
} }
fn switch_xpub(&mut self, i: usize, page: usize) -> usize
where
T: Clone,
{
// Context is needed for updating child so that it can request repaint. In this
// case the parent component that handles paging always requests complete
// repaint after page change so we can use a dummy context here.
let mut dummy_ctx = EventCtx::new();
self.xpub_view
.update_title(&mut dummy_ctx, self.xpubs[i].0.clone());
self.xpub_view.update_content(&mut dummy_ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
let npages = p.page_count();
p.change_page(page);
npages
})
}
fn lookup(&self, scrollbar_page: usize) -> (usize, usize) {
let mut xpub_index = 0;
let mut xpub_page = scrollbar_page;
for xpub_len in &self.xpub_pages {
if *xpub_len as usize <= xpub_page {
xpub_page -= *xpub_len as usize;
xpub_index += 1;
} else {
break;
}
}
(xpub_index, xpub_page)
}
} }
impl<T> Paginate for AddressDetails<T> impl<T> Paginate for AddressDetails<T>
@ -85,30 +125,23 @@ where
T: ParagraphStrType + Clone, T: ParagraphStrType + Clone,
{ {
fn page_count(&mut self) -> usize { fn page_count(&mut self) -> usize {
2 + self.xpubs.len() let total_xpub_pages: u8 = self.xpub_pages.iter().copied().sum();
2 + (total_xpub_pages as usize)
} }
fn change_page(&mut self, to_page: usize) { fn change_page(&mut self, to_page: usize) {
self.current_page = to_page; self.current_page = to_page;
if to_page > 1 { if to_page > 1 {
let i = to_page - 2; let i = to_page - 2;
// Context is needed for updating child so that it can request repaint. In this let (xpub_index, xpub_page) = self.lookup(i);
// case the parent component that handles paging always requests complete self.switch_xpub(xpub_index, xpub_page);
// repaint after page change so we can use a dummy context here.
let mut dummy_ctx = EventCtx::new();
self.xpub_view
.update_title(&mut dummy_ctx, self.xpubs[i].0.clone());
self.xpub_view.update_content(&mut dummy_ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
p.change_page(0)
});
} }
} }
} }
impl<T> Component for AddressDetails<T> impl<T> Component for AddressDetails<T>
where where
T: ParagraphStrType, T: ParagraphStrType + Clone,
{ {
type Msg = (); type Msg = ();
@ -116,6 +149,13 @@ where
self.qr_code.place(bounds); self.qr_code.place(bounds);
self.details.place(bounds); self.details.place(bounds);
self.xpub_view.place(bounds); self.xpub_view.place(bounds);
self.xpub_pages.clear();
for i in 0..self.xpubs.len() {
let npages = self.switch_xpub(i, 0) as u8;
unwrap!(self.xpub_pages.push(npages));
}
bounds bounds
} }

View File

@ -81,13 +81,14 @@ where
}) })
} }
pub fn update_content<F>(&mut self, ctx: &mut EventCtx, update_fn: F) pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where where
F: Fn(&mut T), F: Fn(&mut T) -> R,
{ {
self.content.mutate(ctx, |ctx, c| { self.content.mutate(ctx, |ctx, c| {
update_fn(c); let res = update_fn(c);
c.request_complete_repaint(ctx) c.request_complete_repaint(ctx);
res
}) })
} }
} }

View File

@ -8,7 +8,7 @@ use crate::ui::{
use super::{theme, ScrollBar, Swipe, SwipeDirection}; use super::{theme, ScrollBar, Swipe, SwipeDirection};
const SCROLLBAR_HEIGHT: i16 = 32; const SCROLLBAR_HEIGHT: i16 = 18;
pub struct HorizontalPage<T> { pub struct HorizontalPage<T> {
content: T, content: T,

View File

@ -358,7 +358,7 @@ where
impl<T> ComponentMsgObj for AddressDetails<T> impl<T> ComponentMsgObj for AddressDetails<T>
where where
T: ParagraphStrType, T: ParagraphStrType + Clone,
{ {
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
Ok(CANCELLED.as_obj()) Ok(CANCELLED.as_obj())

View File

@ -413,7 +413,7 @@ async def show_address(
def xpub_title(i: int): def xpub_title(i: int):
result = f"MULTISIG XPUB #{i + 1}\n" result = f"MULTISIG XPUB #{i + 1}\n"
result += " (YOURS)" if i == multisig_index else " (COSIGNER)" result += "(YOURS)" if i == multisig_index else "(COSIGNER)"
return result return result
result = await interact( result = await interact(
@ -430,8 +430,7 @@ async def show_address(
"show_address_details", "show_address_details",
ButtonRequestType.Address, ButtonRequestType.Address,
) )
# Can only go back from the address details but corner button returns INFO. assert result is CANCELLED
assert result in (INFO, CANCELLED)
else: else:
result = await interact( result = await interact(

View File

@ -277,7 +277,7 @@ def test_show_multisig_xpubs(
def input_flow(): def input_flow():
yield # show address yield # show address
layout = client.debug.wait_layout() # TODO: do not need to *wait* now? layout = client.debug.wait_layout() # TODO: do not need to *wait* now?
assert layout.get_title() == "RECEIVE ADDRESS (MULTISIG)" assert "RECEIVE ADDRESS (MULTISIG)" in layout.get_title()
assert layout.get_content().replace(" ", "") == address assert layout.get_content().replace(" ", "") == address
client.debug.click(CORNER_BUTTON) client.debug.click(CORNER_BUTTON)
@ -290,16 +290,10 @@ def test_show_multisig_xpubs(
assert "Multisig 2 of 3" in layout.text assert "Multisig 2 of 3" in layout.text
# Three xpub pages with the same testing logic # Three xpub pages with the same testing logic
for xpub_num in range(3): for xpub_num in range(6):
expected_title = f"MULTISIG XPUB #{xpub_num + 1} " + (
"(YOURS)" if i == xpub_num else "(COSIGNER)"
)
client.debug.swipe_left() client.debug.swipe_left()
layout = client.debug.wait_layout() layout = client.debug.wait_layout()
assert layout.get_title() == expected_title assert "MULTISIG XPUB" in layout.get_title()
content = layout.get_content().replace(" ", "")
assert xpubs[xpub_num] in content
client.debug.click(CORNER_BUTTON) client.debug.click(CORNER_BUTTON)
yield # show address yield # show address