Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/rust/bitbox-hal/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub trait Empty {}
pub trait Ui {
type Progress: Progress;
type Empty: Empty;
type UnlockAnimation;

/// Returns `Ok(())` if the user accepts, `Err(UserAbort)` if the user rejects.
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort>;
Expand All @@ -88,7 +89,9 @@ pub trait Ui {
longtouch: bool,
) -> Result<(), UserAbort>;

async fn unlock_animation(&mut self);
fn unlock_animation_create(&mut self) -> Self::UnlockAnimation;

async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation);

async fn status(&mut self, title: &str, status_success: bool);

Expand Down
9 changes: 9 additions & 0 deletions src/rust/bitbox-lvgl-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ fn main() -> Result<(), &'static str> {
println!("cargo::rerun-if-changed={}", lvgl_dir.display());

let target = env::var("TARGET").expect("TARGET not set");
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();

let mut cflags = vec![format!("-I{}", lvgl_dir.display())];
let mut cmake_build = cmake::Config::new(&lvgl_dir);
Expand Down Expand Up @@ -160,6 +162,13 @@ fn main() -> Result<(), &'static str> {
let dst = cmake_build.build();
println!("cargo::rustc-link-search=native={}/lib", dst.display());
println!("cargo::rustc-link-lib=static=lvgl");
if !target.starts_with("thumb") {
match (target_os.as_str(), target_env.as_str()) {
("macos", _) | ("ios", _) => println!("cargo::rustc-link-lib=dylib=c++"),
("windows", "msvc") => {}
_ => println!("cargo::rustc-link-lib=dylib=stdc++"),
}
}

let mut fonts = cc::Build::new();
fonts.file(manifest_dir.join("../../ui/fonts/inter_regular_32.c"));
Expand Down
10 changes: 5 additions & 5 deletions src/rust/bitbox-lvgl-sys/lv_conf.h
Original file line number Diff line number Diff line change
Expand Up @@ -584,11 +584,11 @@
#define LV_ATTRIBUTE_EXTERN_DATA

/** Use `float` as `lv_value_precise_t` */
#define LV_USE_FLOAT 0
#define LV_USE_FLOAT 1

/** Enable matrix support
* - Requires `LV_USE_FLOAT = 1` */
#define LV_USE_MATRIX 0
#define LV_USE_MATRIX 1

/** Include `lvgl_private.h` in `lvgl.h` to access internal data and functions by default */
#ifndef LV_USE_PRIVATE_API
Expand Down Expand Up @@ -773,7 +773,7 @@

#define LV_USE_LIST 1

#define LV_USE_LOTTIE 0 /**< Requires: lv_canvas, thorvg */
#define LV_USE_LOTTIE 1 /**< Requires: lv_canvas, thorvg */

#define LV_USE_MENU 1

Expand Down Expand Up @@ -996,11 +996,11 @@

/** Enable Vector Graphic APIs
* Requires `LV_USE_MATRIX = 1` */
#define LV_USE_VECTOR_GRAPHIC 0
#define LV_USE_VECTOR_GRAPHIC 1

/** Enable ThorVG (vector graphics library) from the src/libs folder.
* Requires LV_USE_VECTOR_GRAPHIC */
#define LV_USE_THORVG_INTERNAL 0
#define LV_USE_THORVG_INTERNAL 1

/** Enable ThorVG by assuming that its installed and linked to the project
* Requires LV_USE_VECTOR_GRAPHIC */
Expand Down
1 change: 1 addition & 0 deletions src/rust/bitbox-lvgl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub use widgets::class;
pub use widgets::image::ImageExt;
pub use widgets::keyboard::{KeyboardExt, LvKeyboard, LvKeyboardMapEntry, keyboard_def_event_cb};
pub use widgets::label::{LabelExt, LvLabel, LvLabelTextError};
pub use widgets::lottie::{LottieExt, LvLottie, LvLottieCreateError};
pub use widgets::obj;
pub use widgets::obj::LvObj;
pub use widgets::obj::{LvHandle, LvTypeError, ObjExt};
Expand Down
1 change: 0 additions & 1 deletion src/rust/bitbox-lvgl/src/widgets/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub struct ImageTag;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CanvasTag;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ArcTag;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down
148 changes: 148 additions & 0 deletions src/rust/bitbox-lvgl/src/widgets/lottie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: Apache-2.0

