Add ui component toast from iced repo examples

This commit is contained in:
edouardparis 2023-11-20 18:09:44 +01:00
parent 71bc585ed3
commit a539cfe157
6 changed files with 490 additions and 82 deletions

View File

@ -31,7 +31,7 @@ pub enum Message {
SpendTxs(Result<Vec<SpendTx>, Error>),
Psbt(Result<Psbt, Error>),
Recovery(Result<SpendTx, Error>),
Signed(Result<(Psbt, Fingerprint), Error>),
Signed(Fingerprint, Result<Psbt, Error>),
WalletRegistered(Result<Fingerprint, Error>),
Updated(Result<(), Error>),
Saved(Result<(), Error>),

View File

@ -10,6 +10,7 @@ use liana::{
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network},
};
use liana_ui::component::toast;
use liana_ui::{
component::{form, modal},
widget::Element,
@ -132,12 +133,26 @@ impl PsbtState {
match &message {
Message::View(view::Message::Spend(msg)) => match msg {
view::SpendTxMessage::Cancel => {
if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) =
&mut self.action
{
*display_modal = false;
return Command::none();
}
self.action = None;
}
view::SpendTxMessage::Delete => {
self.action = Some(PsbtAction::Delete(DeleteAction::default()));
}
view::SpendTxMessage::Sign => {
if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) =
&mut self.action
{
*display_modal = true;
return Command::none();
}
let action = SignAction::new(
self.tx.signers(),
self.wallet.clone(),
@ -365,12 +380,12 @@ impl Action for DeleteAction {
pub struct SignAction {
wallet: Arc<Wallet>,
chosen_hw: Option<usize>,
processing: bool,
hws: HardwareWallets,
error: Option<Error>,
signing: HashSet<Fingerprint>,
signed: HashSet<Fingerprint>,
is_saved: bool,
display_modal: bool,
}
impl SignAction {
@ -382,13 +397,13 @@ impl SignAction {
is_saved: bool,
) -> Self {
Self {
chosen_hw: None,
processing: false,
signing: HashSet::new(),
hws: HardwareWallets::new(datadir_path, network).with_wallet(wallet.clone()),
wallet,
error: None,
signed,
is_saved,
display_modal: true,
}
}
}
@ -416,61 +431,61 @@ impl Action for SignAction {
..
}) = self.hws.list.get(i)
{
self.chosen_hw = Some(i);
self.processing = true;
self.display_modal = false;
self.signing.insert(*fingerprint);
let psbt = tx.psbt.clone();
let fingerprint = *fingerprint;
return Command::perform(
sign_psbt(self.wallet.clone(), device.clone(), *fingerprint, psbt),
Message::Signed,
sign_psbt(self.wallet.clone(), device.clone(), psbt),
move |res| Message::Signed(fingerprint, res),
);
}
}
Message::View(view::Message::Spend(view::SpendTxMessage::SelectHotSigner)) => {
self.processing = true;
return Command::perform(
sign_psbt_with_hot_signer(self.wallet.clone(), tx.psbt.clone()),
Message::Signed,
|(fg, res)| Message::Signed(fg, res),
);
}
Message::Signed(res) => match res {
Err(e) => self.error = Some(e),
Ok((psbt, fingerprint)) => {
self.error = None;
self.signed.insert(fingerprint);
let daemon = daemon.clone();
tx.psbt = psbt.clone();
if self.is_saved {
return Command::perform(
async move { daemon.update_spend_tx(&psbt).map_err(|e| e.into()) },
Message::Updated,
);
// If the spend transaction was never saved before, then both the psbt and
// labels attached to it must be updated.
} else {
let mut labels = HashMap::<LabelItem, Option<String>>::new();
for (item, label) in tx.labels() {
if !label.is_empty() {
labels.insert(label_item_from_str(item), Some(label.clone()));
Message::Signed(fingerprint, res) => {
self.signing.remove(&fingerprint);
match res {
Err(e) => self.error = Some(e),
Ok(psbt) => {
self.error = None;
self.signed.insert(fingerprint);
let daemon = daemon.clone();
tx.psbt = psbt.clone();
if self.is_saved {
return Command::perform(
async move { daemon.update_spend_tx(&psbt).map_err(|e| e.into()) },
Message::Updated,
);
// If the spend transaction was never saved before, then both the psbt and
// labels attached to it must be updated.
} else {
let mut labels = HashMap::<LabelItem, Option<String>>::new();
for (item, label) in tx.labels() {
if !label.is_empty() {
labels.insert(label_item_from_str(item), Some(label.clone()));
}
}
return Command::perform(
async move {
daemon.update_spend_tx(&psbt)?;
daemon.update_labels(&labels).map_err(|e| e.into())
},
Message::Updated,
);
}
return Command::perform(
async move {
daemon.update_spend_tx(&psbt)?;
daemon.update_labels(&labels).map_err(|e| e.into())
},
Message::Updated,
);
}
}
},
}
Message::Updated(res) => match res {
Ok(()) => {
self.processing = false;
match self.wallet.main_descriptor.partial_spend_info(&tx.psbt) {
Ok(sigs) => tx.sigs = sigs,
Err(e) => self.error = Some(Error::Unexpected(e.to_string())),
}
}
Ok(()) => match self.wallet.main_descriptor.partial_spend_info(&tx.psbt) {
Ok(sigs) => tx.sigs = sigs,
Err(e) => self.error = Some(Error::Unexpected(e.to_string())),
},
Err(e) => self.error = Some(e),
},
@ -482,56 +497,62 @@ impl Action for SignAction {
self.error = Some(e.into());
}
},
Message::View(view::Message::Reload) => {
self.chosen_hw = None;
self.error = None;
return self.load(daemon);
}
_ => {}
};
Command::none()
}
fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> {
modal::Modal::new(
let content = toast::Manager::new(
content,
view::psbt::sign_action(
self.error.as_ref(),
&self.hws.list,
self.wallet.signer.as_ref().map(|s| s.fingerprint()),
self.wallet
.signer
.as_ref()
.and_then(|signer| self.wallet.keys_aliases.get(&signer.fingerprint)),
self.processing,
self.chosen_hw,
&self.signed,
),
view::psbt::sign_action_toasts(&self.hws.list, &self.signing),
)
.on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))
.into()
.into();
if self.display_modal {
modal::Modal::new(
content,
view::psbt::sign_action(
self.error.as_ref(),
&self.hws.list,
self.wallet.signer.as_ref().map(|s| s.fingerprint()),
self.wallet
.signer
.as_ref()
.and_then(|signer| self.wallet.keys_aliases.get(&signer.fingerprint)),
&self.signed,
&self.signing,
),
)
.on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))
.into()
} else {
content
}
}
}
async fn sign_psbt_with_hot_signer(
wallet: Arc<Wallet>,
psbt: Psbt,
) -> Result<(Psbt, Fingerprint), Error> {
) -> (Fingerprint, Result<Psbt, Error>) {
if let Some(signer) = &wallet.signer {
let psbt = signer.sign_psbt(psbt).map_err(|e| {
WalletError::HotSigner(format!("Hot signer failed to sign psbt: {}", e))
})?;
Ok((psbt, signer.fingerprint()))
let res = signer
.sign_psbt(psbt)
.map_err(|e| WalletError::HotSigner(format!("Hot signer failed to sign psbt: {}", e)))
.map_err(|e| e.into());
(signer.fingerprint(), res)
} else {
Err(WalletError::HotSigner("Hot signer not loaded".to_string()).into())
(
Fingerprint::default(),
Err(WalletError::HotSigner("Hot signer not loaded".to_string()).into()),
)
}
}
async fn sign_psbt(
wallet: Arc<Wallet>,
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint,
mut psbt: Psbt,
) -> Result<(Psbt, Fingerprint), Error> {
) -> Result<Psbt, Error> {
// The BitBox02 is only going to produce a signature for a single key in the Script. In order
// to make sure it doesn't sign for a public key from another spending path we remove the BIP32
// derivation for the other paths.
@ -560,7 +581,7 @@ async fn sign_psbt(
} else {
hw.sign_tx(&mut psbt).await.map_err(Error::from)?;
}
Ok((psbt, fingerprint))
Ok(psbt)
}
pub struct UpdateAction {

View File

@ -11,9 +11,8 @@ use async_hwi::DeviceKind;
pub fn hw_list_view(
i: usize,
hw: &HardwareWallet,
chosen: bool,
processing: bool,
signed: bool,
signing: bool,
) -> Element<Message> {
let mut bttn = Button::new(match hw {
HardwareWallet::Supported {
@ -24,7 +23,7 @@ pub fn hw_list_view(
registered,
..
} => {
if chosen && processing {
if signing {
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
} else if signed {
hw::sign_success_hardware_wallet(
@ -61,7 +60,7 @@ pub fn hw_list_view(
})
.style(theme::Button::Border)
.width(Length::Fill);
if !processing {
if !signing {
if let HardwareWallet::Supported { registered, .. } = hw {
if *registered != Some(false) {
bttn = bttn.on_press(Message::SelectHardwareWallet(i));

View File

@ -941,9 +941,8 @@ pub fn sign_action<'a>(
hws: &'a [HardwareWallet],
signer: Option<Fingerprint>,
signer_alias: Option<&'a String>,
processing: bool,
chosen_hw: Option<usize>,
signed: &HashSet<Fingerprint>,
signing: &HashSet<Fingerprint>,
) -> Element<'a, Message> {
Column::new()
.push_maybe(warning.map(|w| warn(Some(w))))
@ -963,11 +962,12 @@ pub fn sign_action<'a>(
col.push(hw_list_view(
i,
hw,
Some(i) == chosen_hw,
processing,
hw.fingerprint()
.map(|f| signed.contains(&f))
.unwrap_or(false),
hw.fingerprint()
.map(|f| signing.contains(&f))
.unwrap_or(false),
))
},
))
@ -992,6 +992,41 @@ pub fn sign_action<'a>(
.into()
}
pub fn sign_action_toasts<'a>(
hws: &'a [HardwareWallet],
signing: &HashSet<Fingerprint>,
) -> Vec<Element<'a, Message>> {
hws.iter()
.filter_map(|hw| {
if let HardwareWallet::Supported {
kind,
fingerprint,
version,
alias,
..
} = &hw
{
if signing.contains(fingerprint) {
Some(
card::simple(liana_ui::component::hw::processing_hardware_wallet(
kind,
version.as_ref(),
fingerprint,
alias.as_ref(),
))
.width(Length::Fixed(500.0))
.into(),
)
} else {
None
}
} else {
None
}
})
.collect()
}
pub fn update_spend_view<'a>(
psbt: String,
updated: &form::Value<String>,

View File

@ -9,6 +9,7 @@ pub mod hw;
pub mod modal;
pub mod notification;
pub mod text;
pub mod toast;
pub mod tooltip;
pub use tooltip::tooltip;

View File

@ -0,0 +1,352 @@
use std::fmt;
use std::time::{Duration, Instant};
use super::card;
use super::text::h1;
use super::theme::Theme;
use crate::widget::*;
use iced::{Alignment, Element, Length, Point, Rectangle, Size, Vector};
use iced_native::widget::{Operation, Tree};
use iced_native::{event, layout, mouse, overlay, renderer, window};
use iced_native::{Clipboard, Event, Layout, Shell, Widget};
pub trait Toast {
fn title(&self) -> &str;
fn body(&self) -> &str;
}
pub struct Manager<'a, Message, Renderer> {
content: Element<'a, Message, Renderer>,
toasts: Vec<Element<'a, Message, Renderer>>,
}
impl<'a, Message> Manager<'a, Message, iced::Renderer<Theme>>
where
Message: 'a + Clone,
{
pub fn new(
content: impl Into<Element<'a, Message, iced::Renderer<Theme>>>,
toasts: Vec<Element<'a, Message, iced::Renderer<Theme>>>,
) -> Self {
Self {
content: content.into(),
toasts: toasts.into_iter().collect(),
}
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for Manager<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
{
fn width(&self) -> Length {
self.content.as_widget().width()
}
fn height(&self) -> Length {
self.content.as_widget().height()
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
self.content.as_widget().layout(renderer, limits)
}
fn tag(&self) -> iced_native::widget::tree::Tag {
struct Marker(Vec<Instant>);
iced_native::widget::tree::Tag::of::<Marker>()
}
fn state(&self) -> iced_native::widget::tree::State {
iced_native::widget::tree::State::new(Vec::<Option<Instant>>::new())
}
fn children(&self) -> Vec<Tree> {
std::iter::once(Tree::new(&self.content))
.chain(self.toasts.iter().map(Tree::new))
.collect()
}
fn diff(&self, tree: &mut Tree) {
let instants = tree.state.downcast_mut::<Vec<Option<Instant>>>();
// Invalidating removed instants to None allows us to remove
// them here so that diffing for removed / new toast instants
// is accurate
instants.retain(Option::is_some);
match (instants.len(), self.toasts.len()) {
(old, new) if old > new => {
instants.truncate(new);
}
(old, new) if old < new => {
instants.extend(std::iter::repeat(Some(Instant::now())).take(new - old));
}
_ => {}
}
tree.diff_children(
&std::iter::once(&self.content)
.chain(self.toasts.iter())
.collect::<Vec<_>>(),
);
}
fn operate(
&self,
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.content
.as_widget()
.operate(&mut state.children[0], layout, renderer, operation);
});
}
fn on_event(
&mut self,
state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.content.as_widget_mut().on_event(
&mut state.children[0],
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
}
fn draw(
&self,
state: &Tree,
renderer: &mut Renderer,
theme: &<Renderer as iced_native::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&state.children[0],
renderer,
theme,
style,
layout,
cursor_position,
viewport,
);
}
fn mouse_interaction(
&self,
state: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&state.children[0],
layout,
cursor_position,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let instants = state.state.downcast_mut::<Vec<Option<Instant>>>();
let (content_state, toasts_state) = state.children.split_at_mut(1);
let content = self
.content
.as_widget_mut()
.overlay(&mut content_state[0], layout, renderer);
let toasts = (!self.toasts.is_empty()).then(|| {
overlay::Element::new(
layout.bounds().position(),
Box::new(Overlay {
toasts: &mut self.toasts,
state: toasts_state,
instants,
}),
)
});
let overlays = content.into_iter().chain(toasts).collect::<Vec<_>>();
(!overlays.is_empty()).then(|| overlay::Group::with_children(overlays).overlay())
}
}
struct Overlay<'a, 'b, Message, Renderer> {
toasts: &'b mut [Element<'a, Message, Renderer>],
state: &'b mut [Tree],
instants: &'b mut [Option<Instant>],
}
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, 'b, Message, Renderer>
where
Renderer: iced_native::Renderer,
{
fn layout(&self, renderer: &Renderer, bounds: Size, position: Point) -> layout::Node {
let limits = layout::Limits::new(Size::ZERO, bounds)
.width(Length::Fill)
.height(Length::Fill);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
10.into(),
10.0,
Alignment::End,
self.toasts,
)
.translate(Vector::new(position.x, position.y))
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.toasts
.iter_mut()
.zip(self.state.iter_mut())
.zip(layout.children())
.zip(self.instants.iter_mut())
.map(|(((child, state), layout), instant)| {
let mut local_messages = vec![];
let mut local_shell = Shell::new(&mut local_messages);
let status = child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor_position,
renderer,
clipboard,
&mut local_shell,
);
if !local_shell.is_empty() {
instant.take();
}
shell.merge(local_shell, std::convert::identity);
status
})
.fold(event::Status::Ignored, event::Status::merge)
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &<Renderer as iced_native::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let viewport = layout.bounds();
for ((child, state), layout) in self
.toasts
.iter()
.zip(self.state.iter())
.zip(layout.children())
{
child.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor_position,
&viewport,
);
}
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_native::widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.toasts
.iter()
.zip(self.state.iter_mut())
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.operate(state, layout, renderer, operation);
})
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.toasts
.iter()
.zip(self.state.iter())
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
state,
layout,
cursor_position,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()
}
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
layout
.children()
.any(|layout| layout.bounds().contains(cursor_position))
}
}
impl<'a, Message, Renderer> From<Manager<'a, Message, Renderer>> for Element<'a, Message, Renderer>
where
Renderer: 'a + iced_native::Renderer,
Message: 'a,
{
fn from(manager: Manager<'a, Message, Renderer>) -> Self {
Element::new(manager)
}
}