refactor(core): integrate new drawing library

pull/3662/head
cepetr 2 months ago
parent 407c049eb1
commit 936ed18e48

@ -741,6 +741,7 @@ def cargo_build():
features.append('universal_fw')
features.append('ui')
features.append('translations')
features.append('new_rendering')
if PYOPT == '0':
features.append('debug')
features.append('ui_debug')

@ -830,6 +830,7 @@ def cargo_build():
features.append('universal_fw')
features.append('ui')
features.append('translations')
features.append('new_rendering')
if PYOPT == '0':
features.append('debug')
if DMA2D:

@ -21,6 +21,7 @@ ui_bounds = []
ui_antialiasing = []
ui_blurring = []
ui_jpeg_decoder = []
new_rendering = []
bootloader = []
button = []
touch = []

@ -8,6 +8,7 @@ use crate::{
component::{maybe::PaintOverlapping, MsgMap},
display::{self, Color},
geometry::{Offset, Rect},
shape::Renderer,
},
};
@ -60,6 +61,8 @@ pub trait Component {
/// the `Child` wrapper.
fn paint(&mut self);
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>);
#[cfg(feature = "ui_bounds")]
/// Report current paint bounds of this component. Used for debugging.
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
@ -153,6 +156,10 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.component.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.component.bounds(sink)
@ -253,6 +260,10 @@ where
self.inner.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.inner.bounds(sink)
@ -291,6 +302,11 @@ where
self.1.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.0.render(target);
self.1.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.0.bounds(sink);
@ -340,6 +356,12 @@ where
self.2.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.0.render(target);
self.1.render(target);
self.2.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.0.bounds(sink);
@ -367,6 +389,12 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(ref c) = self {
c.render(target)
}
}
fn place(&mut self, bounds: Rect) -> Rect {
match self {
Some(ref mut c) => c.place(bounds),

@ -1,5 +1,8 @@
use super::{Component, Event, EventCtx};
use crate::ui::geometry::{Insets, Rect};
use crate::ui::{
geometry::{Insets, Rect},
shape::Renderer,
};
pub struct Border<T> {
border: Insets,
@ -39,6 +42,10 @@ where
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.inner.bounds(sink);

@ -3,7 +3,8 @@ use crate::{
ui::{
component::{Component, Event, EventCtx, Never, Pad},
display::{self, Color, Font},
geometry::{Offset, Rect},
geometry::{Alignment, Offset, Rect},
shape::{self, Renderer},
},
};
@ -55,6 +56,20 @@ impl Component for Connect {
)
});
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let font = Font::NORMAL;
self.bg.render(target);
self.message.map(|t| {
shape::Text::new(self.bg.area.center() + Offset::y(font.text_height() / 2), t)
.with_fg(self.fg)
.with_font(font)
.with_align(Alignment::Center)
.render(target);
});
}
}
#[cfg(feature = "micropython")]

@ -1,5 +1,5 @@
use super::{Component, Event, EventCtx, Never};
use crate::ui::geometry::Rect;
use crate::ui::{geometry::Rect, shape::Renderer};
pub struct Empty;
@ -15,6 +15,8 @@ impl Component for Empty {
}
fn paint(&mut self) {}
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
}
#[cfg(feature = "ui_debug")]

@ -6,6 +6,8 @@ use crate::ui::{
Color, Icon,
},
geometry::{Alignment2D, Offset, Point, Rect},
shape,
shape::Renderer,
};
#[derive(PartialEq, Eq, Clone, Copy)]
@ -48,6 +50,12 @@ impl Component for Image {
self.draw(self.area.center(), Alignment2D::CENTER);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
shape::ToifImage::new(self.area.center(), self.toif)
.with_align(Alignment2D::CENTER)
.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(Rect::from_center_and_size(
@ -130,6 +138,15 @@ impl Component for BlendedImage {
self.paint_image();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
shape::ToifImage::new(self.bg_top_left, self.bg.toif)
.with_fg(self.bg_color)
.render(target);
shape::ToifImage::new(self.bg_top_left + self.fg_offset, self.fg.toif)
.with_fg(self.fg_color)
.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(Rect::from_top_left_and_size(

@ -2,6 +2,7 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::Font,
geometry::{Alignment, Insets, Offset, Point, Rect},
shape::Renderer,
};
use super::{text::TextStyle, TextLayout};
@ -119,6 +120,10 @@ where
self.layout.render_text(self.text.as_ref());
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.layout.render_text2(self.text.as_ref(), target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.layout.bounds)

@ -1,5 +1,5 @@
use super::{Component, Event, EventCtx};
use crate::ui::geometry::Rect;
use crate::ui::{geometry::Rect, shape::Renderer};
pub struct MsgMap<T, F> {
inner: T,
@ -31,6 +31,10 @@ where
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.inner.bounds(sink);

@ -3,9 +3,9 @@ use crate::{
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Never, TimerToken},
display,
display::{Color, Font},
geometry::Rect,
display::{self, Color, Font},
geometry::{Offset, Rect},
shape::{self, Renderer},
util::animation_disabled,
},
};
@ -131,6 +131,17 @@ where
self.bg,
);
}
pub fn render_anim<'s>(&'s self, target: &mut impl Renderer<'s>, offset: i16) {
target.in_window(self.area, &mut |target| {
let text_height = self.font.text_height();
let pos = self.area.top_left() + Offset::new(offset, text_height - 1);
shape::Text::new(pos, self.text.as_ref())
.with_font(self.font)
.with_fg(self.fg)
.render(target);
});
}
}
impl<T> Component for Marquee<T>
@ -225,6 +236,30 @@ where
}
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let now = Instant::now();
match self.state {
State::Initial => {
self.render_anim(target, 0);
}
State::PauseRight => {
self.render_anim(target, self.min_offset);
}
State::PauseLeft => {
self.render_anim(target, self.max_offset);
}
_ => {
let progress = self.progress(now);
if let Some(done) = progress {
self.render_anim(target, done);
} else {
self.render_anim(target, 0);
}
}
}
}
}
#[cfg(feature = "ui_debug")]

@ -2,6 +2,7 @@ use crate::ui::{
component::{Component, ComponentExt, Event, EventCtx, Pad},
display::{self, Color},
geometry::Rect,
shape::Renderer,
};
pub struct Maybe<T> {
@ -94,6 +95,13 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
if self.visible {
self.inner.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);

@ -1,6 +1,8 @@
use crate::ui::{
display::{self, Color},
geometry::Rect,
shape,
shape::Renderer,
};
pub struct Pad {
@ -52,4 +54,10 @@ impl Pad {
display::rect_fill(self.area, self.color);
}
}
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
shape::Bar::new(self.area)
.with_bg(self.color)
.render(target);
}
}

@ -4,6 +4,8 @@ use crate::ui::{
component::{image::Image, Component, Event, EventCtx, Never},
display,
geometry::{Alignment2D, Rect},
shape,
shape::Renderer,
};
pub struct Painter<F> {
@ -39,6 +41,15 @@ where
(self.func)(self.area);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let area = self.area;
shape::Bar::new(area)
.with_thickness(1)
.with_fg(display::Color::white())
.render(target);
shape::Text::new(area.top_left(), "Paint").render(target); // !@# replace
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
@ -64,7 +75,10 @@ pub fn jpeg_painter<'a>(
scale: u8,
) -> Painter<impl FnMut(Rect)> {
let off = Offset::new(size.x / (2 << scale), size.y / (2 << scale));
#[cfg(not(feature = "new_rendering"))]
let f = move |area: Rect| display::tjpgd::jpeg(image(), area.center() - off, scale);
#[cfg(feature = "new_rendering")]
let f = move |area: Rect| {};
Painter::new(f)
}

@ -1,6 +1,7 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Alignment, Alignment2D, Axis, Grid, GridCellSpan, Insets, Offset, Rect},
shape::Renderer,
};
pub struct GridPlaced<T> {
@ -63,6 +64,10 @@ where
fn paint(&mut self) {
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
}
#[cfg(feature = "ui_debug")]
@ -106,6 +111,10 @@ where
fn paint(&mut self) {
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
}
#[cfg(feature = "ui_debug")]
@ -178,6 +187,10 @@ where
fn paint(&mut self) {
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target);
}
}
#[cfg(feature = "ui_debug")]
@ -269,6 +282,11 @@ where
self.first.paint();
self.second.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.first.render(target);
self.second.render(target);
}
}
#[cfg(feature = "ui_debug")]

@ -8,6 +8,8 @@ use crate::{
constant,
display::{pixeldata, pixeldata_dirty, rect_fill_rounded, set_window, Color},
geometry::{Insets, Offset, Rect},
shape,
shape::Renderer,
},
};
@ -141,6 +143,39 @@ impl Component for Qr {
Self::draw(&qr, area, self.border, scale);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let mut outbuffer = [0u8; QR_MAX_VERSION.buffer_len()];
let mut tempbuffer = [0u8; QR_MAX_VERSION.buffer_len()];
let qr = QrCode::encode_text(
self.text.as_ref(),
&mut tempbuffer,
&mut outbuffer,
QrCodeEcc::Medium,
Version::MIN,
QR_MAX_VERSION,
None,
true,
);
let qr = unwrap!(qr);
let scale = (self.area.width().min(self.area.height()) - self.border) / (qr.size() as i16);
let side = scale * qr.size() as i16;
let qr_area = Rect::from_center_and_size(self.area.center(), Offset::uniform(side));
if self.border > 0 {
shape::Bar::new(qr_area.expand(self.border))
.with_bg(LIGHT)
.with_radius(CORNER_RADIUS as i16 + 1) // !@# + 1 to fix difference on TR
.render(target);
}
shape::QrImage::new(qr_area, &qr)
.with_fg(LIGHT)
.with_bg(DARK)
.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)

@ -3,11 +3,12 @@ use crate::{
ui::{
component::{Component, Event, EventCtx, Never, Paginate},
geometry::{Alignment, Offset, Rect},
shape::Renderer,
},
};
use super::{
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer},
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer, TextRenderer2},
op::OpTextLayout,
};
@ -136,6 +137,10 @@ impl<T: StringType + Clone> Component for FormattedText<T> {
self.layout_content(&mut TextRenderer);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.layout_content(&mut TextRenderer2::new(target));
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.op_layout.layout.bounds)

@ -2,6 +2,8 @@ use crate::ui::{
display,
display::{toif::Icon, Color, Font, GlyphMetrics},
geometry::{Alignment, Alignment2D, Dimensions, Offset, Point, Rect},
shape,
shape::Renderer,
};
const ELLIPSIS: &str = "...";
@ -235,6 +237,15 @@ impl TextLayout {
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer)
}
/// Draw as much text as possible on the current screen.
pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit {
self.layout_text(
text,
&mut self.initial_cursor(),
&mut TextRenderer2::new(target),
)
}
/// Loop through the `text` and try to fit it on the current screen,
/// reporting events to `sink`, which may do something with them (e.g. draw
/// on screen).
@ -530,6 +541,67 @@ impl LayoutSink for TextRenderer {
}
}
pub struct TextRenderer2<'a, 's, R>(pub &'a mut R, core::marker::PhantomData<&'s ()>)
where
R: Renderer<'s>;
impl<'a, 's, R> TextRenderer2<'a, 's, R>
where
R: Renderer<'s>,
{
pub fn new(target: &'a mut R) -> Self {
Self(target, core::marker::PhantomData)
}
}
impl<'a, 's, R> LayoutSink for TextRenderer2<'a, 's, R>
where
R: Renderer<'s>,
{
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) {
shape::Text::new(cursor, text)
.with_font(layout.style.text_font)
.with_fg(layout.style.text_color)
.render(self.0);
}
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
shape::Text::new(cursor, "-")
.with_font(layout.style.text_font)
.with_fg(layout.style.hyphen_color)
.render(self.0);
}
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
if let Some((icon, margin)) = layout.style.ellipsis_icon {
let bottom_left = cursor + Offset::x(margin);
shape::ToifImage::new(bottom_left, icon.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(layout.style.ellipsis_color)
.render(self.0);
} else {
shape::Text::new(cursor, ELLIPSIS)
.with_font(layout.style.text_font)
.with_fg(layout.style.ellipsis_color)
.render(self.0);
}
}
fn prev_page_ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
if let Some((icon, _margin)) = layout.style.prev_page_ellipsis_icon {
shape::ToifImage::new(cursor, icon.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(layout.style.ellipsis_color)
.render(self.0);
} else {
shape::Text::new(cursor, ELLIPSIS)
.with_font(layout.style.text_font)
.with_fg(layout.style.ellipsis_color)
.render(self.0);
}
}
}
#[cfg(feature = "ui_debug")]
pub mod trace {
use crate::{trace::ListTracer, ui::geometry::Point};

@ -8,6 +8,8 @@ use crate::{
geometry::{
Alignment, Alignment2D, Dimensions, Insets, LinearPlacement, Offset, Point, Rect,
},
shape,
shape::Renderer,
},
};
@ -188,6 +190,17 @@ where
)
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
Self::foreach_visible(
&self.source,
&self.visible,
self.offset,
&mut |layout, content| {
layout.render_text2(content, target);
},
)
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);
@ -606,6 +619,19 @@ impl<T> Checklist<T> {
layout.style.background_color,
);
}
fn render_icon<'s>(
&self,
layout: &TextLayout,
icon: Icon,
offset: Offset,
target: &mut impl Renderer<'s>,
) {
let top_left = Point::new(self.area.x0, layout.bounds.y0);
shape::ToifImage::new(top_left + offset, icon.toif)
.with_fg(layout.style.text_color)
.render(target);
}
}
impl<T> Component for Checklist<T>
@ -645,6 +671,28 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.paragraphs.render(target);
let current_visible = self.current.saturating_sub(self.paragraphs.offset.par);
for layout in self.paragraphs.visible.iter().take(current_visible) {
self.render_icon(
&layout.layout(&self.paragraphs.source),
self.icon_done,
self.done_offset,
target,
);
}
if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) {
self.render_icon(
&layout.layout(&self.paragraphs.source),
self.icon_current,
self.current_offset,
target,
);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);