use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec;
use core::cell::RefCell;
use core::ffi::c_void;
use core::ptr::NonNull;

use super::canvas::{CanvasExt, LvCanvas};
use super::image::ImageExt;
use super::obj::ObjExt;
use crate::{LvHandle, LvObj, class, ffi};

#[derive(Debug, PartialEq, Eq)]
pub struct LvLottie {
inner: LvHandle<class::CanvasTag>,
}

type AnimationCallback = RefCell<Box<dyn FnMut() + 'static>>;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LvLottieCreateError {
InvalidDimensions,
CreateFailed,
EventRegistrationFailed,
}

unsafe extern "C" fn animation_completed_trampoline(anim: *mut ffi::lv_anim_t) {
let user_data = unsafe { ffi::lv_anim_get_user_data(anim) };
if user_data.is_null() {
return;
}

let callback_ptr = user_data.cast::<AnimationCallback>().cast_const();
unsafe {
Rc::increment_strong_count(callback_ptr);
}
let callback = unsafe { Rc::from_raw(callback_ptr) };
let mut callback = callback.borrow_mut();
callback.as_mut()();
}

unsafe extern "C" fn animation_deleted_trampoline(anim: *mut ffi::lv_anim_t) {
let user_data = unsafe { ffi::lv_anim_get_user_data(anim) };
if user_data.is_null() {
return;
}

drop(unsafe { Rc::from_raw(user_data.cast::<AnimationCallback>().cast_const()) });
}

pub trait LottieExt: CanvasExt {
fn set_src_data(&self, src: &'static [u8]) {
unsafe {
ffi::lv_lottie_set_src_data(self.as_ptr(), src.as_ptr().cast::<c_void>(), src.len())
}
}

fn pause(&self) {
unsafe { ffi::lv_anim_pause(self.animation()) }
}

fn resume(&self) {
unsafe { ffi::lv_anim_resume(self.animation()) }
}

fn set_repeat_count(&self, repeat_count: u32) {
unsafe { ffi::lv_anim_set_repeat_count(self.animation(), repeat_count) }
}

fn set_completed_cb<F>(&self, cb: F)
where
F: FnMut() + 'static,
{
let callback: Rc<AnimationCallback> = Rc::new(RefCell::new(Box::new(cb)));
let user_data = Rc::into_raw(callback).cast_mut().cast::<c_void>();
unsafe {
ffi::lv_anim_set_user_data(self.animation(), user_data);
ffi::lv_anim_set_completed_cb(self.animation(), Some(animation_completed_trampoline));
ffi::lv_anim_set_deleted_cb(self.animation(), Some(animation_deleted_trampoline));
}
}

fn animation(&self) -> *mut ffi::lv_anim_t {
unsafe { ffi::lv_lottie_get_anim(self.as_ptr()) }
}
}

impl LvLottie {
pub fn new<P: class::LvClass>(
parent: &LvHandle<P>,
width: u32,
height: u32,
) -> Result<Self, LvLottieCreateError> {
let Ok(width_i32) = i32::try_from(width) else {
return Err(LvLottieCreateError::InvalidDimensions);
};
let Ok(height_i32) = i32::try_from(height) else {
return Err(LvLottieCreateError::InvalidDimensions);
};
let Some(pixel_count) = width.checked_mul(height) else {
return Err(LvLottieCreateError::InvalidDimensions);
};
let Ok(pixel_count) = usize::try_from(pixel_count) else {
return Err(LvLottieCreateError::InvalidDimensions);
};

let Some(lottie) = NonNull::new(unsafe { ffi::lv_lottie_create(parent.as_ptr()) }) else {
return Err(LvLottieCreateError::CreateFailed);
};
let lottie = LvLottie {
inner: LvHandle::from_ptr(lottie),
};

let attachment = super::util::attach_to_object(&lottie.inner, vec![0u32; pixel_count])
.map_err(|_| LvLottieCreateError::EventRegistrationFailed)?;

unsafe {
ffi::lv_lottie_set_buffer(
lottie.as_ptr(),
width_i32,
height_i32,
(*attachment.as_ptr()).as_mut_ptr().cast::<c_void>(),
)
};

Ok(lottie)
}

pub fn to_canvas(self) -> LvCanvas {
self.inner
}

pub fn to_obj(self) -> LvObj {
self.inner.cast()
}
}

