Add game_of_life
example
RIP John Conway
This commit is contained in:
parent
afa0bca4fd
commit
70f86f998b
@ -43,6 +43,7 @@ members = [
|
|||||||
"examples/custom_widget",
|
"examples/custom_widget",
|
||||||
"examples/download_progress",
|
"examples/download_progress",
|
||||||
"examples/events",
|
"examples/events",
|
||||||
|
"examples/game_of_life",
|
||||||
"examples/geometry",
|
"examples/geometry",
|
||||||
"examples/integration",
|
"examples/integration",
|
||||||
"examples/pane_grid",
|
"examples/pane_grid",
|
||||||
|
12
examples/game_of_life/Cargo.toml
Normal file
12
examples/game_of_life/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "game_of_life"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { path = "../..", features = ["async-std", "canvas", "debug"] }
|
||||||
|
iced_native = { path = "../../native" }
|
||||||
|
async-std = { version = "1.0", features = ["unstable"] }
|
||||||
|
itertools = "0.9"
|
18
examples/game_of_life/README.md
Normal file
18
examples/game_of_life/README.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
## Bézier tool
|
||||||
|
|
||||||
|
A Paint-like tool for drawing Bézier curves using the `Canvas` widget.
|
||||||
|
|
||||||
|
The __[`main`]__ file contains all the code of the example.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://gfycat.com/soulfulinfiniteantbear">
|
||||||
|
<img src="https://thumbs.gfycat.com/SoulfulInfiniteAntbear-small.gif">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
You can run it with `cargo run`:
|
||||||
|
```
|
||||||
|
cargo run --package bezier_tool
|
||||||
|
```
|
||||||
|
|
||||||
|
[`main`]: src/main.rs
|
408
examples/game_of_life/src/main.rs
Normal file
408
examples/game_of_life/src/main.rs
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
//! This example showcases an interactive version of the Game of Life, invented
|
||||||
|
//! by John Conway. It leverages a `Canvas` together with other widgets.
|
||||||
|
mod style;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
use grid::Grid;
|
||||||
|
use iced::{
|
||||||
|
button::{self, Button},
|
||||||
|
executor,
|
||||||
|
slider::{self, Slider},
|
||||||
|
Align, Application, Column, Command, Container, Element, Length, Row,
|
||||||
|
Settings, Subscription, Text,
|
||||||
|
};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
GameOfLife::run(Settings {
|
||||||
|
antialiasing: true,
|
||||||
|
..Settings::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct GameOfLife {
|
||||||
|
grid: Grid,
|
||||||
|
is_playing: bool,
|
||||||
|
speed: u64,
|
||||||
|
next_speed: Option<u64>,
|
||||||
|
toggle_button: button::State,
|
||||||
|
next_button: button::State,
|
||||||
|
clear_button: button::State,
|
||||||
|
speed_slider: slider::State,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
Grid(grid::Message),
|
||||||
|
Tick(Instant),
|
||||||
|
Toggle,
|
||||||
|
Next,
|
||||||
|
Clear,
|
||||||
|
SpeedChanged(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application for GameOfLife {
|
||||||
|
type Message = Message;
|
||||||
|
type Executor = executor::Default;
|
||||||
|
type Flags = ();
|
||||||
|
|
||||||
|
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
speed: 1,
|
||||||
|
..Self::default()
|
||||||
|
},
|
||||||
|
Command::none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> String {
|
||||||
|
String::from("Game of Life - Iced")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
match message {
|
||||||
|
Message::Grid(message) => {
|
||||||
|
self.grid.update(message);
|
||||||
|
}
|
||||||
|
Message::Tick(_) | Message::Next => {
|
||||||
|
self.grid.tick();
|
||||||
|
|
||||||
|
if let Some(speed) = self.next_speed.take() {
|
||||||
|
self.speed = speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Toggle => {
|
||||||
|
self.is_playing = !self.is_playing;
|
||||||
|
}
|
||||||
|
Message::Clear => {
|
||||||
|
self.grid = Grid::default();
|
||||||
|
}
|
||||||
|
Message::SpeedChanged(speed) => {
|
||||||
|
if self.is_playing {
|
||||||
|
self.next_speed = Some(speed.round() as u64);
|
||||||
|
} else {
|
||||||
|
self.speed = speed.round() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
if self.is_playing {
|
||||||
|
time::every(Duration::from_millis(1000 / self.speed))
|
||||||
|
.map(Message::Tick)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&mut self) -> Element<Message> {
|
||||||
|
let playback_controls = Row::new()
|
||||||
|
.spacing(10)
|
||||||
|
.push(
|
||||||
|
Button::new(
|
||||||
|
&mut self.toggle_button,
|
||||||
|
Text::new(if self.is_playing { "Pause" } else { "Play" }),
|
||||||
|
)
|
||||||
|
.on_press(Message::Toggle)
|
||||||
|
.style(style::Button),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
Button::new(&mut self.next_button, Text::new("Next"))
|
||||||
|
.on_press(Message::Next)
|
||||||
|
.style(style::Button),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
Button::new(&mut self.clear_button, Text::new("Clear"))
|
||||||
|
.on_press(Message::Clear)
|
||||||
|
.style(style::Button),
|
||||||
|
);
|
||||||
|
|
||||||
|
let selected_speed = self.next_speed.unwrap_or(self.speed);
|
||||||
|
let speed_controls = Row::new()
|
||||||
|
.spacing(10)
|
||||||
|
.push(
|
||||||
|
Slider::new(
|
||||||
|
&mut self.speed_slider,
|
||||||
|
1.0..=20.0,
|
||||||
|
selected_speed as f32,
|
||||||
|
Message::SpeedChanged,
|
||||||
|
)
|
||||||
|
.width(Length::Units(200))
|
||||||
|
.style(style::Slider),
|
||||||
|
)
|
||||||
|
.push(Text::new(format!("x{}", selected_speed)).size(16))
|
||||||
|
.align_items(Align::Center);
|
||||||
|
|
||||||
|
let controls = Row::new()
|
||||||
|
.spacing(20)
|
||||||
|
.push(playback_controls)
|
||||||
|
.push(speed_controls);
|
||||||
|
|
||||||
|
let content = Column::new()
|
||||||
|
.spacing(10)
|
||||||
|
.padding(10)
|
||||||
|
.align_items(Align::Center)
|
||||||
|
.push(self.grid.view().map(Message::Grid))
|
||||||
|
.push(controls);
|
||||||
|
|
||||||
|
Container::new(content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(style::Container)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod grid {
|
||||||
|
use iced::{
|
||||||
|
canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path},
|
||||||
|
mouse, ButtonState, Color, Element, Length, MouseCursor, Point,
|
||||||
|
Rectangle, Size, Vector,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Grid {
|
||||||
|
cells: [[Cell; SIZE]; SIZE],
|
||||||
|
mouse_pressed: bool,
|
||||||
|
cache: canvas::Cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
let mut populated_neighbors: [[usize; SIZE]; SIZE] =
|
||||||
|
[[0; SIZE]; SIZE];
|
||||||
|
|
||||||
|
for (i, row) in self.cells.iter().enumerate() {
|
||||||
|
for (j, _) in row.iter().enumerate() {
|
||||||
|
populated_neighbors[i][j] = self.populated_neighbors(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, row) in populated_neighbors.iter().enumerate() {
|
||||||
|
for (j, amount) in row.iter().enumerate() {
|
||||||
|
let is_populated = self.cells[i][j] == Cell::Populated;
|
||||||
|
|
||||||
|
self.cells[i][j] = match amount {
|
||||||
|
2 if is_populated => Cell::Populated,
|
||||||
|
3 => Cell::Populated,
|
||||||
|
_ => Cell::Unpopulated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::Populate { i, j } => {
|
||||||
|
self.cells[i][j] = Cell::Populated;
|
||||||
|
self.cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view<'a>(&'a mut self) -> Element<'a, Message> {
|
||||||
|
Canvas::new(self)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populated_neighbors(&self, row: usize, column: usize) -> usize {
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
let rows = row.saturating_sub(1)..=row + 1;
|
||||||
|
let columns = column.saturating_sub(1)..=column + 1;
|
||||||
|
|
||||||
|
let is_inside_bounds = |i: usize, j: usize| i < SIZE && j < SIZE;
|
||||||
|
let is_neighbor = |i: usize, j: usize| i != row || j != column;
|
||||||
|
|
||||||
|
let is_populated =
|
||||||
|
|i: usize, j: usize| self.cells[i][j] == Cell::Populated;
|
||||||
|
|
||||||
|
rows.cartesian_product(columns)
|
||||||
|
.filter(|&(i, j)| {
|
||||||
|
is_inside_bounds(i, j)
|
||||||
|
&& is_neighbor(i, j)
|
||||||
|
&& is_populated(i, j)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn region(&self, size: Size) -> Rectangle {
|
||||||
|
let side = size.width.min(size.height);
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: (size.width - side) / 2.0,
|
||||||
|
y: (size.height - side) / 2.0,
|
||||||
|
width: side,
|
||||||
|
height: side,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_at(
|
||||||
|
&self,
|
||||||
|
region: Rectangle,
|
||||||
|
position: Point,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
|
if region.contains(position) {
|
||||||
|
let cell_size = region.width / SIZE as f32;
|
||||||
|
|
||||||
|
let i = ((position.y - region.y) / cell_size).ceil() as usize;
|
||||||
|
let j = ((position.x - region.x) / cell_size).ceil() as usize;
|
||||||
|
|
||||||
|
Some((i.saturating_sub(1), j.saturating_sub(1)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Cell {
|
||||||
|
Unpopulated,
|
||||||
|
Populated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cell {
|
||||||
|
fn default() -> Cell {
|
||||||
|
Cell::Unpopulated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Message {
|
||||||
|
Populate { i: usize, j: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> canvas::Program<Message> for Grid {
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: Event,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: Cursor,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if let Event::Mouse(mouse::Event::Input {
|
||||||
|
button: mouse::Button::Left,
|
||||||
|
state,
|
||||||
|
}) = event
|
||||||
|
{
|
||||||
|
self.mouse_pressed = state == ButtonState::Pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor_position = cursor.internal_position(&bounds)?;
|
||||||
|
|
||||||
|
let region = self.region(bounds.size());
|
||||||
|
let (i, j) = self.cell_at(region, cursor_position)?;
|
||||||
|
|
||||||
|
let populate = if self.cells[i][j] != Cell::Populated {
|
||||||
|
Some(Message::Populate { i, j })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::Input {
|
||||||
|
button: mouse::Button::Left,
|
||||||
|
..
|
||||||
|
}) if self.mouse_pressed => populate,
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||||
|
if self.mouse_pressed =>
|
||||||
|
{
|
||||||
|
populate
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
|
||||||
|
let region = self.region(bounds.size());
|
||||||
|
let cell_size = Size::new(1.0, 1.0);
|
||||||
|
|
||||||
|
let life = self.cache.draw(bounds.size(), |frame| {
|
||||||
|
let background =
|
||||||
|
Path::rectangle(region.position(), region.size());
|
||||||
|
frame.fill(
|
||||||
|
&background,
|
||||||
|
Color::from_rgb(
|
||||||
|
0x40 as f32 / 255.0,
|
||||||
|
0x44 as f32 / 255.0,
|
||||||
|
0x4B as f32 / 255.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.with_save(|frame| {
|
||||||
|
frame.translate(Vector::new(region.x, region.y));
|
||||||
|
frame.scale(region.width / SIZE as f32);
|
||||||
|
|
||||||
|
let cells = Path::new(|p| {
|
||||||
|
for (i, row) in self.cells.iter().enumerate() {
|
||||||
|
for (j, cell) in row.iter().enumerate() {
|
||||||
|
if *cell == Cell::Populated {
|
||||||
|
p.rectangle(
|
||||||
|
Point::new(j as f32, i as f32),
|
||||||
|
cell_size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frame.fill(&cells, Color::WHITE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let hovered_cell = {
|
||||||
|
let mut frame = Frame::new(bounds.size());
|
||||||
|
|
||||||
|
frame.translate(Vector::new(region.x, region.y));
|
||||||
|
frame.scale(region.width / SIZE as f32);
|
||||||
|
|
||||||
|
if let Some(cursor_position) = cursor.internal_position(&bounds)
|
||||||
|
{
|
||||||
|
if let Some((i, j)) = self.cell_at(region, cursor_position)
|
||||||
|
{
|
||||||
|
let interaction = Path::rectangle(
|
||||||
|
Point::new(j as f32, i as f32),
|
||||||
|
cell_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.fill(
|
||||||
|
&interaction,
|
||||||
|
Color {
|
||||||
|
a: 0.5,
|
||||||
|
..Color::BLACK
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.into_geometry()
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![life, hovered_cell]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_cursor(
|
||||||
|
&self,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: Cursor,
|
||||||
|
) -> MouseCursor {
|
||||||
|
let region = self.region(bounds.size());
|
||||||
|
|
||||||
|
match cursor.internal_position(&bounds) {
|
||||||
|
Some(position) if region.contains(position) => {
|
||||||
|
MouseCursor::Crosshair
|
||||||
|
}
|
||||||
|
_ => MouseCursor::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
examples/game_of_life/src/style.rs
Normal file
96
examples/game_of_life/src/style.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use iced::{button, container, slider, Background, Color};
|
||||||
|
|
||||||
|
const ACTIVE: Color = Color::from_rgb(
|
||||||
|
0x72 as f32 / 255.0,
|
||||||
|
0x89 as f32 / 255.0,
|
||||||
|
0xDA as f32 / 255.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const HOVERED: Color = Color::from_rgb(
|
||||||
|
0x67 as f32 / 255.0,
|
||||||
|
0x7B as f32 / 255.0,
|
||||||
|
0xC4 as f32 / 255.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub struct Container;
|
||||||
|
|
||||||
|
impl container::StyleSheet for Container {
|
||||||
|
fn style(&self) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(
|
||||||
|
0x36, 0x39, 0x3F,
|
||||||
|
))),
|
||||||
|
text_color: Some(Color::WHITE),
|
||||||
|
..container::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Button;
|
||||||
|
|
||||||
|
impl button::StyleSheet for Button {
|
||||||
|
fn active(&self) -> button::Style {
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(ACTIVE)),
|
||||||
|
border_radius: 3,
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
..button::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hovered(&self) -> button::Style {
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(HOVERED)),
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
..self.active()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pressed(&self) -> button::Style {
|
||||||
|
button::Style {
|
||||||
|
border_width: 1,
|
||||||
|
border_color: Color::WHITE,
|
||||||
|
..self.hovered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Slider;
|
||||||
|
|
||||||
|
impl slider::StyleSheet for Slider {
|
||||||
|
fn active(&self) -> slider::Style {
|
||||||
|
slider::Style {
|
||||||
|
rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
|
||||||
|
handle: slider::Handle {
|
||||||
|
shape: slider::HandleShape::Circle { radius: 9 },
|
||||||
|
color: ACTIVE,
|
||||||
|
border_width: 0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hovered(&self) -> slider::Style {
|
||||||
|
let active = self.active();
|
||||||
|
|
||||||
|
slider::Style {
|
||||||
|
handle: slider::Handle {
|
||||||
|
color: HOVERED,
|
||||||
|
..active.handle
|
||||||
|
},
|
||||||
|
..active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dragging(&self) -> slider::Style {
|
||||||
|
let active = self.active();
|
||||||
|
|
||||||
|
slider::Style {
|
||||||
|
handle: slider::Handle {
|
||||||
|
color: Color::from_rgb(0.85, 0.85, 0.85),
|
||||||
|
..active.handle
|
||||||
|
},
|
||||||
|
..active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
examples/game_of_life/src/time.rs
Normal file
34
examples/game_of_life/src/time.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use iced::futures;
|
||||||
|
|
||||||
|
pub fn every(
|
||||||
|
duration: std::time::Duration,
|
||||||
|
) -> iced::Subscription<std::time::Instant> {
|
||||||
|
iced::Subscription::from_recipe(Every(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Every(std::time::Duration);
|
||||||
|
|
||||||
|
impl<H, I> iced_native::subscription::Recipe<H, I> 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::<Self>().hash(state);
|
||||||
|
self.0.hash(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream(
|
||||||
|
self: Box<Self>,
|
||||||
|
_input: futures::stream::BoxStream<'static, I>,
|
||||||
|
) -> futures::stream::BoxStream<'static, Self::Output> {
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
|
||||||
|
async_std::stream::interval(self.0)
|
||||||
|
.map(|_| std::time::Instant::now())
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user