@ -3,6 +3,7 @@ use crate::{
ui::{
display::{Color, Font},
geometry::{Alignment, Rect},
shape::Renderer,
},
};
@ -37,6 +38,33 @@ pub fn text_multiline(
}
}
/// Draws longer multiline texts inside an area.
/// Splits lines on word boundaries/whitespace.
/// When a word is too long to fit one line, splitting
/// it on multiple lines with "-" at the line-ends.
///
/// If it fits, returns the rest of the area.
/// If it does not fit, returns `None`.
pub fn text_multiline2<'s>(
target: &mut impl Renderer<'s>,
area: Rect,
text: TString<'_>,
font: Font,
fg_color: Color,
bg_color: Color,
alignment: Alignment,
) -> Option<Rect> {
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
let text_layout = TextLayout::new(text_style)
.with_bounds(area)
.with_align(alignment);
let layout_fit = text.map(|t| text_layout.render_text2(t, target));
match layout_fit {
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
LayoutFit::OutOfBounds { .. } => None,
}
}
/// Same as `text_multiline` above, but aligns the text to the bottom of the
/// area.
pub fn text_multiline_bottom(
@ -66,3 +94,34 @@ pub fn text_multiline_bottom(
}
})
}
/// Same as `text_multiline` above, but aligns the text to the bottom of the
/// area.
pub fn text_multiline_bottom2<'s>(
target: &mut impl Renderer<'s>,
area: Rect,
text: TString<'_>,
font: Font,
fg_color: Color,
bg_color: Color,
alignment: Alignment,
) -> Option<Rect> {
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
let mut text_layout = TextLayout::new(text_style)
.with_bounds(area)
.with_align(alignment);
// When text fits the area, displaying it in the bottom part.
// When not, render it "normally".
text.map(|t| match text_layout.fit_text(t) {
LayoutFit::Fitting { height, .. } => {
let (top, bottom) = area.split_bottom(height);
text_layout = text_layout.with_bounds(bottom);
text_layout.render_text2(t, target);
Some(top)
}
LayoutFit::OutOfBounds { .. } => {
text_layout.render_text2(t, target);
None
}
})
}

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{Component, Event, EventCtx, TimerToken},
geometry::Rect,
shape::Renderer,
},
};
@ -44,6 +45,8 @@ impl Component for Timeout {
}
fn paint(&mut self) {}
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
}
#[cfg(feature = "ui_debug")]

@ -24,6 +24,7 @@ pub fn jpeg(data: &[u8], pos: Point, scale: u8) {
}
}
#[cfg(not(feature = "new_rendering"))]
pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> {
let mut buffer = BufferJpegWork::get_cleared();
let pool = buffer.buffer.as_mut_slice();
@ -40,6 +41,84 @@ pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> {
result
}
#[cfg(feature = "new_rendering")]
pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> {
const M_SOI: u16 = 0xFFD8;
const M_SOF0: u16 = 0xFFC0;
const M_DRI: u16 = 0xFFDD;
const M_RST0: u16 = 0xFFD0;
const M_RST7: u16 = 0xFFD7;
const M_SOS: u16 = 0xFFDA;
const M_EOI: u16 = 0xFFD9;
let mut result = None;
let mut ofs = 0;
let read_u16 = |ofs| -> Option<u16> {
if ofs + 1 < data.len() {
let result = Some(((data[ofs] as u16) << 8) + data[ofs + 1] as u16);
result
} else {
None
}
};
let read_u8 = |ofs| -> Option<u8> {
if ofs < data.len() {
let result = Some(data[ofs]);
result
} else {
None
}
};
while ofs < data.len() {
if read_u16(ofs)? == M_SOI {
break;
}
ofs += 1;
}
loop {
let marker = read_u16(ofs)?;
if (marker & 0xFF00) != 0xFF00 {
return None;
}
ofs += 2;
ofs += match marker {
M_SOI => 0,
M_SOF0 => {
let w = read_u16(ofs + 3)? as i16;
let h = read_u16(ofs + 5)? as i16;
// Number of components
let nc = read_u8(ofs + 7)?;
if (nc != 1) && (nc != 3) {
return None;
}
// Sampling factor of the first component
let c1 = read_u8(ofs + 9)?;
if (c1 != 0x11) && (c1 != 0x21) & (c1 != 0x22) {
return None;
};
let mcu_height = (8 * (c1 & 15)) as i16;
result = Some((Offset::new(w, h), mcu_height));
read_u16(ofs)?
}
M_DRI => 4,
M_EOI => return None,
M_RST0..=M_RST7 => 0,
M_SOS => break,
_ => read_u16(ofs)?,
} as usize;
}
result
}
pub fn jpeg_test(data: &[u8]) -> bool {
let mut buffer = BufferJpegWork::get_cleared();
let pool = buffer.buffer.as_mut_slice();

@ -19,8 +19,9 @@ use crate::{
ui::{
component::{Component, Event, EventCtx, Never, Root, TimerToken},
constant,
display::sync,
display::{sync, Color},
geometry::Rect,
shape::render_on_display,
},
};
@ -69,9 +70,26 @@ where
}
fn obj_paint(&mut self) -> bool {
let will_paint = self.inner().will_paint();
self.paint();
will_paint
#[cfg(not(feature = "new_rendering"))]
let legacy_mode = true;
#[cfg(feature = "new_rendering")]
let legacy_mode = false;
if legacy_mode {
let will_paint = self.inner().will_paint();
self.paint();
will_paint
} else {
let will_paint = self.inner().will_paint();
if will_paint {
render_on_display(None, Some(Color::black()), |target| {
self.render(target);
});
self.skip_paint();
}
will_paint
}
}
#[cfg(feature = "ui_bounds")]

@ -2,6 +2,8 @@ use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
geometry::{Alignment, Alignment2D, Rect},
layout::simplified::ReturnToC,
shape,
shape::Renderer,
};
use super::super::{
@ -101,6 +103,27 @@ impl<'a> Component for Intro<'a> {
self.buttons.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
let area = self.bg.area;
shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(BLD_FG)
.render(target);
shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif)
.with_align(Alignment2D::TOP_RIGHT)
.with_fg(BLD_FG)
.render(target);
self.warn.render(target);
self.text.render(target);
self.buttons.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.title.bounds(sink);

@ -7,8 +7,10 @@ use crate::{
constant::screen,
display,
display::{Font, Icon},
geometry::{Alignment2D, Offset, Point, Rect},
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
layout::simplified::ReturnToC,
shape,
shape::Renderer,
},
};
@ -69,6 +71,26 @@ impl Choice for MenuChoice {
);
}
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool) {
// Icon on top and two lines of text below
shape::ToifImage::new(SCREEN_CENTER + Offset::y(-20), self.icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(BLD_FG)
.render(target);
shape::Text::new(SCREEN_CENTER, self.first_line)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
shape::Text::new(SCREEN_CENTER + Offset::y(10), self.second_line)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
}
fn btn_layout(&self) -> ButtonLayout {
ButtonLayout::arrow_armed_arrow("SELECT".into())
}
@ -162,6 +184,11 @@ impl Component for Menu {
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.choice_page.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.choice_page.bounds(sink)

@ -1,7 +1,9 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display::{self, Font},
geometry::{Offset, Rect},
geometry::{Alignment, Offset, Rect},
shape,
shape::Renderer,
};
use super::super::theme::bootloader::{BLD_BG, BLD_FG};
@ -57,4 +59,28 @@ impl Component for Welcome {
BLD_BG,
);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
let top_center = self.bg.area.top_center();
shape::Text::new(top_center + Offset::y(24), "Get started with")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
shape::Text::new(top_center + Offset::y(32), "your Trezor at")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
shape::Text::new(top_center + Offset::y(48), "trezor.io/start")
.with_align(Alignment::Center)
.with_font(Font::BOLD)
.with_fg(BLD_FG)
.render(target);
}
}

@ -10,6 +10,7 @@ use crate::{
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
},
geometry::Rect,
shape::Renderer,
},
};
@ -256,6 +257,16 @@ impl Component for AddressDetails {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.buttons.render(target);
match self.current_page {
0 => self.qr_code.render(target),
1 => self.details_view.render(target),
_ => self.xpub_view.render(target),
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)

@ -4,6 +4,8 @@ use crate::{
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
display::{self, Color, Font},
geometry::{Point, Rect},
shape,
shape::Renderer,
},
};
@ -227,6 +229,32 @@ where
self.buttons.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
let mut display_top_left = |text: TString<'static>| {
text.map(|t| {
shape::Text::new(Point::zero(), t)
.with_font(Font::BOLD)
.with_fg(WHITE)
.render(target);
});
};
// We are either on the info screen or on the "main" screen
if self.showing_info_screen {
if let Some(title) = self.info_title {
display_top_left(title);
}
self.info_text.render(target);
} else {
display_top_left(self.title);
self.message.render(target);
self.alert.render(target);
}
self.buttons.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.buttons.bounds(sink);

@ -7,6 +7,8 @@ use crate::{
display::{self, Color, Font, Icon},
event::PhysicalButton,
geometry::{Alignment2D, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
@ -265,6 +267,88 @@ impl Component for Button {
}
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style();
let fg_color = style.text_color;
let bg_color = fg_color.negate();
let area = self.get_current_area();
let inversed_colors = bg_color != theme::BG;
// Filling the background (with 2-pixel rounding when applicable)
if inversed_colors {
shape::Bar::new(area)
.with_radius(3)
.with_bg(bg_color)
.render(target);
} else if style.with_outline {
shape::Bar::new(area)
.with_radius(3)
.with_fg(fg_color)
.render(target);
} else {
shape::Bar::new(area).with_bg(bg_color).render(target);
}
// Optionally display "arms" at both sides of content - always in FG and BG
// colors (they are not inverted).
if style.with_arms {
shape::ToifImage::new(area.left_center(), theme::ICON_ARM_LEFT.toif)
.with_align(Alignment2D::TOP_RIGHT)
.with_fg(theme::FG)
.render(target);
shape::ToifImage::new(area.right_center(), theme::ICON_ARM_RIGHT.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(theme::FG)
.render(target);
}
// Painting the content
match &self.content {
ButtonContent::Text(text) => text.map(|t| {
shape::Text::new(
self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)),
t,
)
.with_font(style.font)
.with_fg(fg_color)
.render(target);
}),
ButtonContent::Icon(icon) => {
// Allowing for possible offset of the area from current style
let icon_area = area.translate(style.offset);
if style.with_outline {
shape::ToifImage::new(icon_area.center(), icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(fg_color)
.render(target);
} else {
// Positioning the icon in the corresponding corner/center
match self.pos {
ButtonPos::Left => {
shape::ToifImage::new(icon_area.bottom_left(), icon.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(fg_color)
.render(target)
}
ButtonPos::Right => {
shape::ToifImage::new(icon_area.bottom_right(), icon.toif)
.with_align(Alignment2D::BOTTOM_RIGHT)
.with_fg(fg_color)
.render(target)
}
ButtonPos::Middle => shape::ToifImage::new(icon_area.center(), icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(fg_color)
.render(target),
}
}
}
}
}
}
#[derive(PartialEq, Eq)]

@ -7,6 +7,7 @@ use crate::{
component::{base::Event, Component, EventCtx, Pad, TimerToken},
event::{ButtonEvent, PhysicalButton},
geometry::Rect,
shape::Renderer,
},
};
@ -93,6 +94,18 @@ impl ButtonType {
Self::Nothing => {}
}
}
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
match self {
Self::Button(button) => {
button.render(target);
}
Self::HoldToConfirm(htc) => {
htc.render(target);
}
Self::Nothing => {}
}
}
}
/// Wrapping a button and its state, so that it can be easily
@ -154,6 +167,10 @@ impl ButtonContainer {
self.button_type.paint();
}
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.button_type.render(target);
}
/// Setting the visual state of the button - released/pressed.
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
if let ButtonType::Button(btn) = &mut self.button_type {
@ -575,6 +592,13 @@ impl Component for ButtonController {
self.right_btn.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.left_btn.render(target);
self.middle_btn.render(target);
self.right_btn.render(target);
}
fn place(&mut self, bounds: Rect) -> Rect {
// Saving button area so that we can re-place the buttons
// when they get updated
@ -754,6 +778,8 @@ impl Component for AutomaticMover {
fn paint(&mut self) {}
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Moving automatically only when we receive a TimerToken that we have
// requested before

@ -2,6 +2,8 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display::Font,
geometry::{Alignment, Point, Rect},
shape,
shape::Renderer,
util::long_line_content_with_ellipsis,
};
@ -109,16 +111,39 @@ where
common::display_left(baseline, &self.text, self.font);
}
fn render_left<'s>(&'s self, target: &mut impl Renderer<'s>) {
let baseline = Point::new(self.pad.area.x0, self.y_baseline());
shape::Text::new(baseline, &self.text.as_ref())
.with_font(self.font)
.render(target);
}
fn paint_center(&self) {
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
common::display_center(baseline, &self.text, self.font);
}
fn render_center<'s>(&'s self, target: &mut impl Renderer<'s>) {
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
shape::Text::new(baseline, &self.text.as_ref())
.with_align(Alignment::Center)
.with_font(self.font)
.render(target);
}
fn paint_right(&self) {
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
common::display_right(baseline, &self.text, self.font);
}
fn render_right<'s>(&'s self, target: &mut impl Renderer<'s>) {
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
shape::Text::new(baseline, &self.text.as_ref())
.with_align(Alignment::End)
.with_font(self.font)
.render(target);
}
fn paint_long_content_with_ellipsis(&self) {
let text_to_display = long_line_content_with_ellipsis(
self.text.as_ref(),
@ -175,4 +200,20 @@ where
}
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
if self.show_content {
// In the case text cannot fit, show ellipsis and its right part
if !self.text_fits_completely() {
self.paint_long_content_with_ellipsis();
} else {
match self.alignment {
Alignment::Start => self.render_left(target),
Alignment::Center => self.render_center(target),
Alignment::End => self.render_right(target),
}
}
}
}
}

