Skip to content

Commit

Permalink
✨ Add Cupertino spinner
Browse files Browse the repository at this point in the history
  • Loading branch information
Brett Byler committed Mar 13, 2023
1 parent 0323f2e commit 0edca3d
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ target/
pkg/
.idea
*.lock
.DS_Store

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ badge = []
card = []
date_picker = ["chrono", "lazy_static", "icon_text"]
color_picker = ["icon_text", "iced_graphics/canvas"]
cupertino = ["iced_graphics/canvas"]
floating_element = []
grid = []
glow = [] # TODO
Expand Down Expand Up @@ -55,6 +56,7 @@ default = [

[dependencies]
num-traits = { version = "0.2.15", optional = true }
time = { version = "0.3.5", features = ["local-offset"] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
chrono = { version = "0.4.23", optional = true }
Expand Down Expand Up @@ -83,6 +85,7 @@ members = [
"examples/badge",
"examples/card",
"examples/color_picker",
"examples/cupertino/cupertino_spinner",
"examples/date_picker",
"examples/floating_element",
"examples/floating_element_anchors",
Expand All @@ -108,3 +111,4 @@ version = "0.8.0"
[workspace.dependencies.iced_aw]
path = "./"
default-features = false

12 changes: 12 additions & 0 deletions examples/cupertino/cupertino_spinner/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "cupertino-spinner"
version = "0.1.0"
authors = ["Brett Byler <[email protected]>"]
edition = "2021"
publish = false

[dependencies]
iced = { version = "0.8.0", features = ["canvas", "debug", "tokio"] }
iced_aw = { path = "../../../", features = ["cupertino"] }
tokio = { version = "1.26.0", features = ["time"] }

7 changes: 7 additions & 0 deletions examples/cupertino/cupertino_spinner/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
SHELL := /bin/bash

run:
cargo run --package cupertino-spinner

.ONESHELL:

19 changes: 19 additions & 0 deletions examples/cupertino/cupertino_spinner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Cupertino Spinner Example
=========================

An application that uses the `CupertinoSpinner` widget to draw a
spinner.

The __[`main`]__ file contains all the code of the example.

You can run it with `cargo run`:

```bash
cargo run --package cupertino-spinner

# Or
make run
```

[`main`]: src/main.rs

91 changes: 91 additions & 0 deletions examples/cupertino/cupertino_spinner/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use iced::{Application, Command, Element, executor, Length, Settings, Theme};
use iced::alignment;
use iced::widget::{column, container, text};
use iced_aw::native::cupertino::cupertino_spinner::CupertinoSpinner;

pub fn main() -> iced::Result {
Spinner::run(Settings { antialiasing: true, ..Settings::default() })
}

#[derive(Debug, Clone)]
struct State {
hello: String,
}

enum Spinner {
Loading,
Loaded(State),
}

#[derive(Debug, Clone)]
enum Message {
Loaded(Result<State, ()>),
}

impl State {
async fn load() -> Result<State, ()> {
println!("Doing stuff...");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
return Ok(Self { hello: "Loaded!".to_string() });
}
}

impl Application for Spinner {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();

fn new(_flags: ()) -> (Self, Command<Message>) {
(Spinner::Loading, Command::perform(State::load(), Message::Loaded))
}

fn title(&self) -> String { String::from("CupertinoSpinner - Iced") }

fn update(&mut self, message: Message) -> Command<Message> {
match self {
Spinner::Loading => {
match message {
Message::Loaded(Ok(state)) => {
*self = Spinner::Loaded(State {
hello: state.hello,
});
},

_ => ()
}
},

_ => ()

}

Command::none()
}

fn view(&self) -> Element<Message> {
match self {
Spinner::Loading => {
container(
CupertinoSpinner::new().width(Length::Fill).height(Length::Fill)
).into()
},

Spinner::Loaded(state) => {
container(column![text(&state.hello)
.width(Length::Fill)
.size(25)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center)
])
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
},
}
}

fn theme(&self) -> Self::Theme { Theme::Light }
}

Binary file added images/showcase/cupertino/cupertino-spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
219 changes: 219 additions & 0 deletions src/native/cupertino/cupertino_spinner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
use iced_graphics::{Backend, Renderer};
use iced_native::event::Status;
use iced_native::layout::{Limits, Node};
use iced_native::renderer::Style;
use iced_native::widget::{tree::{State, Tag}, Tree};

use iced_native::{
Clipboard, Color, Element, Event, Layout, Length, Point, Rectangle, Size, Shell,
Vector, Widget, window,
};

use iced_graphics::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke,};

use std::f32::consts::PI;

const HAND_COUNT: usize = 8;
const ALPHAS: [u16; 8] = [47, 47, 47, 47, 72, 97, 122, 147];

/**
* `CupertinoSpinner`
*
* See
*
* 1. [Flutter Activity Indicator](https://github.com/flutter/flutter/blob/0b451b6dfd6de73ff89d89081c33d0f971db1872/packages/flutter/lib/src/cupertino/activity_indicator.dart)
* 2. [Flutter Cupertino Widgets](https://docs.flutter.dev/development/ui/widgets/cupertino)
*
* for reference. The Flutter source is used for constants. The implementation for this widget
* pulls together ideas from:
*
* 1. the mainline Clock example
* 2. the existing `iced_aw` Spinner
* 3. the Flutter Activity Indicator above
* 4. the QR Code widget
*
* See the examples folder (`examples/cupertino/cupertino_spinner`) for a full example of usage.
*
*/
#[allow(missing_debug_implementations)]
#[derive(Debug)]
pub struct CupertinoSpinner {
width: Length,
height: Length,
radius: f32,
}

struct SpinnerState {
now: time::OffsetDateTime,
spinner: Cache,
}

impl Default for CupertinoSpinner {
fn default() -> Self {
Self {
width: Length::Fixed(20.0),
height: Length::Fixed(20.0),
radius: 20.0,
}
}
}

impl CupertinoSpinner {
/// Creates a new [`CupertinoSpinner`] widget.
pub fn new() -> Self {
Self::default()
}

/// Sets the width of the [`CupertinoSpinner`](CupertinoSpinner).
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}

/// Sets the height of the [`CupertinoSpinner`](CupertinoSpinner).
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}

