diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 06ade039d4..80f7a493f8 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -17,7 +17,7 @@ use super::firmware::{ AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, - SelectWordScreen, TextScreen, TextScreenMsg, + SelectWordScreen, SetBrightnessMsg, SetBrightnessScreen, TextScreen, TextScreenMsg, }; impl ComponentMsgObj for PinKeyboard<'_> { @@ -128,3 +128,12 @@ impl ComponentMsgObj for ConfirmHomescreen { } } } + +impl ComponentMsgObj for SetBrightnessScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + SetBrightnessMsg::Confirmed => Ok(CONFIRMED.as_obj()), + SetBrightnessMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/brightness_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/brightness_screen.rs new file mode 100644 index 0000000000..e3594e336e --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/brightness_screen.rs @@ -0,0 +1,216 @@ +use crate::{ + storage, + translations::TR, + trezorhal::display, + ui::{ + component::{Component, Event, EventCtx}, + event::TouchEvent, + geometry::{Alignment2D, Insets, Offset, Point, Rect}, + shape::{Bar, Renderer}, + }, +}; + +use super::super::{ + constant::SCREEN, + firmware::{Header, HeaderMsg}, + theme, +}; + +pub struct SetBrightnessScreen { + header: Header, + slider: VerticalSlider, +} + +pub enum SetBrightnessMsg { + Confirmed, + Cancelled, +} + +impl SetBrightnessScreen { + const SLIDER_HEIGHT: i16 = 392; + pub fn new(min: u16, max: u16, init_value: u16) -> Self { + Self { + header: Header::new(TR::brightness__title.into()).with_close_button(), + slider: VerticalSlider::new(min, max, init_value), + } + } +} + +impl Component for SetBrightnessScreen { + type Msg = SetBrightnessMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + // assert full screen + debug_assert_eq!(bounds.height(), SCREEN.height()); + debug_assert_eq!(bounds.width(), SCREEN.width()); + + let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT); + let (slider_area, _) = rest.split_top(Self::SLIDER_HEIGHT); + + self.header.place(header_area); + self.slider.place(slider_area); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(HeaderMsg::Cancelled) = self.header.event(ctx, event) { + return Some(SetBrightnessMsg::Cancelled); + } + + if let Some(value) = self.slider.event(ctx, event) { + unwrap!(storage::set_brightness(value as _)); + return Some(SetBrightnessMsg::Confirmed); + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.header.render(target); + self.slider.render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for SetBrightnessScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("SetBrightnessScreen"); + t.child("Header", &self.header); + t.child("Slider", &self.slider); + } +} + +struct VerticalSlider { + area: Rect, + touch_area: Rect, + min: u16, + max: u16, + value: u16, + val_pct: u16, + touching: bool, +} + +impl VerticalSlider { + const SLIDER_WIDTH: i16 = 120; + + pub fn new(min: u16, max: u16, value: u16) -> Self { + debug_assert!(min < max); + let value = value.clamp(min, max); + Self { + area: Rect::zero(), + touch_area: Rect::zero(), + min, + max, + value, + val_pct: 0, + touching: false, + } + } + + pub fn update_value(&mut self, pos: Point, ctx: &mut EventCtx) { + // Area where slider value is not saturated + let proportional_area = self.area.inset(Insets::new( + Self::SLIDER_WIDTH / 2, + 0, + Self::SLIDER_WIDTH / 2, + 0, + )); + + let filled = (proportional_area.y1 - pos.y).clamp(0, proportional_area.height()); + let val_pct = (filled as u16 * 100) / proportional_area.height() as u16; + let val = (val_pct * (self.max - self.min)) / 100 + self.min; + + if val != self.value { + ctx.request_paint(); + self.value = val; + } + } +} + +impl Component for VerticalSlider { + type Msg = u8; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = Rect::snap( + bounds.center(), + Offset::new(Self::SLIDER_WIDTH, bounds.height()), + Alignment2D::CENTER, + ); + self.touch_area = self.area.outset(Insets::uniform(20)); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Touch(touch_event) = event { + match touch_event { + TouchEvent::TouchStart(pos) if self.touch_area.contains(pos) => { + // Detect only touches inside the touch area + self.touching = true; + self.update_value(pos, ctx); + display::backlight(self.value as _); + ctx.request_paint(); + } + TouchEvent::TouchMove(pos) if self.touching => { + self.update_value(pos, ctx); + // Update only if the touch started inside the touch area + display::backlight(self.value as _); + } + TouchEvent::TouchEnd(pos) if self.touching => { + self.touching = false; + self.update_value(pos, ctx); + ctx.request_paint(); + // Confirm the value only if the touch ended inside the touch area + if self.touch_area.contains(pos) { + return Some(self.value as _); + } else { + display::backlight(self.value as _); + } + } + _ => {} + }; + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let val_pct = ((100 * (self.value - self.min)) / (self.max - self.min)).clamp(0, 100); + + // Square area for the slider + let (_, small_area) = self.area.split_bottom(Self::SLIDER_WIDTH); + + // Background pad + Bar::new(self.area) + .with_radius(12) + .with_bg(theme::GREY_EXTRA_DARK) + .render(target); + + // Moving slider + Bar::new(small_area.translate( + Offset::y(val_pct as i16 * (self.area.height() - Self::SLIDER_WIDTH) / 100).neg(), + )) + .with_radius(4) + .with_bg(theme::GREY_LIGHT) + .render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for VerticalSlider { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("VerticalSlider"); + t.int("value", self.value as i64); + } +} + +#[cfg(test)] +mod tests { + use super::{super::super::constant::SCREEN, *}; + + #[test] + fn test_component_heights_fit_screen() { + assert!( + SetBrightnessScreen::SLIDER_HEIGHT + Header::HEADER_HEIGHT <= SCREEN.height(), + "Components overflow the screen height", + ); + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs index 900800a2bf..5da82a8ffc 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -1,4 +1,5 @@ mod action_bar; +mod brightness_screen; mod confirm_homescreen; mod header; mod hint; @@ -14,6 +15,7 @@ mod vertical_menu; mod vertical_menu_screen; pub use action_bar::{ActionBar, ActionBarMsg}; +pub use brightness_screen::{SetBrightnessMsg, SetBrightnessScreen}; pub use confirm_homescreen::{ConfirmHomescreen, ConfirmHomescreenMsg}; pub use header::{Header, HeaderMsg}; pub use hint::Hint; diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 228d7eca84..3bbf6a5979 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -34,7 +34,7 @@ use super::{ firmware::{ ActionBar, Bip39Input, ConfirmHomescreen, Header, HeaderMsg, Hint, Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen, SelectWordScreen, - Slip39Input, TextScreen, + SetBrightnessScreen, Slip39Input, TextScreen, }, flow, fonts, theme, UIEckhart, }; @@ -648,8 +648,14 @@ impl FirmwareUI for UIEckhart { Ok(layout) } - fn set_brightness(_current_brightness: Option) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + fn set_brightness(current_brightness: Option) -> Result { + let content = SetBrightnessScreen::new( + theme::backlight::get_backlight_min() as u16, + theme::backlight::get_backlight_max() as u16, + current_brightness.unwrap_or(theme::backlight::get_backlight_normal()) as u16, + ); + let layout = RootComponent::new(content); + Ok(layout) } fn show_address_details(