@ -6,11 +6,16 @@ use crate::{
ui::{
component::{
base::Never,
text::util::{text_multiline, text_multiline_bottom},
text::util::{
text_multiline, text_multiline2, text_multiline_bottom, text_multiline_bottom2,
},
Component, Event, EventCtx,
},
display::{self, Font},
geometry::{Alignment, Insets, Rect},
geometry::{Alignment, Insets, Offset, Rect},
model_tr::cshape,
shape,
shape::Renderer,
util::animation_disabled,
},
};
@ -130,6 +135,55 @@ where
);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// TOP
let center = self.area.center() + Offset::y(self.loader_y_offset);
if self.indeterminate {
text_multiline2(
target,
self.area,
TR::coinjoin__title_progress.into(),
Font::BOLD,
theme::FG,
theme::BG,
Alignment::Center,
);
cshape::LoaderSmall::new(center, self.value)
.with_color(theme::FG)
.render(target);
} else {
cshape::LoaderCircular::new(center, self.value)
.with_color(theme::FG)
.render(target);
shape::ToifImage::new(center, theme::ICON_TICK_FAT.toif)
.with_fg(theme::FG)
.render(target);
}
// BOTTOM
let top_rest = text_multiline_bottom2(
target,
self.area,
TR::coinjoin__do_not_disconnect.into(),
Font::BOLD,
theme::FG,
theme::BG,
Alignment::Center,
);
if let Some(rest) = top_rest {
text_multiline_bottom2(
target,
rest.inset(Insets::bottom(FOOTER_TEXT_MARGIN)),
self.text.as_ref().into(),
Font::NORMAL,
theme::FG,
theme::BG,
Alignment::Center,
);
}
}
}
#[cfg(feature = "ui_debug")]