/// Sets the radius of the [`CupertinoSpinner`](CupertinoSpinner).
/// NOTE: While you _can_ tweak the radius, the scale may be all out of whack if not using a
/// number close to the default of `20.0`.
pub fn radius(mut self, radius: f32) -> Self {
self.radius = radius;
self
}
}

impl<'a, Message, B, T> Widget<Message, Renderer<B, T>>
for CupertinoSpinner
where B: Backend {
fn width(&self) -> Length { self.width }
fn height(&self) -> Length { self.height }

fn layout(&self, _renderer: &Renderer<B, T>, limits: &Limits) -> Node {
Node::new(limits
.width(self.width)
.height(self.height)
.resolve(Size::new(f32::INFINITY, f32::INFINITY)),
)
}

fn draw(
&self,
state: &Tree,
renderer: &mut Renderer<B, T>,
_theme: &T,
_style: &Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let state: &SpinnerState = state.state.downcast_ref::<SpinnerState>();

let spinner: Geometry = state.spinner.draw(layout.bounds().size(), |frame| {
let center = frame.center();
let radius = self.radius;
let width = radius / 5.0;

let mut hands: Vec<(Path, _)> = vec!();

for i in 0..HAND_COUNT {
hands.push((
Path::line(Point::new(0.0, radius / 3.0), Point::new(0.0, radius / 1.5)),
move || -> Stroke {
// The `60.0` is to shift the original black to dark grey //
gen_stroke(
width,
Color::from_rgba(0.0, 0.0, 0.0, ALPHAS[i] as f32 / (60.0 + 147.0))
)
},
))
}

frame.translate(Vector::new(center.x, center.y));

// Populate the spinner with 8 hands and make them spin! //
// The `(HAND_COUNT - i - 1)` block is to make the spinning
// clockwise. For counterclockwise, leave it at `i`.
frame.with_save(|frame| {
let new_index: usize = (state.now.millisecond() / 125 % 8) as usize;

for i in 0..HAND_COUNT {
frame.rotate(hand_rotation(45, 360));

frame.stroke(
&hands[i].0,
hands[((HAND_COUNT - i - 1) + new_index) % 8].1()
);
}
});
//
});

renderer.draw_primitive(spinner.into_primitive());
}

fn tag(&self) -> Tag { Tag::of::<SpinnerState>() }

fn state(&self) -> State {
State::new(SpinnerState {
now: time::OffsetDateTime::now_local().unwrap_or_else(
|_| time::OffsetDateTime::now_utc()
),
spinner: Default::default(),
})
}

fn on_event(
&mut self,
state: &mut Tree,
event: Event,
_layout: Layout<'_>,
_cursor_position: Point,
_renderer: &Renderer<B, T>,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> Status {
let state: &mut SpinnerState = state.state.downcast_mut::<SpinnerState>();

if let Event::Window(window::Event::RedrawRequested(_now)) = &event {
// if is_visible(&bounds) {
state.now = time::OffsetDateTime::now_local().unwrap_or_else(
|_| time::OffsetDateTime::now_utc()
);

state.spinner.clear();
shell.request_redraw(window::RedrawRequest::NextFrame);
return Status::Captured;
}

return Status::Ignored;
}
}

impl<'a, Message, B, T> From<CupertinoSpinner>
for Element<'a, Message, Renderer<B, T>>
where B: Backend {
fn from(spinner: CupertinoSpinner) -> Self {
Self::new(spinner)
}
}

fn gen_stroke(width: f32, color: Color) -> Stroke<'static> {
return Stroke {
width,
style: stroke::Style::Solid(color),
line_cap: LineCap::Round,
..Stroke::default()
};
}

const K: f32 = PI * 2.0;

fn hand_rotation(n: u16, total: u16) -> f32 {
let turns = n as f32 / total as f32;

K * turns
}

5 changes: 5 additions & 0 deletions src/native/cupertino/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// Use a Cupertino-style spinner element.
///
/// *This API requires the following crate features to be activated: `cupertino`*
pub mod cupertino_spinner;

Loading

0 comments on commit 0edca3d

Please sign in to comment.