impl ObjExt for LvLottie {
fn as_ptr(&self) -> *mut ffi::lv_obj_t {
self.inner.as_ptr()
}
}

impl ImageExt for LvLottie {}
impl CanvasExt for LvLottie {}
impl LottieExt for LvLottie {}
1 change: 1 addition & 0 deletions src/rust/bitbox-lvgl/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod class;
pub mod image;
pub mod keyboard;
pub mod label;
pub mod lottie;
pub mod obj;
pub mod slider;
pub mod span;
Expand Down
2 changes: 2 additions & 0 deletions src/rust/bitbox-platform-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ zeroize = { workspace = true }

[features]
app-u2f = ["bitbox-hal/app-u2f"]
simulator-graphical = []
testing = []

[dev-dependencies]
async_test = { path = "../async_test" }
Expand Down
9 changes: 9 additions & 0 deletions src/rust/bitbox-platform-host/src/securechip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
use alloc::collections::VecDeque;
use alloc::{boxed::Box, vec::Vec};

#[cfg(feature = "simulator-graphical")]
use bitbox_hal::Timer;

use bitcoin::hashes::Hash;
use hex_lit::hex;

Expand Down Expand Up @@ -128,6 +131,12 @@ impl bitbox_hal::SecureChip for FakeSecureChip {
));
engine.input(msg);
let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);

// Keep KDF completion visibly delayed on the host so unlock animation start/play can be
// manually tested in the graphical simulators.
#[cfg(all(feature = "simulator-graphical", not(feature = "testing")))]
crate::timer::HostTimer::delay_for(core::time::Duration::from_millis(1000)).await;

Ok(Box::new(zeroize::Zeroizing::new(
hmac_result.to_byte_array(),
)))
Expand Down
1 change: 1 addition & 0 deletions src/rust/bitbox02-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ app-cardano = [

testing = [
"dep:bitbox-platform-host",
"bitbox-platform-host?/testing",
"bitbox02/testing",
"bitbox-secp256k1/testing",
"util/testing"
Expand Down
25 changes: 24 additions & 1 deletion src/rust/bitbox02-rust/src/hal/testing/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub enum Screen {
choices: Vec<String>,
selected: u8,
},
UnlockAnimationPaused,
UnlockAnimationPlayed,
More,
}

Expand Down Expand Up @@ -78,9 +80,12 @@ pub struct NoopEmpty;

impl Empty for NoopEmpty {}

pub struct NoopUnlockAnimation;

impl Ui for TestingUi<'_> {
type Progress = NoopProgress;
type Empty = NoopEmpty;
type UnlockAnimation = NoopUnlockAnimation;

fn progress_create(&mut self, _title: &str) -> Self::Progress {
NoopProgress
Expand All @@ -90,6 +95,11 @@ impl Ui for TestingUi<'_> {
NoopEmpty
}

fn unlock_animation_create(&mut self) -> Self::UnlockAnimation {
self.screens.push(Screen::UnlockAnimationPaused);
NoopUnlockAnimation
}

async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> {
self.confirm_display_sizes.push(params.display_size);
self.screens.push(Screen::Confirm {
Expand Down Expand Up @@ -159,7 +169,9 @@ impl Ui for TestingUi<'_> {
Ok(())
}

async fn unlock_animation(&mut self) {}
async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) {
self.screens.push(Screen::UnlockAnimationPlayed);
}

async fn status(&mut self, title: &str, status_success: bool) {
self.screens.push(Screen::Status {
Expand Down Expand Up @@ -513,4 +525,15 @@ mod tests {
Err(UserAbort)
));
}

#[async_test::test]
async fn test_unlock_animation_records_screen() {
let mut ui = TestingUi::new();
let animation = ui.unlock_animation_create();
ui.unlock_animation_play(animation).await;
assert_eq!(
ui.screens,
vec![Screen::UnlockAnimationPaused, Screen::UnlockAnimationPlayed]
);
}
}
Loading
Loading