@ -3,6 +3,9 @@ use crate::ui::{
constant::{screen, WIDTH},
display,
geometry::{Alignment2D, Offset, Point, Rect},
model_tr::cshape,
shape,
shape::Renderer,
};
use super::super::{
@ -95,4 +98,29 @@ impl<T: AsRef<str>> Component for ErrorScreen<T> {
self.footer.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
if self.show_icons {
shape::ToifImage::new(screen().top_left(), theme::ICON_WARN_TITLE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(FG)
.render(target);
shape::ToifImage::new(screen().top_right(), theme::ICON_WARN_TITLE.toif)
.with_align(Alignment2D::TOP_RIGHT)
.with_fg(FG)
.render(target);
}
self.title.render(target);
self.message.render(target);
cshape::HorizontalLine::new(Point::new(0, DIVIDER_POSITION), WIDTH)
.with_step(3)
.with_color(FG)
.render(target);
self.footer.render(target);
}
}

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
geometry::Rect,
shape::Renderer,
},
};
@ -308,6 +309,23 @@ where
// (and painting buttons last would cover the lower part).
self.current_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
// Scrollbars are painted only with a title and when requested
if self.title.is_some() {
if self.show_scrollbar {
self.scrollbar.render(target);
}
self.title.render(target);
}
self.buttons.render(target);
// On purpose painting current page at the end, after buttons,
// because we sometimes (in the case of QR code) need to use the
// whole height of the display for showing the content
// (and painting buttons last would cover the lower part).
self.current_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{base::Component, FormattedText, Paginate},
geometry::Rect,
shape::Renderer,
},
};
@ -132,6 +133,10 @@ where
self.formatted.paint();
}
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.formatted.render(target);
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.formatted.place(bounds);
self.page_count = self.page_count();

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Paginate},
geometry::{Insets, Rect},
shape::Renderer,
},
};
@ -83,6 +84,11 @@ where
self.title.paint();
self.content.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
self.content.render(target);
}
}
impl<T, U> Paginate for Frame<T, U>
@ -204,6 +210,12 @@ where
self.scrollbar.paint();
self.content.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
self.scrollbar.render(target);
self.content.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -5,6 +5,7 @@ use crate::{
component::{Component, Event, EventCtx},
event::ButtonEvent,
geometry::Rect,
shape::Renderer,
},
};
@ -122,6 +123,10 @@ impl Component for HoldToConfirm {
fn paint(&mut self) {
self.loader.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.loader.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -1,6 +1,10 @@
use crate::{
error::Error,
micropython::buffer::StrBuffer,
micropython::{
buffer::{get_buffer, StrBuffer},
gc::Gc,
obj::Obj,
},
strutil::StringType,
translations::TR,
trezorhal::usb::usb_configured,
@ -13,8 +17,10 @@ use crate::{
Font, Icon,
},
event::USBEvent,
geometry::{Alignment2D, Insets, Offset, Point, Rect},
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
shape,
shape::Renderer,
},
};
@ -48,6 +54,16 @@ fn paint_default_image() {
);
}
fn render_default_image<'s>(target: &mut impl Renderer<'s>) {
shape::ToifImage::new(
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
theme::ICON_LOGO.toif,
)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.render(target);
}
enum CurrentScreen {
EmptyAtStart,
Homescreen,
@ -62,6 +78,7 @@ where
// always painted, so we need to always paint the label too
label: Label<T>,
notification: Option<(T, u8)>,
custom_image: Option<Gc<[u8]>>,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController>,
/// Holds the loader component
@ -85,6 +102,7 @@ where
Self {
label: Label::centered(label, theme::TEXT_BIG),
notification,
custom_image: get_user_custom_image().ok(),
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
loader,
show_loader: false,
@ -93,8 +111,8 @@ where
}
fn paint_homescreen_image(&self) {
let homescreen_bytes = get_user_custom_image().ok();
let homescreen = homescreen_bytes
let homescreen = self
.custom_image
.as_ref()
.and_then(|data| Toif::new(data.as_ref()).ok())
.filter(check_homescreen_format);
@ -105,6 +123,22 @@ where
}
}
fn render_homescreen_image<'s>(&'s self, target: &mut impl Renderer<'s>) {
let homescreen = self
.custom_image
.as_ref()
.and_then(|data| Toif::new(data.as_ref()).ok())
.filter(check_homescreen_format);
if let Some(toif) = homescreen {
shape::ToifImage::new(TOP_CENTER, toif)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.render(target);
} else {
render_default_image(target);
}
}
fn paint_notification(&self) {
let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height());
if !usb_configured() {
@ -136,6 +170,49 @@ where
}
}
fn render_notification<'s>(&'s self, target: &mut impl Renderer<'s>) {
let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height());
if !usb_configured() {
shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0)
.with_bg(theme::BG)
.render(target);
// TODO: fill warning icons here as well?
TR::homescreen__title_no_usb_connection.map_translated(|t| {
shape::Text::new(baseline, t)
.with_align(Alignment::Center)
.with_font(NOTIFICATION_FONT)
.render(target)
});
} else if let Some((notification, _level)) = &self.notification {
shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0)
.with_bg(theme::BG)
.render(target);
shape::Text::new(baseline, notification.as_ref())
.with_align(Alignment::Center)
.with_font(NOTIFICATION_FONT)
.render(target);
// Painting warning icons in top corners when the text is short enough not to
// collide with them
let icon_width = NOTIFICATION_ICON.toif.width();
let text_width = NOTIFICATION_FONT.text_width(notification.as_ref());
if AREA.width() >= text_width + (icon_width + 1) * 2 {
shape::ToifImage::new(AREA.top_left(), NOTIFICATION_ICON.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(theme::FG)
.with_bg(theme::BG)
.render(target);
shape::ToifImage::new(AREA.top_right(), NOTIFICATION_ICON.toif)
.with_align(Alignment2D::TOP_RIGHT)
.with_fg(theme::FG)
.with_bg(theme::BG)
.render(target);
}
}
}
fn paint_label(&mut self) {
// paint black background to place the label
let mut outset = Insets::uniform(LABEL_OUTSET);
@ -146,6 +223,19 @@ where
self.label.paint();
}
fn render_label<'s>(&'s self, target: &mut impl Renderer<'s>) {
// paint black background to place the label
let mut outset = Insets::uniform(LABEL_OUTSET);
// the margin at top is bigger (caused by text-height vs line-height?)
// compensate by shrinking the outset
outset.top -= 5;
shape::Bar::new(self.label.text_area().outset(outset))
.with_bg(theme::BG)
.render(target);
self.label.render(target);
}
/// So that notification is well visible even on homescreen image
fn fill_notification_background(&self) {
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
@ -236,6 +326,19 @@ where
self.paint_label();
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Redraw the whole screen when the screen changes (loader vs homescreen)
if self.show_loader {
self.loader.render(target);
} else {
// Painting the homescreen image first, as the notification and label
// should be "on top of it"
self.render_homescreen_image(target);
self.render_notification(target);
self.render_label(target);
}
}
}
pub struct Lockscreen<T>
@ -320,35 +423,58 @@ where
)
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if self.screensaver {
// keep screen blank
return;
}
shape::ToifImage::new(
TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN),
theme::ICON_LOCK.toif,
)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.render(target);
self.instruction.render(target);
self.label.render(target);
if let Some(icon) = &self.coinjoin_icon {
shape::ToifImage::new(COINJOIN_CORNER, icon.toif)
.with_align(Alignment2D::TOP_RIGHT)
.with_fg(theme::FG)
.render(target);
}
}
}
pub struct ConfirmHomescreen<T, F>
pub struct ConfirmHomescreen<T>
where
T: StringType,
{
title: Child<Label<T>>,
buffer_func: F,
image: Obj,
buttons: Child<ButtonController>,
}
impl<T, F> ConfirmHomescreen<T, F>
impl<T> ConfirmHomescreen<T>
where
T: StringType + Clone,
{
pub fn new(title: T, buffer_func: F) -> Self {
pub fn new(title: T, image: Obj) -> Self {
let btn_layout = ButtonLayout::cancel_none_text(TR::buttons__change.into());
ConfirmHomescreen {
title: Child::new(Label::centered(title, theme::TEXT_BOLD)),
buffer_func,
image,
buttons: Child::new(ButtonController::new(btn_layout)),
}
}
}
impl<'a, T, F> Component for ConfirmHomescreen<T, F>
impl<'a, T> Component for ConfirmHomescreen<T>
where
T: StringType + Clone,
F: Fn() -> &'a [u8],
{
type Msg = CancelConfirmMsg;
@ -375,11 +501,13 @@ where
fn paint(&mut self) {
// Drawing the image full-screen first and then other things on top
let buffer = (self.buffer_func)();
if buffer.is_empty() {
// SAFETY: We expect no existing mutable reference. Resulting reference is
// discarded before returning to micropython.
let image_data = unwrap!(unsafe { get_buffer(self.image) });
if image_data.is_empty() {
paint_default_image();
} else {
let toif_data = unwrap!(Toif::new(buffer));
let toif_data = unwrap!(Toif::new(image_data));
toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
};
// Need to make all the title background black, so the title text is well
@ -389,6 +517,32 @@ where
self.title.paint();
self.buttons.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Drawing the image full-screen first and then other things on top
// SAFETY: We expect no existing mutable reference. Resulting reference is
// discarded before returning to micropython.
let image_data = unwrap!(unsafe { get_buffer(self.image) });
if image_data.is_empty() {
render_default_image(target);
} else {
let toif_data = unwrap!(Toif::new(image_data));
shape::ToifImage::new(TOP_CENTER, toif_data)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.render(target);
};
// Need to make all the title background black, so the title text is well
// visible
let title_area = self.title.inner().area();
shape::Bar::new(title_area)
.with_bg(theme::BG)
.render(target);
self.title.render(target);
self.buttons.render(target);
}
}
pub fn check_homescreen_format(toif: &Toif) -> bool {
@ -420,7 +574,7 @@ where
}
#[cfg(feature = "ui_debug")]
impl<T, F> crate::trace::Trace for ConfirmHomescreen<T, F>
impl<T> crate::trace::Trace for ConfirmHomescreen<T>
where
T: StringType,
{

@ -1,6 +1,7 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
shape::Renderer,
util::animation_disabled,
};
@ -14,11 +15,17 @@ pub trait Choice {
// Only `paint_center` is required, the rest is optional
// and therefore has a default implementation.
fn paint_center(&self, area: Rect, inverse: bool);
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool);
fn width_center(&self) -> i16 {
0
}
fn paint_side(&self, _area: Rect) {}
fn render_side<'s>(&self, _target: &mut impl Renderer<'s>, _area: Rect) {}
fn width_side(&self) -> i16 {
0
}
@ -248,6 +255,43 @@ where
}
}
/// Display current, previous and next choices according to
/// the current ChoiceItem.
fn render_choices<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Getting the row area for the choices - so that displaying
// items in the used font will show them in the middle vertically.
let area_height_half = self.pad.area.height() / 2;
let font_size_half = theme::FONT_CHOICE_ITEMS.visible_text_height("Ay") / 2;
let center_row_area = self
.pad
.area
.split_top(area_height_half)
.0
.outset(Insets::bottom(font_size_half));
// Drawing the current item in the middle.
self.show_current_choice2(target, center_row_area);
// Not drawing the rest when not wanted
if self.show_only_one_item {
return;
}
// Getting the remaining left and right areas.
let center_width = self.get_current_item().width_center();
let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
// Possibly drawing on the left side.
if self.has_previous_choice() || self.is_carousel {
self.show_left_choices2(target, left_area);
}
// Possibly drawing on the right side.
if self.has_next_choice() || self.is_carousel {
self.show_right_choices2(target, right_area);
}
}
/// Setting current buttons, and clearing.
fn update(&mut self, ctx: &mut EventCtx) {
self.set_buttons(ctx);
@ -296,6 +340,12 @@ where
.paint_center(area, self.inverse_selected_item);
}
/// Display the current choice in the middle.
fn show_current_choice2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
self.get_current_item()
.render_center(target, area, self.inverse_selected_item);
}
/// Display all the choices fitting on the left side.
/// Going as far as possible.
fn show_left_choices(&self, area: Rect) {
@ -338,6 +388,48 @@ where
}
}
/// Display all the choices fitting on the left side.
/// Going as far as possible.
fn show_left_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
// NOTE: page index can get negative here, so having it as i16 instead of usize
let mut page_index = self.page_counter as i16 - 1;
let mut current_area = area.split_right(self.items_distance).0;
while current_area.width() > 0 {
// Breaking out of the loop if we exhausted left items
// and the carousel mode is not enabled.
if page_index < 0 {
if self.is_carousel {
// Moving to the last page.
page_index = self.last_page_index() as i16;
} else {
break;
}
}
let (choice, _) = self.choices.get(page_index as usize);
let choice_width = choice.width_side();
if current_area.width() <= choice_width && !self.show_incomplete {
// early break for an item that will not fit the remaining space
break;
}
// We need to calculate the area explicitly because we want to allow it
// to exceed the bounds of the original area.
let choice_area = Rect::from_top_right_and_size(
current_area.top_right(),
Offset::new(choice_width, current_area.height()),
);
choice.render_side(target, choice_area);
// Updating loop variables.
current_area = current_area
.split_right(choice_width + self.items_distance)
.0;
page_index -= 1;
}
}
/// Display all the choices fitting on the right side.
/// Going as far as possible.
fn show_right_choices(&self, area: Rect) {
@ -379,6 +471,47 @@ where
}
}
/// Display all the choices fitting on the right side.
/// Going as far as possible.
fn show_right_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
let mut page_index = self.page_counter + 1;
let mut current_area = area.split_left(self.items_distance).1;
while current_area.width() > 0 {
// Breaking out of the loop if we exhausted right items
// and the carousel mode is not enabled.
if page_index > self.last_page_index() {
if self.is_carousel {
// Moving to the first page.
page_index = 0;
} else {
break;
}
}
let (choice, _) = self.choices.get(page_index);
let choice_width = choice.width_side();
if current_area.width() <= choice_width && !self.show_incomplete {
// early break for an item that will not fit the remaining space
break;
}
// We need to calculate the area explicitly because we want to allow it
// to exceed the bounds of the original area.
let choice_area = Rect::from_top_left_and_size(
current_area.top_left(),
Offset::new(choice_width, current_area.height()),
);
choice.render_side(target, choice_area);
// Updating loop variables.
current_area = current_area
.split_left(choice_width + self.items_distance)
.1;
page_index += 1;
}
}
/// Decrease the page counter to the previous page.
fn decrease_page_counter(&mut self) {
self.page_counter -= 1;
@ -586,6 +719,12 @@ where
self.buttons.paint();
self.paint_choices();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.buttons.render(target);
self.render_choices(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -3,6 +3,8 @@ use crate::{
ui::{
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
geometry::{Alignment2D, Offset, Rect},
shape,
shape::Renderer,
},
};
@ -107,6 +109,27 @@ impl Choice for ChoiceItem {
);
}
/// Painting the item as the main choice in the middle.
/// Showing both the icon and text, if the icon is available.
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, area: Rect, inverse: bool) {
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
render_rounded_highlight(
target,
area,
Offset::new(width, self.font.visible_text_height("Ay")),
inverse,
);
render_text_icon(
target,
area,
width,
Some(self.text.as_ref()),
self.icon,
self.font,
inverse,
);
}
/// Getting the overall width in pixels when displayed in center.
/// That means both the icon and text will be shown.
fn width_center(&self) -> i16 {
@ -125,6 +148,20 @@ impl Choice for ChoiceItem {
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
}
/// Painting smaller version of the item on the side.
fn render_side<'s>(&self, target: &mut impl Renderer<'s>, area: Rect) {
let width = text_icon_width(self.side_text(), self.icon, self.font);
render_text_icon(
target,
area,
width,
self.side_text(),
self.icon,
self.font,
false,
);
}
/// Getting current button layout.
fn btn_layout(&self) -> ButtonLayout {
self.btn_layout.clone()
@ -151,6 +188,37 @@ fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {
}
}
fn render_rounded_highlight<'s>(
target: &mut impl Renderer<'s>,
area: Rect,
size: Offset,
inverse: bool,
) {
let bound = theme::BUTTON_OUTLINE;
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
let x_size = size.x + 2 * bound;
let y_size = size.y + 2 * bound;
let outline_size = Offset::new(x_size, y_size);
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
if inverse {
shape::Bar::new(outline)
.with_radius(1)
.with_bg(theme::FG)
.render(target);
} else {
// Draw outline by drawing two rounded rectangles
shape::Bar::new(outline)
.with_radius(1)
.with_bg(theme::FG)
.render(target);
shape::Bar::new(outline.shrink(1))
.with_radius(1)
.with_bg(theme::BG)
.render(target);
}
}
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
match (text, icon) {
(Some(text), Some(icon)) => {
@ -194,6 +262,40 @@ fn paint_text_icon(
}
}
fn render_text_icon<'s>(
target: &mut impl Renderer<'s>,
area: Rect,
width: i16,
text: Option<&str>,
icon: Option<Icon>,
font: Font,
inverse: bool,
) {
let fg_color = if inverse { theme::BG } else { theme::FG };
let mut baseline = area.bottom_center() - Offset::x(width / 2);
if let Some(icon) = icon {
let height_diff = font.visible_text_height("Ay") - icon.toif.height();
let vertical_offset = Offset::y(-height_diff / 2);
shape::ToifImage::new(baseline + vertical_offset, icon.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(fg_color)
.render(target);
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
}
if let Some(text) = text {
// Possibly shifting the baseline left, when there is a text bearing.
// This is to center the text properly.
baseline = baseline - Offset::x(font.start_x_bearing(text));
shape::Text::new(baseline, text)
.with_font(font)
.with_fg(fg_color)
.render(target);
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
shape::Renderer,
},
};
@ -81,6 +82,10 @@ impl Component for NumberInput {
fn paint(&mut self) {
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.choice_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -6,6 +6,7 @@ use crate::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::Icon,
geometry::Rect,
shape::Renderer,
util::char_to_string,
},
};
@ -448,6 +449,11 @@ impl Component for PassphraseEntry {
self.passphrase_dots.paint();
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.passphrase_dots.render(target);
self.choice_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -6,6 +6,7 @@ use crate::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::{Font, Icon},
geometry::Rect,
shape::Renderer,
},
};
@ -329,6 +330,12 @@ where
self.pin_line.paint();
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.header_line.render(target);
self.pin_line.render(target);
self.choice_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -4,6 +4,7 @@ use crate::{
ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
shape::Renderer,
},
};
@ -115,6 +116,10 @@ impl Component for SimpleChoice {
fn paint(&mut self) {
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.choice_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -4,6 +4,7 @@ use crate::{
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
geometry::Rect,
shape::Renderer,
util::char_to_string,
},
};
@ -310,6 +311,11 @@ impl Component for WordlistEntry {
self.chosen_letters.paint();
self.choice_page.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.chosen_letters.render(target);
self.choice_page.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -6,7 +6,9 @@ use crate::{
component::{Child, Component, Event, EventCtx},
constant,
display::{self, Color, Font, LOADER_MAX},
geometry::{Offset, Rect},
geometry::{Offset, Point, Rect},
shape,
shape::Renderer,
util::animation_disabled,
},
};
@ -164,6 +166,51 @@ impl Loader {
invert_from as i16,
);
}
pub fn render_loader<'s>(
&'s self,
target: &mut impl Renderer<'s>,
style: &LoaderStyle,
done: i32,
) {
let width = self.area.width();
// NOTE: need to calculate this in `i32`, it would overflow using `i16`
let split_point = (((width as i32 + 1) * done) / (display::LOADER_MAX as i32)) as i16;
let (r_left, r_right) = self.area.split_left(split_point);
let parts = [(r_left, true), (r_right, false)];
parts.map(|(r, invert)| {
target.in_clip(r, &|target| {
if invert {
shape::Bar::new(self.area)
.with_radius(3)
.with_bg(style.fg_color)
.render(target);
} else {
shape::Bar::new(self.area)
.with_radius(3)
.with_fg(style.fg_color)
.render(target);
}
let text_color = if invert {
style.bg_color
} else {
style.fg_color
};
self.get_text().map(|t| {
let pt = Point::new(
style.font.horz_center(self.area.x0, self.area.x1, t),
style.font.vert_center(self.area.y0, self.area.y1, "A"),
);
shape::Text::new(pt, t)
.with_font(style.font)
.with_fg(text_color)
.render(target);
});
});
});
}
}
impl Component for Loader {
@ -223,6 +270,28 @@ impl Component for Loader {
}
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful
// in automated tests.
// In practice, taking the current instant here is more precise in case some
// other component in the tree takes a long time to draw.
let now = Instant::now();
if let State::Initial = self.state {
self.render_loader(target, self.styles.normal, 0);
} else if let State::Grown = self.state {
self.render_loader(target, self.styles.normal, display::LOADER_MAX as i32);
} else {
let progress = self.progress(now);
if let Some(done) = progress {
self.render_loader(target, self.styles.normal, done as i32);
} else {
self.render_loader(target, self.styles.normal, 0);
}
}
}
}
pub struct LoaderStyleSheet {
@ -332,6 +401,10 @@ where
fn paint(&mut self) {
self.loader.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.loader.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -4,6 +4,7 @@ use crate::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
display::Color,
geometry::{Insets, Rect},
shape::Renderer,
},
};
@ -215,6 +216,12 @@ where
self.content.paint();
self.buttons.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.content.render(target);
self.buttons.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -11,7 +11,10 @@ use crate::{
},
constant,
display::{self, Font, Icon, LOADER_MAX},
geometry::Rect,
geometry::{Alignment2D, Offset, Rect},
model_tr::cshape,
shape,
shape::Renderer,
util::animation_disabled,
},
};
@ -182,6 +185,29 @@ where
self.description.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
let area = constant::screen();
let center = area.center() + Offset::y(self.loader_y_offset);
if self.indeterminate {
cshape::LoaderStarry::new(center, self.value)
.with_color(theme::FG)
.render(target);
} else {
cshape::LoaderCircular::new(center, self.value)
.with_color(theme::FG)
.render(target);
shape::ToifImage::new(center, self.icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(theme::FG)
.render(target);
}
self.description_pad.render(target);
self.description.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(Self::AREA);

@ -3,6 +3,8 @@ use crate::ui::{
constant::{screen, HEIGHT, WIDTH},
display::{Color, Icon},
geometry::{Alignment2D, Offset, Point, Rect},
shape,
shape::Renderer,
};
const MESSAGE_AREA_START: i16 = 24 + 11;
@ -107,4 +109,17 @@ impl<'a> Component for ResultScreen<'a> {
self.message_top.paint();
self.message_bottom.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.small_pad.render(target);
shape::ToifImage::new(screen().top_center() + Offset::y(ICON_TOP), self.icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(self.fg_color)
.render(target);
self.message_top.render(target);
self.message_bottom.render(target);
}
}

@ -2,6 +2,8 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad, Paginate},
display,
geometry::{Offset, Point, Rect},
shape,
shape::Renderer,
};
use super::super::theme;
@ -103,6 +105,35 @@ impl ScrollBar {
}
}
/// Create a (seemingly circular) dot given its top left point.
/// Make it full when it is active, otherwise paint just the perimeter and
/// leave center empty.
fn render_dot<'s>(&self, target: &mut impl Renderer<'s>, dot_type: &DotType, top_right: Point) {
let full_square =
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
match dot_type {
DotType::BigFull => shape::Bar::new(full_square)
.with_radius(2)
.with_bg(theme::FG)
.render(target),
DotType::Big => shape::Bar::new(full_square)
.with_radius(2)
.with_fg(theme::FG)
.render(target),
DotType::Middle => shape::Bar::new(full_square.shrink(1))
.with_radius(1)
.with_fg(theme::FG)
.render(target),
DotType::Small => shape::Bar::new(full_square.shrink(2))
.with_bg(theme::FG)
.render(target),
}
}
/// Get a sequence of dots to be drawn, with specifying their appearance.
/// Painting only big dots in case of 2 and 3 pages,
/// three big and 1 middle in case of 4 pages,
@ -202,6 +233,14 @@ impl ScrollBar {
top_right.x -= Self::DOTS_INTERVAL;
}
}
fn render_horizontal<'s>(&'s self, target: &mut impl Renderer<'s>) {
let mut top_right = self.pad.area.top_right();
for dot in self.get_drawable_dots().iter().rev() {
self.render_dot(target, dot, top_right);
top_right.x -= Self::DOTS_INTERVAL;
}
}
}
impl Component for ScrollBar {
@ -233,6 +272,17 @@ impl Component for ScrollBar {
self.pad.paint();
self.paint_horizontal();
}
/// Displaying one dot for each page.
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Not showing the scrollbar dot when there is only one page
if self.page_count <= 1 {
return;
}
self.pad.render(target);
self.render_horizontal(target);
}
}
impl Paginate for ScrollBar {

@ -3,10 +3,13 @@ use crate::{
translations::TR,
ui::{
component::{
text::util::text_multiline, Child, Component, Event, EventCtx, Never, Paginate,
text::util::{text_multiline, text_multiline2},
Child, Component, Event, EventCtx, Never, Paginate,
},
display::Font,
geometry::{Alignment, Offset, Rect},
shape,
shape::Renderer,
},
};
@ -95,6 +98,20 @@ where
);
}
/// Display the final page with user confirmation.
fn render_final_page<'s>(&'s self, target: &mut impl Renderer<'s>) {
let final_text = self.get_final_text();
text_multiline2(
target,
self.area.split_top(INFO_TOP_OFFSET).1,
final_text.as_str().into(),
Font::NORMAL,
theme::FG,
theme::BG,
Alignment::Start,
);
}
/// Display current set of recovery words.
fn paint_words(&mut self) {
let mut y_offset = 0;
@ -112,6 +129,32 @@ where
display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT);
}
}
/// Display current set of recovery words.
fn render_words<'s>(&'s self, target: &mut impl Renderer<'s>) {
let mut y_offset = 0;
// Showing the word index and the words itself
for i in 0..WORDS_PER_PAGE {
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
let index = self.word_index() + i;
if index >= self.share_words.len() {
break;
}
let word = &self.share_words[index];
let baseline = self.area.top_left() + Offset::y(y_offset);
let ordinal = build_string!(5, inttostr!(index as u8 + 1), ".");
shape::Text::new(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal)
.with_font(NUMBER_FONT)
.with_fg(theme::FG)
.render(target);
shape::Text::new(baseline + Offset::x(WORD_X_OFFSET), word.as_ref())
.with_font(WORD_FONT)
.with_fg(theme::FG)
.render(target);
}
}
}
impl<T> Component for ShareWords<T>
@ -147,6 +190,17 @@ where
self.paint_words();
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Showing scrollbar in all cases
// Individual pages are responsible for not colliding with it
self.scrollbar.render(target);
if self.is_final_page() {
self.render_final_page(target);
} else {
self.render_words(target);
}
}
}
impl<T> Paginate for ShareWords<T>

