Merge #95: Add check height to coins panel

34b833f1484f1979087ad76847cc2942c06d0817 Add check height to coins panel (edouard)

Pull request description:

  close #82

ACKs for top commit:
  edouardparis:
    Self-ACK 34b833f1484f1979087ad76847cc2942c06d0817

Tree-SHA512: 0d7ff21265d9efd6aabe17dcdca482a13b906a46027f90e43241191e5dd4f74cf5056832e54052bc995258a5ff75a3f15b5a159c127f00049402249a84caa668
This commit is contained in:
edouard 2022-11-17 11:56:55 +01:00
commit 1de3678409
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
8 changed files with 347 additions and 33 deletions

81
gui/Cargo.lock generated
View File

@ -23,6 +23,12 @@ dependencies = [
"mach 0.1.2",
]
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "ab_glyph"
version = "0.2.15"
@ -86,6 +92,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "approx"
version = "0.5.1"
@ -1159,6 +1171,17 @@ dependencies = [
"thiserror",
]
[[package]]
name = "iced_lazy"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333979d705964832864ee7676516ab3c3df4ab0b65efb603c86a256d4adbec6f"
dependencies = [
"iced_native",
"iced_pure",
"ouroboros",
]
[[package]]
name = "iced_native"
version = "0.5.1"
@ -1624,7 +1647,9 @@ dependencies = [
"dirs",
"fern",
"iced",
"iced_lazy",
"iced_native",
"iced_pure",
"log",
"minisafe",
"serde",
@ -1920,6 +1945,30 @@ dependencies = [
"num-traits",
]
[[package]]
name = "ouroboros"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f357ef82d1b4db66fbed0b8d542cbd3c22d0bf5b393b3c257b9ba4568e70c9c3"
dependencies = [
"aliasable",
"ouroboros_macro",
"stable_deref_trait",
]
[[package]]
name = "ouroboros_macro"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44a0b52c2cbaef7dffa5fec1a43274afe8bd2a644fa9fc50a9ef4ff0269b1257"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "owned_ttf_parser"
version = "0.15.0"
@ -2012,6 +2061,30 @@ dependencies = [
"toml",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.41"
@ -2537,6 +2610,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -2725,7 +2804,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"rand 0.8.5",
"static_assertions",
]

View File

@ -21,6 +21,8 @@ base64 = "0.13"
iced = { version = "0.4", default-features= false, features = ["tokio", "wgpu", "svg", "qr_code", "pure"] }
iced_native = "0.5"
iced_lazy = { version = "0.1.1", features = ["pure"] }
iced_pure = "0.2.2"
tokio = {version = "1.21.0", features = ["signal"]}
serde = { version = "1.0", features = ["derive"] }

View File

@ -65,7 +65,11 @@ impl App {
)
.into(),
menu::Menu::Home => Home::new(&self.cache.coins).into(),
menu::Menu::Coins => CoinsPanel::new(&self.cache.coins).into(),
menu::Menu::Coins => CoinsPanel::new(
&self.cache.coins,
self.daemon.config().main_descriptor.timelock_value(),
)
.into(),
menu::Menu::Receive => ReceivePanel::default().into(),
menu::Menu::Spend => SpendPanel::new(self.config.clone(), &self.cache.spend_txs).into(),
menu::Menu::CreateSpendTx => {

View File

@ -12,14 +12,26 @@ pub struct CoinsPanel {
coins: Vec<Coin>,
selected_coin: Option<usize>,
warning: Option<Error>,
/// timelock value to pass for the heir to consume a coin.
timelock: u32,
}
impl CoinsPanel {
pub fn new(coins: &[Coin]) -> Self {
pub fn new(coins: &[Coin], timelock: u32) -> Self {
Self {
coins: coins.to_owned(),
coins: coins
.iter()
.filter_map(|coin| {
if coin.spend_info.is_none() {
Some(*coin)
} else {
None
}
})
.collect(),
selected_coin: None,
warning: None,
timelock,
}
}
}
@ -30,7 +42,7 @@ impl State for CoinsPanel {
&Menu::Coins,
cache,
self.warning.as_ref(),
view::coins::coins_view(&self.coins),
view::coins::coins_view(cache, &self.coins, self.timelock),
)
}
@ -45,7 +57,16 @@ impl State for CoinsPanel {
Err(e) => self.warning = Some(e),
Ok(coins) => {
self.warning = None;
self.coins = coins;
self.coins = coins
.iter()
.filter_map(|coin| {
if coin.spend_info.is_none() {
Some(*coin)
} else {
None
}
})
.collect();
}
},
Message::View(view::Message::Close) => {

View File

@ -1,16 +1,21 @@
use iced::{
pure::{button, column, container, row, Element},
pure::{column, container, row, Element},
Alignment, Length,
};
use crate::ui::{
component::{badge, button::Style, card, text::*},
color,
component::{badge, card, collapse::collapse, separation, text::*},
icon,
util::Collection,
};
use crate::{app::view::message::Message, daemon::model::Coin};
use crate::{
app::{cache::Cache, view::message::Message},
daemon::model::Coin,
};
pub fn coins_view<'a>(coins: &[Coin]) -> Element<'a, Message> {
pub fn coins_view<'a>(cache: &Cache, coins: &'a [Coin], timelock: u32) -> Element<'a, Message> {
column()
.push(
container(
@ -21,32 +26,64 @@ pub fn coins_view<'a>(coins: &[Coin]) -> Element<'a, Message> {
.width(Length::Fill),
)
.push(
column().spacing(10).push(
coins
.iter()
.enumerate()
.fold(column().spacing(10), |col, (i, coin)| {
col.push(coin_list_view(i, coin))
}),
),
column()
.spacing(10)
.push(coins.iter().fold(column().spacing(10), |col, coin| {
col.push(coin_list_view(coin, timelock, cache.blockheight as u32))
})),
)
.align_items(Alignment::Center)
.spacing(20)
.into()
}
fn coin_list_view<'a>(i: usize, coin: &Coin) -> Element<'a, Message> {
container(
button(
row()
#[allow(clippy::collapsible_else_if)]
fn coin_list_view(coin: &Coin, timelock: u32, blockheight: u32) -> Element<Message> {
container(collapse::<_, _, _, _, _>(
move || {
row::<Message, _>()
.push(
row()
.push(badge::coin())
.push_maybe(coin.spend_info.map(|_| {
container(text(" Spent ").small())
.padding(3)
.style(badge::PillStyle::Success)
}))
.push_maybe(if coin.spend_info.is_some() {
Some(
container(text(" Spent ").small())
.padding(3)
.style(badge::PillStyle::Success),
)
} else {
if let Some(b) = coin.block_height {
if blockheight > b as u32 + timelock {
Some(container(
row()
.spacing(5)
.push(text(" 0").small().color(color::ALERT))
.push(
icon::hourglass_done_icon()
.small()
.color(color::ALERT),
)
.align_items(Alignment::Center),
))
} else {
Some(container(
row()
.spacing(5)
.push(
text(&format!(
" {}",
b as u32 + timelock - blockheight
))
.small(),
)
.push(icon::hourglass_icon().small())
.align_items(Alignment::Center),
))
}
} else {
None
}
})
.spacing(10)
.align_items(Alignment::Center)
.width(Length::Fill),
@ -57,12 +94,79 @@ fn coin_list_view<'a>(i: usize, coin: &Coin) -> Element<'a, Message> {
.width(Length::Shrink),
)
.align_items(Alignment::Center)
.spacing(20),
)
.padding(10)
.on_press(Message::Select(i))
.style(Style::TransparentBorder),
)
.spacing(20)
.into()
},
move || {
column()
.spacing(10)
.push(separation().width(Length::Fill))
.push(
column()
.padding(10)
.spacing(5)
.push_maybe(if coin.spend_info.is_none() {
if let Some(b) = coin.block_height {
if blockheight > b as u32 + timelock {
Some(container(
text("The recovery path is available")
.bold()
.small()
.color(color::ALERT),
))
} else {
Some(container(
text(&format!(
"The recovery path will be available in {} blocks",
b as u32 + timelock - blockheight
))
.bold()
.small(),
))
}
} else {
None
}
} else {
None
})
.push(
column()
.push(
row()
.push(text("Outpoint:").small().bold())
.push(text(&format!("{}", coin.outpoint)).small())
.spacing(5),
)
.push_maybe(coin.block_height.map(|b| {
row()
.push(text("Block height:").small().bold())
.push(text(&format!("{}", b)).small())
.spacing(5)
})),
)
.push_maybe(coin.spend_info.map(|info| {
column()
.push(
row()
.push(text("Spend txid:").small().bold())
.push(text(&format!("{}", info.txid)).small())
.spacing(5),
)
.push(if let Some(height) = info.height {
row()
.push(text("Spend block height:").small().bold())
.push(text(&format!("{}", height)).small())
.spacing(5)
} else {
row().push(text("Not in a block").bold().small())
})
.spacing(5)
})),
)
.into()
},
))
.style(card::SimpleCardStyle)
.into()
}

View File

@ -0,0 +1,95 @@
use iced::pure::{button, column};
use iced_lazy::pure::{self, Component};
use iced_native::text;
use iced_pure::Element;
use std::marker::PhantomData;
use crate::ui::component::button::Style;
pub fn collapse<
'a,
Message: 'a,
T: Into<Message> + Clone + 'a,
Renderer: text::Renderer + 'static,
H: Fn() -> Element<'a, T, Renderer> + 'a,
C: Fn() -> Element<'a, T, Renderer> + 'a,
>(
header: H,
content: C,
) -> impl Into<Element<'a, Message, Renderer>> {
Collapse {
header,
content,
phantom: PhantomData,
}
}
struct Collapse<'a, H, C> {
header: H,
content: C,
phantom: PhantomData<&'a H>,
}
#[derive(Debug, Clone, Copy)]
enum Event<T> {
Internal(T),
Collapse(bool),
}
impl<'a, Message, Renderer, T, H, C> Component<Message, Renderer> for Collapse<'a, H, C>
where
T: Into<Message> + Clone + 'a,
H: Fn() -> Element<'a, T, Renderer>,
C: Fn() -> Element<'a, T, Renderer>,
Renderer: text::Renderer + 'static,
{
type State = bool;
type Event = Event<T>;
fn update(&mut self, state: &mut Self::State, event: Event<T>) -> Option<Message> {
match event {
Event::Internal(e) => Some(e.into()),
Event::Collapse(s) => {
*state = s;
None
}
}
}
fn view(&self, state: &Self::State) -> Element<Self::Event, Renderer> {
if *state {
column()
.push(
button((self.header)().map(Event::Internal))
.style(Style::TransparentBorder)
.padding(10)
.on_press(Event::Collapse(false)),
)
.push((self.content)().map(Event::Internal))
.into()
} else {
column()
.push(
button((self.header)().map(Event::Internal))
.padding(10)
.style(Style::TransparentBorder)
.on_press(Event::Collapse(true)),
)
.into()
}
}
}
impl<'a, Message, Renderer, T, H: 'a, C: 'a> From<Collapse<'a, H, C>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'static + text::Renderer,
T: Into<Message> + Clone + 'a,
H: Fn() -> Element<'a, T, Renderer>,
C: Fn() -> Element<'a, T, Renderer>,
{
fn from(c: Collapse<'a, H, C>) -> Self {
pure::component(c)
}
}

View File

@ -1,6 +1,7 @@
pub mod badge;
pub mod button;
pub mod card;
pub mod collapse;
pub mod form;
pub mod text;

View File

@ -14,6 +14,14 @@ fn icon(unicode: char) -> Text {
.size(20)
}
pub fn hourglass_icon() -> Text {
icon('\u{F41F}')
}
pub fn hourglass_done_icon() -> Text {
icon('\u{F41E}')
}
pub fn vault_icon() -> Text {
icon('\u{F65A}')
}