use iced::{ button, Align, Application, Button, Column, Command, Container, Element, HorizontalAlignment, Length, Row, Settings, Subscription, Text, }; use std::time::{Duration, Instant}; pub fn main() { Stopwatch::run(Settings::default()) } struct Stopwatch { duration: Duration, state: State, toggle: button::State, reset: button::State, } enum State { Idle, Ticking { last_tick: Instant }, } #[derive(Debug, Clone)] enum Message { Toggle, Reset, Tick(Instant), } impl Application for Stopwatch { type Message = Message; fn new() -> (Stopwatch, Command) { ( Stopwatch { duration: Duration::default(), state: State::Idle, toggle: button::State::new(), reset: button::State::new(), }, Command::none(), ) } fn title(&self) -> String { String::from("Stopwatch - Iced") } fn update(&mut self, message: Message) -> Command { match message { Message::Toggle => match self.state { State::Idle => { self.state = State::Ticking { last_tick: Instant::now(), }; } State::Ticking { .. } => { self.state = State::Idle; } }, Message::Tick(now) => match &mut self.state { State::Ticking { last_tick } => { self.duration += now - *last_tick; *last_tick = now; } _ => {} }, Message::Reset => { self.duration = Duration::default(); } } Command::none() } fn subscription(&self) -> Subscription { match self.state { State::Idle => Subscription::none(), State::Ticking { .. } => { time::every(Duration::from_millis(10)).map(Message::Tick) } } } fn view(&mut self) -> Element { const MINUTE: u64 = 60; const HOUR: u64 = 60 * MINUTE; let seconds = self.duration.as_secs(); let duration = Text::new(format!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, (seconds % HOUR) / MINUTE, seconds % MINUTE, self.duration.subsec_millis() / 10, )) .width(Length::Shrink) .size(40); let button = |state, label, style| { Button::new( state, Text::new(label) .horizontal_alignment(HorizontalAlignment::Center), ) .min_width(80) .padding(10) .style(style) }; let toggle_button = { let (label, color) = match self.state { State::Idle => ("Start", style::Button::Primary), State::Ticking { .. } => ("Stop", style::Button::Destructive), }; button(&mut self.toggle, label, color).on_press(Message::Toggle) }; let reset_button = button(&mut self.reset, "Reset", style::Button::Secondary) .on_press(Message::Reset); let controls = Row::new() .width(Length::Shrink) .spacing(20) .push(toggle_button) .push(reset_button); let content = Column::new() .width(Length::Shrink) .align_items(Align::Center) .spacing(20) .push(duration) .push(controls); Container::new(content) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() .into() } } mod time { pub fn every( duration: std::time::Duration, ) -> iced::Subscription { iced::Subscription::from_recipe(Every(duration)) } struct Every(std::time::Duration); impl iced_native::subscription::Recipe for Every where H: std::hash::Hasher, { type Output = std::time::Instant; fn hash(&self, state: &mut H) { use std::hash::Hash; std::any::TypeId::of::().hash(state); self.0.hash(state); } fn stream( self: Box, _input: I, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::stream::StreamExt; async_std::stream::interval(self.0) .map(|_| std::time::Instant::now()) .boxed() } } } mod style { use iced::{button, Background, Color, Vector}; pub enum Button { Primary, Secondary, Destructive, } impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { background: Some(Background::Color(match self { Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), })), border_radius: 12, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::WHITE, ..button::Style::default() } } } }