@ -3,6 +3,7 @@ use crate::{
ui::{
component::{Child, Component, Event, EventCtx},
geometry::{Insets, Rect},
shape::Renderer,
},
};
@ -77,6 +78,11 @@ where
self.content.paint();
self.buttons.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target);
self.buttons.render(target);
}
}
// DEBUG-ONLY SECTION BELOW

@ -4,7 +4,9 @@ use crate::{
ui::{
component::{Component, Event, EventCtx, Marquee, Never},
display,
geometry::{Offset, Rect},
geometry::{Alignment, Offset, Rect},
shape,
shape::Renderer,
},
};
@ -69,6 +71,16 @@ where
);
}
/// Display title/header at the top left of the given area.
pub fn render_header_left<'s>(target: &mut impl Renderer<'s>, title: &T, area: Rect) {
let text_height = theme::FONT_HEADER.text_height();
let title_baseline = area.top_left() + Offset::y(text_height - 1);
shape::Text::new(title_baseline, title.as_ref())
.with_font(theme::FONT_HEADER)
.with_fg(theme::FG)
.render(target);
}
/// Display title/header centered at the top of the given area.
pub fn paint_header_centered(title: &T, area: Rect) {
let text_height = theme::FONT_HEADER.text_height();
@ -81,6 +93,17 @@ where
theme::BG,
);
}
/// Display title/header centered at the top of the given area.
pub fn render_header_centered<'s>(target: &mut impl Renderer<'s>, title: &T, area: Rect) {
let text_height = theme::FONT_HEADER.text_height();
let title_baseline = area.top_center() + Offset::y(text_height - 1);
shape::Text::new(title_baseline, title.as_ref())
.with_align(Alignment::Center)
.with_font(theme::FONT_HEADER)
.with_fg(theme::FG)
.render(target);
}
}
impl<T> Component for Title<T>
@ -116,6 +139,16 @@ where
Self::paint_header_left(&self.title, self.area);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if self.needs_marquee {
self.marquee.render(target);
} else if self.centered {
Self::render_header_centered(target, &self.title, self.area);
} else {
Self::render_header_left(target, &self.title, self.area);
}
}
}
// DEBUG-ONLY SECTION BELOW

@ -1,6 +1,8 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
geometry::{Alignment2D, Offset, Rect},
shape,
shape::Renderer,
};
use super::super::theme;
@ -52,6 +54,30 @@ impl Component for WelcomeScreen {
theme::BG,
);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
shape::ToifImage::new(
self.area.bottom_center() - Offset::y(5),
theme::ICON_DEVICE_NAME.toif,
)
.with_align(Alignment2D::BOTTOM_CENTER)
.with_fg(theme::FG)
.render(target);
let icon = if self.empty_lock {
theme::ICON_LOGO_EMPTY
} else {
theme::ICON_LOGO
};
shape::ToifImage::new(
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
icon.toif,
)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.render(target);
}
}
#[cfg(feature = "ui_debug")]

@ -0,0 +1,88 @@
use crate::ui::{
canvas::Canvas,
display::Color,
geometry::{Offset, Point, Rect},
shape::{DrawingCache, Renderer, Shape, ShapeClone},
};
use without_alloc::alloc::LocalAllocLeakExt;
// Shape of horizontal solid/dotted line
pub struct HorizontalLine {
/// Position of the left-top point
pos: Point,
// Length of the line
length: i16,
/// Line thickness (default 1)
thickness: u8,
/// Steps of dots (default 0 - full line)
step: u8,
/// Color
color: Color,
}
impl HorizontalLine {
pub fn new(pos: Point, length: i16) -> Self {
Self {
pos,
length,
thickness: 1,
step: 0,
color: Color::white(),
}
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn with_thickness(self, thickness: u8) -> Self {
Self { thickness, ..self }
}
pub fn with_step(self, step: u8) -> Self {
Self { step, ..self }
}
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
renderer.render_shape(self);
}
}
impl<'s> Shape<'s> for HorizontalLine {
fn bounds(&self, _cache: &DrawingCache<'s>) -> Rect {
let size = Offset::new(self.length, self.thickness as i16);
Rect::from_top_left_and_size(self.pos, size)
}
fn cleanup(&mut self, _cache: &DrawingCache) {}
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
if self.step <= self.thickness {
// Solid line
let size = Offset::new(self.length, self.thickness as i16);
let r = Rect::from_top_left_and_size(self.pos, size);
canvas.fill_rect(r, self.color, 255);
} else {
// Dotted line
let thickness = self.thickness as i16;
for x in (0..self.length - thickness).step_by(self.step as usize) {
let r = Rect::from_top_left_and_size(
self.pos + Offset::x(x),
Offset::uniform(thickness),
);
canvas.fill_rect(r, self.color, 255);
}
}
}
}
impl<'s> ShapeClone<'s> for HorizontalLine {
fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>>
where
T: LocalAllocLeakExt<'alloc>,
{
let clone = bump.alloc_t::<HorizontalLine>()?;
Some(clone.uninit.init(HorizontalLine { ..self }))
}
}

@ -0,0 +1,117 @@
use crate::ui::{
canvas::Canvas,
display::Color,
geometry::{Offset, Point, Rect},
shape::{DrawingCache, Renderer, Shape, ShapeClone},
};
use without_alloc::alloc::LocalAllocLeakExt;
static CELLS: [Offset; 24] = [
Offset::new(1, -4),
Offset::new(2, -4),
Offset::new(3, -3),
Offset::new(4, -2),
Offset::new(4, -1),
Offset::new(4, 0),
Offset::new(4, 1),
Offset::new(4, 2),
Offset::new(3, 3),
Offset::new(2, 4),
Offset::new(1, 4),
Offset::new(0, 4),
Offset::new(-1, 4),
Offset::new(-2, 4),
Offset::new(-3, 3),
Offset::new(-4, 2),
Offset::new(-4, 1),
Offset::new(-4, 0),
Offset::new(-4, -1),
Offset::new(-4, -2),
Offset::new(-3, -3),
Offset::new(-2, -4),
Offset::new(-1, -4),
Offset::new(0, -4),
];
pub struct LoaderCircular {
/// Position of point (0,0)
pos: Point,
/// Value 0..1000
value: u16,
/// Color
color: Color,
/// Scale (length of square size)
scale: i16,
}
impl LoaderCircular {
pub fn new(pos: Point, value: u16) -> Self {
Self {
pos,
value,
color: Color::white(),
scale: 2,
}
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn with_scale(self, scale: i16) -> Self {
Self { scale, ..self }
}
fn cells(&self) -> &[Offset] {
let value = self.value.clamp(0, 1000);
let last = (CELLS.len() * value as usize) / 1000;
&CELLS[..last]
}
fn cell_rect(&self, offset: Offset) -> Rect {
let pt = Point::new(
self.pos.x + (offset.x * self.scale) - self.scale / 2,
self.pos.y + (offset.y * self.scale) - self.scale / 2,
);
Rect::from_top_left_and_size(pt, Offset::uniform(self.scale))
}
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
renderer.render_shape(self);
}
}
impl<'s> Shape<'s> for LoaderCircular {
fn bounds(&self, _cache: &DrawingCache<'s>) -> Rect {
let cells = self.cells();
if cells.is_empty() {
Rect::zero()
} else {
let mut b = self.cell_rect(cells[0]);
cells[1..]
.iter()
.for_each(|c| b = b.union(self.cell_rect(*c)));
b
}
}
fn cleanup(&mut self, _cache: &DrawingCache) {}
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
for c in self.cells().iter() {
canvas.fill_rect(self.cell_rect(*c), self.color, 255);
}
}
}
impl ShapeClone<'_> for LoaderCircular {
fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape>
where
T: LocalAllocLeakExt<'alloc>,
{
let clone = bump.alloc_t::<LoaderCircular>()?;
Some(clone.uninit.init(LoaderCircular { ..self }))
}
}

@ -0,0 +1,89 @@
use crate::ui::{
canvas::Canvas,
display::Color,
geometry::{Offset, Point, Rect},
shape::{DrawingCache, Renderer, Shape, ShapeClone},
};
use without_alloc::alloc::LocalAllocLeakExt;
use core::f32::consts::SQRT_2;
const STAR_COUNT: usize = 8;
const RADIUS: i16 = 3;
const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16;
// Offset of the normal point and then the extra offset for the main point
static STARS: [(Offset, Offset); STAR_COUNT] = [
(Offset::y(-RADIUS), Offset::y(-1)),
(Offset::new(DIAGONAL, -DIAGONAL), Offset::new(1, -1)),
(Offset::x(RADIUS), Offset::x(1)),
(Offset::new(DIAGONAL, DIAGONAL), Offset::new(1, 1)),
(Offset::y(RADIUS), Offset::y(1)),
(Offset::new(-DIAGONAL, DIAGONAL), Offset::new(-1, 1)),
(Offset::x(-RADIUS), Offset::x(-1)),
(Offset::new(-DIAGONAL, -DIAGONAL), Offset::new(-1, -1)),
];
/// A shape of a TS3 small loader
pub struct LoaderSmall {
/// Position of point (0,0)
pos: Point,
/// Value 0..1000
value: u16,
/// Color
color: Color,
}
impl LoaderSmall {
pub fn new(pos: Point, value: u16) -> Self {
Self {
pos,
value,
color: Color::white(),
}
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
renderer.render_shape(self);
}
}
impl Shape<'_> for LoaderSmall {
fn bounds(&self, _cache: &DrawingCache) -> Rect {
Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + 1)
}
fn cleanup(&mut self, _cache: &DrawingCache) {}
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
// Calculate index of the highlighted star
let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT;
for (i, (star_offset, hili_offset)) in STARS.iter().enumerate() {
if (sel_idx + 1) % STAR_COUNT != i {
// Draw a star if it's not behind the highlighted one (clockwise)
let star_pos = self.pos + *star_offset;
canvas.draw_pixel(star_pos, self.color);
if sel_idx == i {
// Higlight the main star
canvas.draw_pixel(star_pos + *hili_offset, self.color);
}
}
}
}
}
impl ShapeClone<'_> for LoaderSmall {
fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape>
where
T: LocalAllocLeakExt<'alloc>,
{
let clone = bump.alloc_t::<LoaderSmall>()?;
Some(clone.uninit.init(LoaderSmall { ..self }))
}
}

@ -0,0 +1,106 @@
use crate::ui::{
canvas::Canvas,
display::Color,
geometry::{Offset, Point, Rect},
shape::{DrawingCache, Renderer, Shape, ShapeClone},
};
use without_alloc::alloc::LocalAllocLeakExt;
use core::f32::consts::SQRT_2;
const STAR_COUNT: usize = 8;
const STAR_SMALL: i16 = 2;
const STAR_MEDIUM: i16 = 4;
const STAR_LARGE: i16 = 6;
const RADIUS: i16 = 13;
const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16;
// Offset of the normal point and then the extra offset for the main point
static STARS: [Offset; STAR_COUNT] = [
Offset::y(-RADIUS),
Offset::new(DIAGONAL, -DIAGONAL),
Offset::x(RADIUS),
Offset::new(DIAGONAL, DIAGONAL),
Offset::y(RADIUS),
Offset::new(-DIAGONAL, DIAGONAL),
Offset::x(-RADIUS),
Offset::new(-DIAGONAL, -DIAGONAL),
];
/// A shape of a TS3 starry loader
pub struct LoaderStarry {
/// Position of point (0,0)
pos: Point,
/// Value 0..1000
value: u16,
/// Color
color: Color,
}
impl LoaderStarry {
pub fn new(pos: Point, value: u16) -> Self {
Self {
pos,
value,
color: Color::white(),
}
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
renderer.render_shape(self);
}
fn draw_large_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_LARGE));
canvas.fill_round_rect(r, 2, self.color, 255);
}
fn draw_medium_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_MEDIUM));
canvas.fill_round_rect(r, 1, self.color, 255);
}
fn draw_small_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_SMALL));
canvas.fill_rect(r, self.color, 255);
}
}
impl Shape<'_> for LoaderStarry {
fn bounds(&self, _cache: &DrawingCache) -> Rect {
Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + STAR_LARGE)
}
fn cleanup(&mut self, _cache: &DrawingCache) {}
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
// Calculate the index of the big star
let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT;
for (i, c) in STARS.iter().enumerate() {
if i == sel_idx {
self.draw_large_star(canvas, *c);
} else if (sel_idx + 1) % 8 == i || (sel_idx - 1) % 8 == i {
self.draw_medium_star(canvas, *c);
} else {
self.draw_small_star(canvas, *c);
}
}
}
}
impl ShapeClone<'_> for LoaderStarry {
fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape>
where
T: LocalAllocLeakExt<'alloc>,
{
let clone = bump.alloc_t::<LoaderStarry>()?;
Some(clone.uninit.init(LoaderStarry { ..self }))
}
}

@ -0,0 +1,9 @@
pub mod dotted_line;
pub mod loader_circular;
pub mod loader_small;
pub mod loader_starry;
pub use dotted_line::HorizontalLine;
pub use loader_circular::LoaderCircular;
pub use loader_small::LoaderSmall;
pub use loader_starry::LoaderStarry;

@ -234,10 +234,9 @@ where
}
}
impl<'a, T, F> ComponentMsgObj for ConfirmHomescreen<T, F>
impl<'a, T> ComponentMsgObj for ConfirmHomescreen<T>
where
T: StringType + Clone,
F: Fn() -> &'a [u8],
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -422,15 +421,8 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
// Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8]
// in every paint pass.
// SAFETY: We expect no existing mutable reference. Resulting reference is
// discarded before returning to micropython.
let buffer_func = move || unsafe { unwrap!(get_buffer(data)) };
let obj = LayoutObj::new(ConfirmHomescreen::new(title, buffer_func))?;
let image: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
let obj = LayoutObj::new(ConfirmHomescreen::new(title, image))?;
Ok(obj.into())
};

@ -3,6 +3,7 @@ pub mod bootloader;
pub mod common_messages;
pub mod component;
pub mod constant;
pub mod cshape;
#[cfg(feature = "micropython")]
pub mod layout;
pub mod screens;

@ -1,9 +1,14 @@
#[cfg(feature = "micropython")]
use crate::micropython::buffer::StrBuffer;
use crate::ui::{
component::base::Component, constant::screen, display, model_tr::component::WelcomeScreen,
component::base::Component, constant::screen, model_tr::component::WelcomeScreen,
};
#[cfg(not(feature = "new_rendering"))]
use crate::ui::display;
#[cfg(feature = "new_rendering")]
use crate::ui::{display::Color, shape::render_on_display};
use super::{component::ErrorScreen, constant};
#[cfg(not(feature = "micropython"))]
@ -27,6 +32,13 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
let mut frame = ErrorScreen::new(title, msg, footer);
frame.place(constant::screen());
#[cfg(feature = "new_rendering")]
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
#[cfg(not(feature = "new_rendering"))]
frame.paint();
}
@ -34,7 +46,16 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
extern "C" fn screen_boot_full() {
let mut frame = WelcomeScreen::new(false);
frame.place(screen());
display::sync();
frame.paint();
display::refresh();
#[cfg(feature = "new_rendering")]
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
#[cfg(not(feature = "new_rendering"))]
{
display::sync();
frame.paint();
display::refresh();
}
}

@ -11,6 +11,7 @@ use crate::ui::{
CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA,
},
},
shape::Renderer,
};
#[repr(u32)]
@ -102,6 +103,15 @@ impl<'a> Component for Intro<'a> {
self.menu.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.text.render(target);
self.warn.render(target);
self.host.render(target);
self.menu.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.menu.bounds(sink);

@ -13,6 +13,7 @@ use crate::{
X32,
},
},
shape::Renderer,
},
};
@ -108,6 +109,14 @@ impl Component for Menu {
self.reset.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.close.render(target);
self.reboot.render(target);
self.reset.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.close.bounds(sink);

@ -1,12 +1,14 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
constant::screen,
display::{self, Font, Icon},
geometry::{Alignment2D, Offset, Rect},
display::{self, toif::Toif, Font, Icon},
geometry::{Alignment, Alignment2D, Offset, Rect},
model_tt::theme::{
bootloader::{START_URL, WELCOME_COLOR},
BLACK, GREY_MEDIUM, WHITE,
},
shape,
shape::Renderer,
};
pub struct Welcome {
@ -56,4 +58,26 @@ impl Component for Welcome {
BLACK,
);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
shape::Text::new(screen().top_center() + Offset::y(102), "Get started with")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(GREY_MEDIUM)
.render(target);
shape::Text::new(screen().top_center() + Offset::y(126), "your Trezor at")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(GREY_MEDIUM)
.render(target);
let icon = unwrap!(Toif::new(START_URL));
shape::ToifImage::new(screen().top_center() + Offset::y(135), icon)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(WHITE)
.render(target);
}
}

@ -11,6 +11,7 @@ use crate::{
Component, Event, EventCtx, Paginate, Qr,
},
geometry::Rect,
shape::Renderer,
},
};
@ -187,6 +188,14 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
match self.current_page {
0 => self.qr_code.render(target),
1 => self.details.render(target),
_ => self.xpub_view.render(target),
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
match self.current_page {

@ -15,6 +15,8 @@ use crate::ui::{
WHITE,
},
},
shape,
shape::Renderer,
};
const ICON_TOP: i16 = 17;
@ -234,6 +236,40 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.content_pad.render(target);
if let Some(info) = self.info.as_ref() {
if self.show_info {
info.close_button.render(target);
info.title.render(target);
info.text.render(target);
self.left_button.render(target);
self.right_button.render(target);
// short-circuit before painting the main components
return;
} else {
info.info_button.render(target);
// pass through to the rest of the paint
}
}
self.message.render(target);
self.alert.render(target);
self.left_button.render(target);
self.right_button.render(target);
match &self.title {
ConfirmTitle::Text(label) => label.render(target),
ConfirmTitle::Icon(icon) => {
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(WHITE)
.render(target);
}
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.left_button.bounds(sink);

@ -9,6 +9,8 @@ use crate::{
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
@ -189,6 +191,18 @@ impl<T> Button<T> {
}
}
pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
match &self.content {
ButtonContent::IconBlend(_, _, _) => {}
_ => shape::Bar::new(self.area)
.with_bg(style.button_color)
.with_fg(style.border_color)
.with_thickness(style.border_width)
.with_radius(style.border_radius as i16)
.render(target),
}
}
pub fn paint_content(&self, style: &ButtonStyle)
where
T: AsRef<str>,
@ -229,6 +243,47 @@ impl<T> Button<T> {
),
}
}
pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle)
where
T: AsRef<str>,
{
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let text = text.as_ref();
let width = style.font.text_width(text);
let height = style.font.text_height();
let start_of_baseline = self.area.center()
+ Offset::new(-width / 2, height / 2)
+ Offset::y(Self::BASELINE_OFFSET);
shape::Text::new(start_of_baseline, text)
.with_font(style.font)
.with_fg(style.text_color)
.render(target);
}
ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center(), icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.text_color)
.render(target);
}
ButtonContent::IconAndText(child) => {
child.paint(self.area, self.style(), Self::BASELINE_OFFSET);
}
ButtonContent::IconBlend(bg, fg, offset) => {
shape::Bar::new(self.area)
.with_bg(style.background_color)
.render(target);
shape::ToifImage::new(self.area.top_left(), bg.toif)
.with_fg(style.button_color)
.render(target);
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
.with_fg(style.text_color)
.render(target);
}
}
}
}
impl<T> Component for Button<T>
@ -320,6 +375,12 @@ where
self.paint_content(style);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style();
self.render_background(target, style);
self.render_content(target, style);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);

@ -6,12 +6,16 @@ use crate::{
micropython::buffer::StrBuffer,
translations::TR,
ui::{
canvas::algo::PI4,
component::{
base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label,
Split,
},
constant,
display::loader::{loader_circular_uncompress, LoaderDimensions},
geometry::{Insets, Rect},
geometry::{Insets, Offset, Rect},
shape,
shape::Renderer,
util::animation_disabled,
},
};
@ -129,6 +133,40 @@ where
);
self.label.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target);
let center = constant::screen().center() + Offset::y(LOADER_OFFSET);
let active_color = theme::FG;
let background_color = theme::BG;
let inactive_color = background_color.blend(active_color, 85);
let start = (self.value as i16 - 100) % 1000;
let end = (self.value as i16 + 100) % 1000;
let start = ((start as i32 * 8 * PI4 as i32) / 1000) as i16;
let end = ((end as i32 * 8 * PI4 as i32) / 1000) as i16;
shape::Circle::new(center, LOADER_OUTER)
.with_bg(inactive_color)
.render(target);
shape::Circle::new(center, LOADER_OUTER)
.with_bg(active_color)
.with_start_angle(start)
.with_end_angle(end)
.render(target);
shape::Circle::new(center, LOADER_INNER + 2)
.with_bg(active_color)
.render(target);
shape::Circle::new(center, LOADER_INNER)
.with_bg(background_color)
.render(target);
self.label.render(target);
}
}
#[cfg(feature = "ui_debug")]

@ -10,6 +10,7 @@ use crate::{
Child, Component, Event, EventCtx, Never,
},
geometry::{Insets, LinearPlacement, Rect},
shape::Renderer,
},
};
@ -71,6 +72,11 @@ where
self.controls.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target);
self.controls.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.content.bounds(sink);
@ -198,6 +204,12 @@ where
self.controls.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.image.render(target);
self.paragraphs.render(target);
self.controls.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.image.bounds(sink);

@ -2,6 +2,8 @@ use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
geometry::{Alignment2D, Point, Rect},
shape,
shape::Renderer,
};
use crate::ui::model_tt::{
@ -86,4 +88,19 @@ impl<T: AsRef<str>> Component for ErrorScreen<'_, T> {
self.message.paint();
self.footer.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
let icon = ICON_WARNING40;
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
.with_fg(WHITE)
.with_bg(FATAL_ERROR_COLOR)
.with_align(Alignment2D::TOP_CENTER)
.render(target);
self.title.render(target);
self.message.render(target);
self.footer.render(target);
}
}

@ -7,6 +7,8 @@ use crate::ui::{
swipe::{Swipe, SwipeDirection},
theme, ScrollBar,
},
shape,
shape::Renderer,
};
use super::CancelConfirmMsg;
@ -210,6 +212,36 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.icon.render(target);
self.controls.render(target);
self.app_name.render(target);
if self.scrollbar.page_count > 1 {
self.scrollbar.render(target);
}
// Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area();
let real_area = account_name_area
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
shape::Bar::new(real_area).with_bg(theme::BG).render(target);
// Account name is optional.
// Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.)
let account_name = self.account_name.text().as_ref();
let app_name = self.app_name.text().as_ref();
if !account_name.is_empty() && account_name != app_name {
self.account_name.render(target);
}
if self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.icon.bounds(sink);

@ -6,6 +6,7 @@ use crate::ui::{
display::Icon,
geometry::{Alignment, Insets, Offset, Rect},
model_tt::component::{Button, ButtonMsg, CancelInfoConfirmMsg},
shape::Renderer,
};
pub struct Frame<T, U> {
@ -169,6 +170,13 @@ where
self.content.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
self.subtitle.render(target);
self.button.render(target);
self.content.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.title.bounds(sink);

@ -1,6 +1,7 @@
mod render;
use crate::{
micropython::gc::Gc,
strutil::TString,
time::{Duration, Instant},
translations::TR,
@ -9,9 +10,10 @@ use crate::{
component::{Component, Event, EventCtx, Pad, TimerToken},
display::{self, tjpgd::jpeg_info, toif::Icon, Color, Font},
event::{TouchEvent, USBEvent},
geometry::{Offset, Point, Rect},
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
model_tt::{constant, theme::IMAGE_HOMESCREEN},
shape::{self, Renderer},
},
};
@ -20,7 +22,7 @@ use crate::{
ui::{
constant::HEIGHT,
display::{
tjpgd::{jpeg_test, BufferInput},
tjpgd::BufferInput,
toif::{Toif, ToifFormat},
},
model_tt::component::homescreen::render::{
@ -49,6 +51,7 @@ const LOADER_DURATION: Duration = Duration::from_millis(2000);
pub struct Homescreen {
label: TString<'static>,
notification: Option<(TString<'static>, u8)>,
custom_image: Option<Gc<[u8]>>,
hold_to_lock: bool,
loader: Loader,
pad: Pad,
@ -69,6 +72,7 @@ impl Homescreen {
Self {
label,
notification,
custom_image: get_user_custom_image().ok(),
hold_to_lock,
loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
pad: Pad::with_background(theme::BG),
@ -119,6 +123,16 @@ impl Homescreen {
self.loader.paint()
}
fn render_loader<'s>(&'s self, target: &mut impl Renderer<'s>) {
TR::progress__locking_device.map_translated(|t| {
shape::Text::new(TOP_CENTER + Offset::y(HOLD_Y), t)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(theme::FG);
});
self.loader.render(target)
}
pub fn set_paint_notification(&mut self) {
self.paint_notification_only = true;
}
@ -210,10 +224,9 @@ impl Component for Homescreen {
let notification = self.get_notification();
let res = get_user_custom_image();
let mut show_default = true;
if let Ok(data) = res {
if let Some(ref data) = self.custom_image {
if is_image_jpeg(data.as_ref()) {
let input = BufferInput(data.as_ref());
let mut pool = BufferJpegWork::get_cleared();
@ -254,6 +267,86 @@ impl Component for Homescreen {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) {
self.render_loader(target);
} else {
let img_data = match self.custom_image {
Some(ref img) => img.as_ref(),
None => IMAGE_HOMESCREEN,
};
if is_image_jpeg(img_data) {
shape::JpegImage::new(self.pad.area.center(), img_data)
.with_align(Alignment2D::CENTER)
.render(target);
} else if is_image_toif(img_data) {
shape::ToifImage::new(self.pad.area.center(), unwrap!(Toif::new(img_data)))
.with_align(Alignment2D::CENTER)
.render(target);
}
self.label.map(|t| {
let r = Rect::new(Point::new(6, 198), Point::new(234, 233));
shape::Bar::new(r)
.with_bg(Color::black())
.with_alpha(89)
.with_radius(3)
.render(target);
let style = theme::TEXT_DEMIBOLD;
let pos = Point::new(self.pad.area.center().x, LABEL_Y);
shape::Text::new(pos, t)
.with_align(Alignment::Center)
.with_font(style.text_font)
.with_fg(theme::FG)
.render(target);
});
if let Some(notif) = self.get_notification() {
const NOTIFICATION_HEIGHT: i16 = 36;
const NOTIFICATION_BORDER: i16 = 6;
const TEXT_ICON_SPACE: i16 = 8;
let banner = self
.pad
.area
.inset(Insets::sides(NOTIFICATION_BORDER))
.with_height(NOTIFICATION_HEIGHT)
.translate(Offset::y(NOTIFICATION_BORDER));
shape::Bar::new(banner)
.with_radius(2)
.with_bg(notif.color)
.render(target);
notif.text.map(|t| {
let style = theme::TEXT_BOLD;
let icon_width = notif.icon.toif.width() + TEXT_ICON_SPACE;
let text_pos = Point::new(
style
.text_font
.horz_center(banner.x0 + icon_width, banner.x1, t),
style.text_font.vert_center(banner.y0, banner.y1, "A"),
);
shape::Text::new(text_pos, t)
.with_font(style.text_font)
.with_fg(style.text_color)
.render(target);
let icon_pos = Point::new(text_pos.x - icon_width, banner.center().y);
shape::ToifImage::new(icon_pos, notif.icon.toif)
.with_fg(style.text_color)
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
});
}
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.loader.bounds(sink);
@ -271,6 +364,7 @@ impl crate::trace::Trace for Homescreen {
pub struct Lockscreen {
label: TString<'static>,
custom_image: Option<Gc<[u8]>>,
bootscreen: bool,
coinjoin_authorized: bool,
}
@ -279,6 +373,7 @@ impl Lockscreen {
pub fn new(label: TString<'static>, bootscreen: bool, coinjoin_authorized: bool) -> Self {
Lockscreen {
label,
custom_image: get_user_custom_image().ok(),
bootscreen,
coinjoin_authorized,
}
@ -343,10 +438,9 @@ impl Component for Lockscreen {
texts = &texts[1..];
}
let res = get_user_custom_image();
let mut show_default = true;
if let Ok(data) = res {
if let Some(ref data) = self.custom_image {
if is_image_jpeg(data.as_ref()) {
let input = BufferInput(data.as_ref());
let mut pool = BufferJpegWork::get_cleared();
@ -370,10 +464,113 @@ impl Component for Lockscreen {
homescreen_blurred(&mut hs_img, texts);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let img_data = match self.custom_image {
Some(ref img) => img.as_ref(),
None => IMAGE_HOMESCREEN,
};
let center = constant::screen().center();
if is_image_jpeg(img_data) {
shape::JpegImage::new(center, img_data)
.with_align(Alignment2D::CENTER)
.with_blur(4)
.with_dim(140)
.render(target);
} else if is_image_toif(img_data) {
shape::ToifImage::new(center, unwrap!(Toif::new(img_data)))
.with_align(Alignment2D::CENTER)
//.with_blur(5)
.render(target);
}
let (locked, tap) = if self.bootscreen {
(
TR::lockscreen__title_not_connected,
TR::lockscreen__tap_to_connect,
)
} else {
(TR::lockscreen__title_locked, TR::lockscreen__tap_to_unlock)
};
let mut label_style = theme::TEXT_DEMIBOLD;
label_style.text_color = theme::GREY_LIGHT;
let mut texts: &[HomescreenText] = &[
HomescreenText {
text: "".into(),
style: theme::TEXT_NORMAL,
offset: Offset::new(2, COINJOIN_Y),
icon: Some(theme::ICON_COINJOIN),
},
HomescreenText {
text: locked.into(),
style: theme::TEXT_BOLD,
offset: Offset::y(LOCKED_Y),
icon: Some(theme::ICON_LOCK),
},
HomescreenText {
text: tap.into(),
style: theme::TEXT_NORMAL,
offset: Offset::y(TAP_Y),
icon: None,
},
HomescreenText {
text: self.label,
style: label_style,
offset: Offset::y(LABEL_Y),
icon: None,
},
];
if !self.coinjoin_authorized {
texts = &texts[1..];
}
for item in texts.iter() {
item.text.map(|t| {
const TEXT_ICON_SPACE: i16 = 2;
let icon_width = match item.icon {
Some(icon) => icon.toif.width() + TEXT_ICON_SPACE,
None => 0,
};
let area = constant::screen();
let text_pos = Point::new(
item.style
.text_font
.horz_center(area.x0 + icon_width, area.x1, t),
0,
) + item.offset;
shape::Text::new(text_pos, t)
.with_font(item.style.text_font)
.with_fg(item.style.text_color)
.render(target);
if let Some(icon) = item.icon {
let icon_pos = Point::new(text_pos.x - icon_width, text_pos.y);
shape::ToifImage::new(icon_pos, icon.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(item.style.text_color)
.render(target);
}
});
}
}
}
pub fn check_homescreen_format(buffer: &[u8]) -> bool {
is_image_jpeg(buffer) && jpeg_test(buffer)
#[cfg(not(feature = "new_rendering"))]
let result = is_image_jpeg(buffer) && crate::ui::display::tjpgd::jpeg_test(buffer);
#[cfg(feature = "new_rendering")]
let result = is_image_jpeg(buffer); // !@# TODO: test like if `new_rendering` is off
result
}
fn is_image_jpeg(buffer: &[u8]) -> bool {

@ -7,13 +7,15 @@ use crate::{
model_tt::{
component::{
keyboard::{
common::{paint_pending_marker, MultiTapKeyboard},
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
},
Button, ButtonContent, ButtonMsg,
},
theme,
},
shape,
shape::Renderer,
},
};
use heapless::String;
@ -154,6 +156,51 @@ impl Component for Bip39Input {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let area = self.button.area();
let style = self.button.style();
// First, paint the button background.
self.button.render_background(target, style);
// Paint the entered content (the prefix of the suggested word).
let text = self.textbox.content();
let width = style.font.text_width(text);
// Content starts in the left-center point, offset by 16px to the right and 8px
// to the bottom.
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
shape::Text::new(text_baseline, text)
.with_font(style.font)
.with_fg(style.text_color)
.render(target);
// Paint the rest of the suggested dictionary word.
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
let word_baseline = text_baseline + Offset::new(width, 0);
let style = self.button_suggestion.style();
shape::Text::new(word_baseline, word)
.with_font(style.font)
.with_fg(style.text_color)
.render(target);
}
// Paint the pending marker.
if self.multi_tap.pending_key().is_some() {
render_pending_marker(target, text_baseline, text, style.font, style.text_color);
}
// Paint the icon.
if let ButtonContent::Icon(icon) = self.button.content() {
// Icon is painted in the right-center point, of expected size 16x16 pixels, and
// 16px from the right edge.
let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0);
shape::ToifImage::new(icon_center, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.text_color)
.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.button.bounds(sink);

@ -4,6 +4,8 @@ use crate::{
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
display::{self, Color, Font},
geometry::{Offset, Point, Rect},
shape,
shape::Renderer,
},
};
@ -127,3 +129,24 @@ pub fn paint_pending_marker(text_baseline: Point, text: &str, font: Font, color:
display::rect_fill(marker_rect, color);
}
}
/// Create a visible "underscoring" of the last letter of a text.
pub fn render_pending_marker<'s>(
target: &mut impl Renderer<'s>,
text_baseline: Point,
text: &str,
font: Font,
color: Color,
) {
// Measure the width of the last character of input.
if let Some(last) = text.chars().last() {
let width = font.text_width(text);
let last_width = font.char_width(last);
// Draw the marker 2px under the start of the baseline of the last character.
let marker_origin = text_baseline + Offset::new(width - last_width, 2);
// Draw the marker 1px longer than the last character, and 3px thick.
let marker_rect =
Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3));
shape::Bar::new(marker_rect).with_bg(color).render(target);
}
}

@ -5,6 +5,7 @@ use crate::ui::{
component::{Button, ButtonMsg, Swipe, SwipeDirection},
theme,
},
shape::Renderer,
};
pub const MNEMONIC_KEY_COUNT: usize = 9;
@ -182,6 +183,19 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if self.input.inner().inner().is_empty() {
self.prompt.render(target);
} else {
self.input.render(target);
self.back.render(target);
}
for btn in &self.keys {
btn.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.prompt.bounds(sink);

@ -6,10 +6,12 @@ use crate::ui::{
geometry::{Grid, Offset, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg},
keyboard::common::{paint_pending_marker, MultiTapKeyboard},
keyboard::common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
swipe::{Swipe, SwipeDirection},
theme, ScrollBar,
},
shape,
shape::Renderer,
util::long_line_content_with_ellipsis,
};
@ -296,6 +298,20 @@ impl Component for PassphraseKeyboard {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.input.render(target);
self.scrollbar.render(target);
self.confirm.render(target);
self.back.render(target);
for btn in &self.keys {
btn.render(target);
}
if self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.input.bounds(sink);
@ -376,6 +392,40 @@ impl Component for Input {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = theme::label_keyboard();
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
- Offset::y(style.text_font.text_baseline());
let text = self.textbox.content();
shape::Bar::new(self.area).with_bg(theme::BG).render(target);
// Find out how much text can fit into the textbox.
// Accounting for the pending marker, which draws itself one pixel longer than
// the last character
let available_area_width = self.area.width() - 1;
let text_to_display =
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
shape::Text::new(text_baseline, &text_to_display)
.with_font(style.text_font)
.with_fg(style.text_color)
.render(target);
// Paint the pending marker.
if self.multi_tap.pending_key().is_some() {
render_pending_marker(
target,
text_baseline,
&text_to_display,
style.text_font,
style.text_color,
);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)

@ -11,11 +11,13 @@ use crate::{
},
display::{self, Font},
event::TouchEvent,
geometry::{Alignment2D, Grid, Insets, Offset, Rect},
geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg, ButtonMsg::Clicked},
theme,
},
shape,
shape::Renderer,
},
};
@ -267,6 +269,26 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.erase_btn.render(target);
self.textbox_pad.render(target);
if self.textbox.inner().is_empty() {
if let Some(ref w) = self.major_warning {
w.render(target);
} else {
self.major_prompt.render(target);
}
self.minor_prompt.render(target);
self.cancel_btn.render(target);
} else {
self.textbox.render(target);
}
self.confirm_btn.render(target);
for btn in &self.digit_btns {
btn.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.major_prompt.bounds(sink);
@ -367,6 +389,27 @@ impl PinDots {
}
}
fn render_digits<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let center = area.center() + Offset::y(Font::MONO.text_height() / 2);
let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2);
let digits = self.digits.len();
if digits <= MAX_VISIBLE_DOTS {
shape::Text::new(center, &self.digits)
.with_align(Alignment::Center)
.with_font(Font::MONO)
.with_fg(self.style.text_color)
.render(target);
} else {
let offset: usize = digits.saturating_sub(MAX_VISIBLE_DIGITS);
shape::Text::new(right, &self.digits[offset..])
.with_align(Alignment::End)
.with_font(Font::MONO)
.with_fg(self.style.text_color)
.render(target);
}
}
fn paint_dots(&self, area: Rect) {
let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER);
@ -410,6 +453,44 @@ impl PinDots {
cursor.x += step;
}
}
fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER);
let digits = self.digits.len();
let dots_visible = digits.min(MAX_VISIBLE_DOTS);
let step = Self::DOT + Self::PADDING;
// Jiggle when overflowed.
if digits > dots_visible && digits % 2 == 0 {
cursor.x += Self::TWITCH
}
// Small leftmost dot.
if digits > dots_visible + 1 {
shape::ToifImage::new(cursor - Offset::x(2 * step), theme::DOT_SMALL.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(self.style.text_color)
.render(target);
}
// Greyed out dot.
if digits > dots_visible {
shape::ToifImage::new(cursor - Offset::x(step), theme::DOT_ACTIVE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(theme::GREY_LIGHT)
.render(target);
}
// Draw a dot for each PIN digit.
for _ in 0..dots_visible {
shape::ToifImage::new(cursor, theme::DOT_ACTIVE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(self.style.text_color)
.render(target);
cursor.x += step;
}
}
}
impl Component for PinDots {
@ -452,6 +533,16 @@ impl Component for PinDots {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let dot_area = self.area.inset(HEADER_PADDING);
self.pad.render(target);
if self.display_digits {
self.render_digits(dot_area, target)
} else {
self.render_dots(dot_area, target)
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);

@ -14,13 +14,15 @@ use crate::{
model_tt::{
component::{
keyboard::{
common::{paint_pending_marker, MultiTapKeyboard},
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
},
Button, ButtonContent, ButtonMsg,
},
theme,
},
shape,
shape::Renderer,
util::ResultExt,
},
};
@ -185,6 +187,71 @@ impl Component for Slip39Input {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let area = self.button.area();
let style = self.button.style();
// First, paint the button background.
self.button.render_background(target, style);
// Content starts in the left-center point, offset by 16px to the right and 8px
// to the bottom.
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
// To simplify things, we always copy the printed string here, even if it
// wouldn't be strictly necessary.
let mut text: String<MAX_LENGTH> = String::new();
if let Some(word) = self.final_word {
// We're done with input, paint the full word.
text.push_str(word)
.assert_if_debugging_ui("Text buffer is too small");
} else {
// Paint an asterisk for each letter of input.
for ch in iter::repeat('*').take(self.textbox.content().len()) {
text.push(ch)
.assert_if_debugging_ui("Text buffer is too small");
}
// If we're in the pending state, paint the pending character at the end.
if let (Some(key), Some(press)) =
(self.multi_tap.pending_key(), self.multi_tap.pending_press())
{
assert!(!Self::keys()[key].is_empty());
// Now we can be sure that the looped iterator will return a value.
let ch = unwrap!(Self::keys()[key].chars().cycle().nth(press));
text.pop();
text.push(ch)
.assert_if_debugging_ui("Text buffer is too small");
}
}
shape::Text::new(text_baseline, text.as_str())
.with_font(style.font)
.with_fg(style.text_color)
.render(target);
// Paint the pending marker.
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
render_pending_marker(
target,
text_baseline,
text.as_str(),
style.font,
style.text_color,
);
}
// Paint the icon.
if let ButtonContent::Icon(icon) = self.button.content() {
// Icon is painted in the right-center point, of expected size 16x16 pixels, and
// 16px from the right edge.
let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0);
shape::ToifImage::new(icon_center, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.text_color)
.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.button.bounds(sink);

@ -5,6 +5,7 @@ use crate::ui::{
component::button::{Button, ButtonMsg},
theme,
},
shape::Renderer,
};
const NUMBERS: [u32; 5] = [12, 18, 20, 24, 33];
@ -57,6 +58,12 @@ impl Component for SelectWordCount {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
for btn in self.button.iter() {
btn.render(target)
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
for btn in self.button.iter() {

@ -4,10 +4,12 @@ use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
canvas::algo::PI4,
component::{Component, Event, EventCtx, Pad},
display::{self, toif::Icon, Color},
geometry::{Offset, Rect},
geometry::{Alignment2D, Offset, Rect},
model_tt::constant,
shape::{self, Renderer},
util::animation_disabled,
},
};
@ -206,6 +208,53 @@ impl Component for Loader {
);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful
// in automated tests.
// In practice, taking the current instant here is more precise in case some
// other component in the tree takes a long time to draw.
let now = Instant::now();
if let Some(progress) = self.progress(now) {
let style = if progress < display::LOADER_MAX {
self.styles.normal
} else {
self.styles.active
};
self.pad.render(target);
let center = self.pad.area.center();
let inactive_color = Color::black().blend(style.loader_color, 85);
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(inactive_color)
.render(target);
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(style.loader_color)
.with_end_angle(((progress as i32 * PI4 as i32 * 8) / 1000) as i16)
.render(target);
shape::Circle::new(center, constant::LOADER_INNER + 2)
.with_bg(style.loader_color)
.render(target);
shape::Circle::new(center, constant::LOADER_INNER)
.with_bg(style.background_color)
.render(target);
if let Some((icon, color)) = style.icon {
shape::ToifImage::new(center, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(color)
.render(target);
}
}
}
}
pub struct LoaderStyleSheet {

@ -11,7 +11,8 @@ use crate::{
Child, Component, Event, EventCtx, Pad,
},
display::{self, Font},
geometry::{Grid, Insets, Offset, Rect},
geometry::{Alignment, Grid, Insets, Offset, Rect},
shape::{self, Renderer},
},
};
@ -123,6 +124,14 @@ where
self.confirm_button.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.input.render(target);
self.paragraphs_pad.render(target);
self.paragraphs.render(target);
self.info_button.render(target);
self.confirm_button.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);
@ -231,6 +240,25 @@ impl Component for NumberInput {
self.inc.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let mut buf = [0u8; 10];
if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) {
let digit_font = Font::DEMIBOLD;
let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET;
shape::Bar::new(self.area).with_bg(theme::BG).render(target);
shape::Text::new(self.area.center() + Offset::y(y_offset), text)
.with_align(Alignment::Center)
.with_fg(theme::FG)
.with_font(digit_font)
.render(target);
}
self.dec.render(target);
self.inc.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.dec.bounds(sink);

@ -8,6 +8,7 @@ use crate::{
constant,
display::{self, Color},
geometry::{Insets, Rect},
shape::Renderer,
util::animation_disabled,
},
};
@ -17,6 +18,8 @@ use super::{
SwipeDirection,
};
use core::cell::Cell;
/// Allows pagination of inner component. Shows scroll bar, confirm & cancel
/// buttons. Optionally handles hold-to-confirm with loader.
pub struct ButtonPage<T, U> {
@ -40,7 +43,7 @@ pub struct ButtonPage<T, U> {
/// Whether to pass-through right swipe to parent component.
swipe_right: bool,
/// Fade to given backlight level on next paint().
fade: Option<u16>,
fade: Cell<Option<u16>>,
}
impl<T> ButtonPage<T, StrBuffer>
@ -76,7 +79,7 @@ where
cancel_from_any_page: false,
swipe_left: false,
swipe_right: false,
fade: None,
fade: Cell::new(None),
}
}
@ -156,7 +159,7 @@ where
// Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint.
self.fade = Some(theme::BACKLIGHT_NORMAL);
self.fade.set(Some(theme::BACKLIGHT_NORMAL));
}
fn is_cancel_visible(&self) -> bool {
@ -412,6 +415,33 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
match &self.loader {
Some(l) if l.is_animating() => self.loader.render(target),
_ => {
self.content.render(target);
if self.scrollbar.has_pages() {
self.scrollbar.render(target);
}
}
}
if self.button_cancel.is_some() && self.is_cancel_visible() {
self.button_cancel.render(target);
} else {
self.button_prev.render(target);
}
if self.scrollbar.has_next_page() {
self.button_next.render(target);
} else {
self.button_confirm.render(target);
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);

@ -4,6 +4,7 @@ use crate::{
error::Error,
strutil::StringType,
ui::{
canvas::algo::PI4,
component::{
base::ComponentExt,
paginated::Paginate,
@ -11,8 +12,10 @@ use crate::{
Child, Component, Event, EventCtx, Label, Never, Pad,
},
display::{self, Font},
geometry::{Insets, Rect},
geometry::{Insets, Offset, Rect},
model_tt::constant,
shape,
shape::Renderer,
util::animation_disabled,
},
};
@ -120,6 +123,47 @@ where
self.description.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
let center = constant::screen().center() + Offset::y(self.loader_y_offset);
let active_color = theme::FG;
let background_color = theme::BG;
let inactive_color = background_color.blend(active_color, 85);
let (start, end) = if self.indeterminate {
let start = (self.value as i16 - 100) % 1000;
let end = (self.value as i16 + 100) % 1000;
let start = ((start as i32 * 8 * PI4 as i32) / 1000) as i16;
let end = ((end as i32 * 8 * PI4 as i32) / 1000) as i16;
(start, end)
} else {
let end = ((self.value as i32 * 8 * PI4 as i32) / 1000) as i16;
(0, end)
};
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(inactive_color)
.render(target);
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(active_color)
.with_start_angle(start)
.with_end_angle(end)
.render(target);
shape::Circle::new(center, constant::LOADER_INNER + 2)
.with_bg(active_color)
.render(target);
shape::Circle::new(center, constant::LOADER_INNER)
.with_bg(background_color)
.render(target);
self.description_pad.render(target);
self.description.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(Self::AREA);

@ -6,6 +6,8 @@ use crate::{
display::{self, Color, Font, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
model_tt::theme::FG,
shape,
shape::Renderer,
},
};
@ -90,6 +92,20 @@ impl<T: AsRef<str>> Component for ResultFooter<'_, T> {
self.text.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// divider line
let bar = Rect::from_center_and_size(
Point::new(self.area.center().x, self.area.y0),
Offset::new(self.area.width(), 1),
);
shape::Bar::new(bar)
.with_fg(self.style.divider_color)
.render(target);
// footer text
self.text.render(target);
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
@ -164,4 +180,21 @@ impl<'a, T: StringType> Component for ResultScreen<'a, T> {
self.message.paint();
self.footer.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.footer_pad.render(target);
shape::ToifImage::new(
Point::new(screen().center().x, ICON_CENTER_Y),
self.icon.toif,
)
.with_align(Alignment2D::CENTER)
.with_fg(self.style.fg_color)
.with_bg(self.style.bg_color)
.render(target);
self.message.render(target);
self.footer.render(target);
}
}

@ -2,6 +2,8 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::toif::Icon,
geometry::{Alignment2D, Axis, LinearPlacement, Offset, Rect},
shape,
shape::Renderer,
};
use super::theme;
@ -122,6 +124,49 @@ impl Component for ScrollBar {
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
fn dotsize(distance: usize, nhidden: usize) -> Icon {
match (nhidden.saturating_sub(distance)).min(2 - distance) {
0 => theme::DOT_INACTIVE,
1 => theme::DOT_INACTIVE_HALF,
_ => theme::DOT_INACTIVE_QUARTER,
}
}
// Number of visible dots.
let num_shown = self.page_count.min(Self::MAX_DOTS);
// Page indices corresponding to the first (and last) dot.
let first_shown = self
.active_page
.saturating_sub(Self::MAX_DOTS / 2)
.min(self.page_count.saturating_sub(Self::MAX_DOTS));
let last_shown = first_shown + num_shown - 1;
let mut cursor = self.area.center()
- Offset::on_axis(
self.layout.axis,
Self::DOT_INTERVAL * (num_shown.saturating_sub(1) as i16) / 2,
);
for i in first_shown..(last_shown + 1) {
let icon = if i == self.active_page {
theme::DOT_ACTIVE
} else if i <= first_shown + 1 {
let before_first_shown = first_shown;
dotsize(i - first_shown, before_first_shown)
} else if i >= last_shown - 1 {
let after_last_shown = self.page_count - 1 - last_shown;
dotsize(last_shown - i, after_last_shown)
} else {
theme::DOT_INACTIVE
};
shape::ToifImage::new(cursor, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(theme::FG)
.render(target);
cursor = cursor + Offset::on_axis(self.layout.axis, Self::DOT_INTERVAL);
}
}
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
bounds

@ -2,6 +2,7 @@ use crate::ui::{
component::{base::ComponentExt, Component, Event, EventCtx, Pad, PageMsg, Paginate},
display::{self, Color},
geometry::{Axis, Insets, Rect},
shape::Renderer,
};
use super::{theme, ScrollBar, Swipe, SwipeDirection};
@ -165,6 +166,18 @@ where
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.content.render(target);
if self.scrollbar.has_pages() {
self.scrollbar.render(target);
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);

@ -3,6 +3,7 @@ use crate::ui::{
display,
event::TouchEvent,
geometry::{Point, Rect},
shape::Renderer,
};
use super::theme;
@ -159,4 +160,6 @@ impl Component for Swipe {
}
fn paint(&mut self) {}
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
}

@ -2,9 +2,11 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never},
geometry::{Alignment2D, Offset, Rect},
model_tt::theme,
shape,
shape::Renderer,
};
#[cfg(feature = "bootloader")]
use crate::ui::{display::Icon, model_tt::theme::bootloader::DEVICE_NAME};
use crate::ui::{display::{Icon, toif::Toif}, model_tt::theme::bootloader::DEVICE_NAME};
const TEXT_BOTTOM_MARGIN: i16 = 24; // matching the homescreen label margin
const ICON_TOP_MARGIN: i16 = 48;
@ -67,6 +69,40 @@ impl Component for WelcomeScreen {
theme::BG,
);
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let logo = if self.empty_lock {
theme::ICON_LOGO_EMPTY
} else {
theme::ICON_LOGO
};
shape::ToifImage::new(
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
logo.toif,
)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.with_bg(theme::BG)
.render(target);
#[cfg(not(feature = "bootloader"))]
shape::Text::new(
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
model::FULL_NAME,
)
.with_font(MODEL_NAME_FONT)
.with_fg(theme::FG)
.render(target);
#[cfg(feature = "bootloader")]
shape::ToifImage::new(
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
unwrap!(Toif::new(DEVICE_NAME)),
)
.with_align(Alignment2D::BOTTOM_CENTER)
.with_fg(theme::FG)
.render(target);
}
}
#[cfg(feature = "ui_debug")]

@ -7,8 +7,13 @@ pub const HEIGHT: i16 = DISPLAY_RESY as _;
pub const LINE_SPACE: i16 = 4;
pub const FONT_BPP: i16 = 4;
#[cfg(not(feature = "new_rendering"))]
pub const LOADER_OUTER: i16 = 60;
#[cfg(feature = "new_rendering")]
pub const LOADER_OUTER: i16 = 59;
pub const LOADER_INNER: i16 = 42;
pub const LOADER_ICON_MAX_SIZE: i16 = 64;
pub const fn size() -> Offset {

@ -3,13 +3,18 @@ use crate::micropython::buffer::StrBuffer;
use crate::ui::{
component::Component,
constant::screen,
display,
model_tt::{
component::{ErrorScreen, WelcomeScreen},
constant,
},
};
#[cfg(not(feature = "new_rendering"))]
use crate::ui::display;
#[cfg(feature = "new_rendering")]
use crate::ui::{display::Color, shape::render_on_display};
#[cfg(not(feature = "micropython"))]
// SAFETY: Actually safe but see below
unsafe fn get_str(text: &str) -> &str {
@ -31,6 +36,13 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
let mut frame = ErrorScreen::new(title, msg, footer);
frame.place(constant::screen());
#[cfg(feature = "new_rendering")]
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
#[cfg(not(feature = "new_rendering"))]
frame.paint();
}
@ -38,7 +50,16 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
extern "C" fn screen_boot_full() {
let mut frame = WelcomeScreen::new(false);
frame.place(screen());
display::sync();
frame.paint();
display::refresh();
#[cfg(feature = "new_rendering")]
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
#[cfg(not(feature = "new_rendering"))]
{
display::sync();
frame.paint();
display::refresh();
}
}

Loading…
Cancel
Save