diff --git a/Cargo.toml b/Cargo.toml index f777b971..a5f1cf0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,11 @@ members = [ "examples/badge", "examples/card", "examples/color_picker", + "examples/cupertino/cupertino_alert", + "examples/cupertino/cupertino_button", + # "examples/cupertino/cupertino_slider", "examples/cupertino/cupertino_spinner", + "examples/cupertino/cupertino_switch", "examples/date_picker", "examples/floating_element", "examples/floating_element_anchors", diff --git a/examples/cupertino/cupertino_alert/Cargo.toml b/examples/cupertino/cupertino_alert/Cargo.toml new file mode 100644 index 00000000..ef99d2ec --- /dev/null +++ b/examples/cupertino/cupertino_alert/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cupertino-alert" +version = "0.1.0" +authors = ["Brett Byler "] +edition = "2021" +publish = false + +[dependencies] +iced = { version = "0.8.0", features = [] } +iced_aw = { path = "../../../", features = ["cupertino"] } + diff --git a/examples/cupertino/cupertino_alert/Makefile b/examples/cupertino/cupertino_alert/Makefile new file mode 100644 index 00000000..2573848c --- /dev/null +++ b/examples/cupertino/cupertino_alert/Makefile @@ -0,0 +1,7 @@ +SHELL := /bin/bash + +run: + cargo run --package cupertino-alert + +.ONESHELL: + diff --git a/examples/cupertino/cupertino_alert/README.md b/examples/cupertino/cupertino_alert/README.md new file mode 100644 index 00000000..34e4a147 --- /dev/null +++ b/examples/cupertino/cupertino_alert/README.md @@ -0,0 +1,19 @@ +Cupertino Alert Example +========================= + +An application that uses the `CupertinoAlert` widget to draw a +Cupertino-style alert dialog box. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: + +```bash +cargo run --package cupertino-alert + +# Or +make run +``` + +[`main`]: src/main.rs + diff --git a/examples/cupertino/cupertino_alert/src/main.rs b/examples/cupertino/cupertino_alert/src/main.rs new file mode 100644 index 00000000..d9b0d2b7 --- /dev/null +++ b/examples/cupertino/cupertino_alert/src/main.rs @@ -0,0 +1,142 @@ +use iced::{alignment, Application, Command, Element, executor, Length, Settings, Theme}; +use iced::widget::{column, container, Text}; +use iced_aw::native::cupertino::cupertino_alert::{CupertinoAlert, CupertinoDialogAction}; +use iced_aw::native::cupertino::cupertino_button::CupertinoButton; +use iced_aw::native::cupertino::cupertino_colours::system_red; + +pub fn main() -> iced::Result { + Alert::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Debug)] +enum Alert { + BackdropClicked, + CancelEvent, + ConfirmEvent, + DialogEscape, + Loading, + ShowModal, +} + +#[derive(Debug, Clone)] +enum Message { + BackdropClicked, + CancelEvent, + ConfirmEvent, + DialogEscape, + ShowModal, +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +mod constants { + pub static PRIMARY_TEXT: &'static str = "Allow \"Maps\" to access your location while using the app?"; + pub static SECONDARY_TEXT: &'static str = "Your current location will be displayed on the map and used for directions, nearby search results, and estimated travel times. "; + pub static ALLOW: &'static str = "Allow"; + pub static DONT_ALLOW: &'static str = "Don't Allow"; +} + +use constants::*; + +// `cargo fmt` becomes unreadable for this example, so switching off // +#[cfg_attr(rustfmt, rustfmt_skip)] +impl Application for Alert { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + (Alert::Loading, Command::none()) + } + + fn title(&self) -> String { + String::from("CupertinoAlert - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::BackdropClicked => *self = Alert::BackdropClicked, + Message::CancelEvent => *self = Alert::CancelEvent, + Message::ConfirmEvent => *self = Alert::ConfirmEvent, + Message::DialogEscape => *self = Alert::DialogEscape, + Message::ShowModal => *self = Alert::ShowModal, + } + + Command::none() + } + + fn view(&self) -> Element { + let modal_hidden: bool = match self { + Alert::ShowModal => false, + _ => true, + }; + + match self { + Alert::CancelEvent => println!("Received click for the cancel button"), + Alert::ConfirmEvent => println!("Received click for the confirm button"), + _ => (), + }; + + let confirm_button: CupertinoButton = CupertinoButton::new() + .on_pressed(Some(Message::ConfirmEvent)) + .body(Text::new(DONT_ALLOW) + .size(24) + .width(Length::Fixed(100.0)) + .height(Length::Fixed(50.0)) + ); + + let cancel_button: CupertinoButton = CupertinoButton::new() + .on_pressed(Some(Message::CancelEvent)) + .colour(Some(system_red(1.0))) + .body(Text::new(ALLOW) + .size(24) + .width(Length::Fixed(100.0)) + .height(Length::Fixed(50.0)) + ); + + let alert = CupertinoAlert::new() + .is_hidden(modal_hidden) + .title(PRIMARY_TEXT.to_string()) + .content(SECONDARY_TEXT.to_string()) + .actions(vec![ + CupertinoDialogAction::new() + .is_enabled(true) + .child(confirm_button.into()) + .on_pressed(Some(Message::ConfirmEvent)), + + CupertinoDialogAction::new() + .is_enabled(true) + .child(cancel_button.into()) + .on_pressed(Some(Message::CancelEvent)), + + ]) + .backdrop(Some(Message::BackdropClicked)) + .on_escape(Some(Message::DialogEscape)); + + container(column![ + CupertinoButton::new() + .body(Text::new("Click to show the CupertinoAlertDialog") + .size(24.0) + .width(Length::Fixed(200.0)) + .height(Length::Fixed(75.0)) + .horizontal_alignment(alignment::Horizontal::Center) + ) + .on_pressed(Some(Message::ShowModal)), + + alert, + ].align_items(alignment::Horizontal::Center.into())) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn theme(&self) -> Self::Theme { + Theme::Light + } +} + diff --git a/examples/cupertino/cupertino_button/Cargo.toml b/examples/cupertino/cupertino_button/Cargo.toml new file mode 100644 index 00000000..07ea0d4a --- /dev/null +++ b/examples/cupertino/cupertino_button/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cupertino-button" +version = "0.1.0" +authors = ["Brett Byler "] +edition = "2021" +publish = false + +[dependencies] +iced = { version = "0.8.0", features = [] } +iced_aw = { path = "../../../", features = ["cupertino"] } + diff --git a/examples/cupertino/cupertino_button/Makefile b/examples/cupertino/cupertino_button/Makefile new file mode 100644 index 00000000..b355ad83 --- /dev/null +++ b/examples/cupertino/cupertino_button/Makefile @@ -0,0 +1,7 @@ +SHELL := /bin/bash + +run: + cargo run --package cupertino-button + +.ONESHELL: + diff --git a/examples/cupertino/cupertino_button/README.md b/examples/cupertino/cupertino_button/README.md new file mode 100644 index 00000000..5d0c5f9a --- /dev/null +++ b/examples/cupertino/cupertino_button/README.md @@ -0,0 +1,19 @@ +Cupertino Button Example +========================= + +An application that uses the `CupertinoAlert` widget to draw a +switch. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: + +```bash +cargo run --package cupertino-button + +# Or +make run +``` + +[`main`]: src/main.rs + diff --git a/examples/cupertino/cupertino_button/src/main.rs b/examples/cupertino/cupertino_button/src/main.rs new file mode 100644 index 00000000..b62dfb18 --- /dev/null +++ b/examples/cupertino/cupertino_button/src/main.rs @@ -0,0 +1,119 @@ +use iced::{ + alignment, Application, Command, Element, executor, Length, Renderer, Settings, Theme, + widget::{column, container, text, Text}, +}; + +use iced_aw::native::cupertino::cupertino_button::CupertinoButton; + +pub fn main() -> iced::Result { + ButtonApp::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +enum ButtonApp { + Loading, + EnabledButtonClicked, + EnabledFilledButtonClicked, +} + +#[derive(Debug, Clone)] +enum Message { + EnabledButtonClicked, + EnabledFilledButtonClicked, +} + +// `cargo fmt` becomes unreadable for this example, so switching off // +#[cfg_attr(rustfmt, rustfmt_skip)] +impl Application for ButtonApp { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + (ButtonApp::Loading, Command::none()) + } + + fn title(&self) -> String { + String::from("CupertinoButton - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::EnabledButtonClicked => { + println!("You clicked the enabled button!"); + *self = ButtonApp::EnabledButtonClicked; + }, + + Message::EnabledFilledButtonClicked => { + println!("You clicked the filled enabled button!"); + *self = ButtonApp::EnabledFilledButtonClicked; + }, + } + + Command::none() + } + + fn view(&self) -> Element { + let disabled: CupertinoButton = CupertinoButton::new() + .on_pressed(None) + .body(Text::new("Disabled") + .size(24) + .horizontal_alignment(alignment::Horizontal::Center) + .width(Length::Fixed(100.0)) + .height(Length::Fixed(50.0)) + ); + + let disabled_filled: CupertinoButton = CupertinoButton::new() + .on_pressed(None) + .is_filled(true) + .body(Text::new("Disabled") + .size(24) + .horizontal_alignment(alignment::Horizontal::Center) + .width(Length::Fixed(200.0)) + .height(Length::Fixed(50.0)) + ); + + let enabled: CupertinoButton = CupertinoButton::new() + .on_pressed(Some(Message::EnabledButtonClicked)) + .body(Text::new("Enabled") + .size(24) + .horizontal_alignment(alignment::Horizontal::Center) + .width(Length::Fixed(100.0)) + .height(Length::Fixed(50.0)) + ); + + let enabled_filled: CupertinoButton = CupertinoButton::new() + .on_pressed(Some(Message::EnabledFilledButtonClicked)) + .is_filled(true) + .body(Text::new("Enabled") + .size(24) + .horizontal_alignment(alignment::Horizontal::Center) + .width(Length::Fixed(200.0)) + .height(Length::Fixed(50.0)) + ); + + container(column![ + text("Cupertino Button Example!") + .width(Length::Fill) + .height(Length::Fixed(100.0)) + .horizontal_alignment(alignment::Horizontal::Center) + .vertical_alignment(alignment::Vertical::Center), + + disabled, + disabled_filled, + enabled, + enabled_filled, + ].width(Length::Fill).align_items(alignment::Horizontal::Center.into())) + .center_y() + .height(Length::Fill) + .into() + } + + fn theme(&self) -> Self::Theme { + Theme::Light + } +} + diff --git a/examples/cupertino/cupertino_switch/Cargo.toml b/examples/cupertino/cupertino_switch/Cargo.toml new file mode 100644 index 00000000..3eb0a94f --- /dev/null +++ b/examples/cupertino/cupertino_switch/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cupertino-switch" +version = "0.1.0" +authors = ["Brett Byler "] +edition = "2021" +publish = false + +[dependencies] +iced = { version = "0.8.0", features = [] } +iced_aw = { path = "../../../", features = ["cupertino"] } + diff --git a/examples/cupertino/cupertino_switch/Makefile b/examples/cupertino/cupertino_switch/Makefile new file mode 100644 index 00000000..507dac78 --- /dev/null +++ b/examples/cupertino/cupertino_switch/Makefile @@ -0,0 +1,7 @@ +SHELL := /bin/bash + +run: + cargo run --package cupertino-switch + +.ONESHELL: + diff --git a/examples/cupertino/cupertino_switch/README.md b/examples/cupertino/cupertino_switch/README.md new file mode 100644 index 00000000..f257e83a --- /dev/null +++ b/examples/cupertino/cupertino_switch/README.md @@ -0,0 +1,19 @@ +Cupertino Switch Example +========================= + +An application that uses the `CupertinoSwitch` widget to draw a +switch. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: + +```bash +cargo run --package cupertino-switch + +# Or +make run +``` + +[`main`]: src/main.rs + diff --git a/examples/cupertino/cupertino_switch/src/main.rs b/examples/cupertino/cupertino_switch/src/main.rs new file mode 100644 index 00000000..83a0336d --- /dev/null +++ b/examples/cupertino/cupertino_switch/src/main.rs @@ -0,0 +1,108 @@ +use iced::{alignment, Application, Color, Command, Element, executor, Length, Settings, Theme}; +use iced::widget::{column, container, row, text}; +use iced_aw::native::cupertino::cupertino_switch::CupertinoSwitch; + +pub fn main() -> iced::Result { + Switch::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +enum Switch { + Loading, + LeftSwitchChanged(bool), + RightSwitchChanged(bool), +} + +#[derive(Debug, Clone)] +enum Message { + LeftSwitchChanged(bool), + RightSwitchChanged(bool), +} + +// `cargo fmt` becomes unreadable for this example, so switching off // +#[cfg_attr(rustfmt, rustfmt_skip)] +impl Application for Switch { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + (Switch::Loading, Command::none()) + } + + fn title(&self) -> String { + String::from("CupertinoSwitch - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::LeftSwitchChanged(value) => *self = Switch::LeftSwitchChanged(value), + Message::RightSwitchChanged(value) => *self = Switch::RightSwitchChanged(value), + } + + Command::none() + } + + fn view(&self) -> Element { + let toggle_1: CupertinoSwitch = CupertinoSwitch::new().on_changed(Some(Box::new( + Message::LeftSwitchChanged + ))); + + let toggle_2: CupertinoSwitch = CupertinoSwitch::new() + .value(false) + .on_changed(Some(Box::new(Message::RightSwitchChanged))); + + let left_text: String = match self { + Switch::LeftSwitchChanged(v) => format!("Left Toggle State: {}", v), + _ => format!("Left Toggle State: {}", toggle_1.value), + }; + + let right_text: String = match self { + Switch::RightSwitchChanged(v) => format!("Right Toggle State: {}", v), + _ => format!("Right Toggle State: {}", toggle_2.value), + }; + + let content = row![ + toggle_1, + + container(column![ + text(left_text) + .width(Length::Fill) + .size(25) + .horizontal_alignment(alignment::Horizontal::Center) + .vertical_alignment(alignment::Vertical::Center), + + text(right_text) + .width(Length::Fill) + .size(25) + .horizontal_alignment(alignment::Horizontal::Center) + .vertical_alignment(alignment::Vertical::Center), + ]).width(Length::Fill).center_y(), + + toggle_2, + ].spacing(100).align_items(alignment::Alignment::Center).width(Length::Shrink); + + // No effect, but here for demonstrative purposes // + let style: fn(&iced::Theme) -> container::Appearance = |_theme| container::Appearance { + background: Some(Color::TRANSPARENT.into()), + ..Default::default() + }; + // + + container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .style(style) + .into() + } + + fn theme(&self) -> Self::Theme { + Theme::Light + } +} + diff --git a/images/showcase/cupertino/buttons.png b/images/showcase/cupertino/buttons.png new file mode 100644 index 00000000..34685078 Binary files /dev/null and b/images/showcase/cupertino/buttons.png differ diff --git a/images/showcase/cupertino/cupertino-alert.gif b/images/showcase/cupertino/cupertino-alert.gif new file mode 100644 index 00000000..939ef013 Binary files /dev/null and b/images/showcase/cupertino/cupertino-alert.gif differ diff --git a/images/showcase/cupertino/cupertino-switch.gif b/images/showcase/cupertino/cupertino-switch.gif new file mode 100644 index 00000000..5931208b Binary files /dev/null and b/images/showcase/cupertino/cupertino-switch.gif differ diff --git a/src/native/cupertino/cupertino_alert.rs b/src/native/cupertino/cupertino_alert.rs new file mode 100644 index 00000000..7e1415b5 --- /dev/null +++ b/src/native/cupertino/cupertino_alert.rs @@ -0,0 +1,556 @@ +use iced_native::{ + alignment, Clipboard, Color, Element, Event, event::Status, Font, keyboard, Layout, + layout::{Limits, Node}, Length, mouse, Point, Rectangle, renderer::{Quad, Style}, + Size, Shell, touch, Widget, widget::{Text, Tree}, +}; + +use std::ops::Range; + +// INTERNAL // +use crate::native::cupertino::cupertino_colours::secondary_system_fill; +use crate::native::cupertino::fonts::SF_UI_ROUNDED; +// + +/** + * `CupertinoDialogAction` + * + * See + * + * + * + * for constants, and + * + * + * + * for the Flutter example / expected usage. + * + */ +#[allow(missing_debug_implementations)] +pub struct CupertinoDialogAction<'a, Message, Renderer> where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, +{ + on_pressed: Option, + + /// The content to show in the dialog box (typically a button) + child: Element<'a, Message, Renderer>, + + /// Use `is_enabled` to provide logic for making modal action buttons enabled/disabled. + /// Defaults to `true` + is_enabled: bool, +} + +impl<'a, Message, Renderer> Default for CupertinoDialogAction<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, +{ + fn default() -> Self { + Self { + is_enabled: true, + on_pressed: None, + child: Text::new("Example").into(), + } + } +} + +impl<'a, Message, Renderer> CupertinoDialogAction<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, +{ + /// Creates a new [`CupertinoDialogAction`] widget. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the `child` for the [`CupertinoDialogAction`](CupertinoDialogAction). + #[must_use] + pub fn child(mut self, child: Element<'a, Message, Renderer>) -> Self { + self.child = child; + self + } + + /// Sets `is_enabled` for the [`CupertinoDialogAction`](CupertinoDialogAction). + #[must_use] + pub fn is_enabled(mut self, is_enabled: bool) -> Self { + self.is_enabled = is_enabled; + self + } + + /// Sets `on_pressed` for the [`CupertinoDialogAction`](CupertinoDialogAction). + #[must_use] + pub fn on_pressed(mut self, on_pressed: Option) -> Self { + self.on_pressed = on_pressed; + self + } + +} + +/** + * `CupertinoAlert` + * + * See both + * + * 1. + * 2. + * + * as sources for API and behavior. The Iced AW modal (`src/native/modal.rs`) is also a source for + * the design of this struct. + * + * The example under `examples/cupertino/cupertino_alert/` shows how to work with the + * `CupertinoAlert`. + * + * Design and Default Behaviour + * ---------------------------- + * 1. By default: clicking the "Cancel" or "Confirm" buttons causes the modal to close + * 2. By default: pressing the escape key causes the modal to close + * 3. By default: clicking anywhere in the backdrop causes the modal to close + * 4. The modal assumes the actions are of length 2, with the "Confirm" action coming first, and + * the "Cancel" action coming second. + * + */ +#[allow(missing_debug_implementations)] +pub struct CupertinoAlert<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + width: Length, + height: Length, + is_hidden: bool, + title: String, + content: String, + + /// Dialog actions (confirm, cancel, etc.) + actions: Vec>, + + /// The optional message that will be sent when the user clicks on the backdrop. + backdrop: Option, + + /// The optional message that will be sent when the ESC key is pressed. + on_escape: Option, +} + +impl<'a, Message, Renderer> Default for CupertinoAlert<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + fn default() -> Self { + Self { + width: Length::Fixed(400.0), + height: Length::Fixed(200.0), + is_hidden: true, + title: "Title".to_string(), + content: "Content".to_string(), + actions: vec![], + backdrop: None, + on_escape: None, + } + } +} + +impl<'a, Message, Renderer> CupertinoAlert<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + /// Creates a new [`CupertinoAlert`] widget. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the `width` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the `height` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets `is_hidden` for the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn is_hidden(mut self, is_hidden: bool) -> Self { + self.is_hidden = is_hidden; + self + } + + /// Sets the `title` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn title(mut self, title: String) -> Self { + self.title = title; + self + } + + /// Sets the `content` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn content(mut self, content: String) -> Self { + self.content = content; + self + } + + /// Sets the `actions` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn actions(mut self, actions: Vec>) -> Self { + self.actions = actions; + self + } + + /// Sets the `backdrop` of the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn backdrop(mut self, backdrop: Option) -> Self { + self.backdrop = backdrop; + self + } + + /// Sets `on_escape` for the [`CupertinoAlert`](CupertinoAlert). + #[must_use] + pub fn on_escape(mut self, on_escape: Option) -> Self { + self.on_escape = on_escape; + self + } + + // Internal // + fn _text_with_font(element: T) -> Element<'a, Message, Renderer> + where T: Into> { + let as_text_element = element.into().font(SF_UI_ROUNDED); + + return Element::from(as_text_element); + } + +} + +impl<'a, Message, Renderer> Widget +for CupertinoAlert<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + fn width(&self) -> Length { + if !self.is_hidden { self.width } else { Length::Fixed(0.0) } + } + + fn height(&self) -> Length { + if !self.is_hidden { self.height } else { Length::Fixed(0.0) } + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &Limits, + ) -> Node { + return Node::new(limits + .width(if !self.is_hidden { self.width } else { Length::Fixed(0.0) }) + .height(if !self.is_hidden { self.height } else { Length::Fixed(0.0) }) + .resolve(Size::new(f32::INFINITY, f32::INFINITY)) + ); + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + // Technically, only 2 actions are supported at the moment... // + assert!((1..3).contains(&self.actions.len())); + + if !self.is_hidden { + let bounds: Rectangle = layout.bounds(); + let center: Point = bounds.center(); + + // The origin (`Point::ORIGIN`) leaves a slight gap in x and y. Move the point back + // (up-left) in x and y, and scale the size to cover the remaining space. + let rectangle: Rectangle = Rectangle::new( + Point { x: Point::ORIGIN.x - 100.0, y: Point::ORIGIN.y - 100.0 }, + Size { width: viewport.width + 100.0, height: viewport.height + 100.0 } + ); + + let draw_element = |r: &mut Renderer| { + // Overlay // + r.fill_quad( + Quad { + bounds: rectangle, + border_radius: [0.0, 0.0, 0.0, 0.0].into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + + iced_graphics::Background::Color(secondary_system_fill()) + ); + + // Dialog Box // + let Length::Fixed(width) = self.width else { todo!() }; + let Length::Fixed(height) = self.height else { todo!() }; + + // The `center` puts the top-left corner of the rectangle in the origin, so shift the + // rectangle up and to the left. The `height` calculation may seem strange, but + // it's to center the box on the page + let dialog_box: Rectangle = Rectangle::new( + Point { x: center.x - width / 2.0, y: center.y - 0.75 * height }, + Size { width, height } + ); + + r.fill_quad( + Quad { + bounds: dialog_box, + border_radius: [15.0, 15.0, 15.0, 15.0].into(), + border_width: 0.0, + border_color: Color::WHITE, + }, + + iced_graphics::Background::Color(Color::WHITE) + ); + + // Bottom Section // + // TODO: Cover the case in which there is only one action (for whatever reason; + // maybe just a cancel button?) + // This is where things get interesting. Draw lines using very thin rectangles! // + let bottom_bar: Rectangle = Rectangle::new( + Point { x: center.x - width / 2.0, y: center.y }, + Size { width, height: 2.0 } + ); + + // Horizontal Bar // + r.fill_quad( + Quad { + bounds: bottom_bar, + border_radius: [0.0, 0.0, 0.0, 0.0].into(), + border_width: 0.0, + border_color: secondary_system_fill(), + }, + + iced_graphics::Background::Color(secondary_system_fill()) + ); + + // Vertical Bar // + let vertical_bar: Rectangle = Rectangle::new( + Point { x: center.x, y: center.y }, + Size { width: 2.0, height: height / 4.0 } + ); + + r.fill_quad( + Quad { + bounds: vertical_bar, + border_radius: [0.0, 0.0, 0.0, 0.0].into(), + border_width: 0.0, + border_color: secondary_system_fill(), + }, + + iced_graphics::Background::Color(secondary_system_fill()) + ); + + if self.actions.len() == 2 { + let child_1 = self.actions[0].child.as_widget(); + let child_2 = self.actions[1].child.as_widget(); + + let Length::Fixed(child_1_width) = child_1.width() else { todo!() }; + let Length::Fixed(child_1_height) = child_1.height() else { todo!() }; + let Length::Fixed(child_2_width) = child_2.width() else { todo!() }; + let Length::Fixed(child_2_height) = child_2.height() else { todo!() }; + + let mut bottom_left: Node = Node::new(Size { + width: child_1_width, height: child_1_height + }); + + let mut bottom_right: Node = Node::new(Size { + width: child_2_width, height: child_2_height + }); + + bottom_left.move_to( + Point { x: center.x - width / 3.0 , y: center.y + 10.0 } + ); + + bottom_right.move_to( + Point { x: center.x + width / 6.0, y: center.y + 10.0 } + ); + + child_1.draw( + state, + r, + theme, + style, + Layout::new(&bottom_left), + cursor_position, + viewport, + ); + + child_2.draw( + state, + r, + theme, + style, + Layout::new(&bottom_right), + cursor_position, + viewport, + ); + } + + let mut title_node: Node = Node::new(Size { width, height: 75.0 }); + let mut content_node: Node = Node::new(Size { width, height: 150.0 }); + + title_node.move_to( + Point { x: center.x - width / 2.0, y: center.y - height / 1.5 } + ); + + content_node.move_to( + Point { x: center.x - width / 2.0, y: center.y - height / 3.0 } + ); + + let title: Element<'a, Message, Renderer> = CupertinoAlert::<'a, Message, Renderer>::_text_with_font( + Text::new(self.title.clone()).horizontal_alignment(alignment::Horizontal::Center), + ); + + title.as_widget().draw( + state, + r, + theme, + style, + Layout::new(&title_node), + cursor_position, + viewport, + ); + + let content: Element<'a, Message, Renderer> = CupertinoAlert::<'a, Message, Renderer>::_text_with_font( + Text::new(self.content.clone()).horizontal_alignment(alignment::Horizontal::Center), + ); + + content.as_widget().draw( + state, + r, + theme, + style, + Layout::new(&content_node), + cursor_position, + viewport, + ); + }; + + renderer.with_layer(rectangle, draw_element); + } + } + + 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>, + ) -> Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds: Rectangle = layout.bounds(); + + // If either of the bounds width/height are 0, exit early; this means the modal + // is changing from hidden to visible + if bounds.width == 0.0 || bounds.height == 0.0 { + return Status::Ignored; + } + + // TODO: Handle the case when there is only 1 button, for whatever reason... // + if self.actions.len() == 2 { + // For some reason, the button callbacks aren't being triggered... // + + // Check for left button click // + // The hit boxes here are, by design, more generous // + // For the Y hit box, the range is very narrow, hence more tuning for the + // range + if self.actions[0].on_pressed.is_some() { + let hit_x: Range = (bounds.x + 2.0 * bounds.width * 0.3)..(bounds.x + bounds.width * 0.9); + let hit_y: Range = (bounds.y + bounds.height / 2.0 + 10.0)..(bounds.y + bounds.height / 2.0 + 30.0); + + if hit_x.contains(&cursor_position.x) && hit_y.contains(&cursor_position.y) { + shell.publish(self.actions[0].on_pressed.clone().unwrap()); + } + } + + // Check for right button click // + if self.actions[1].on_pressed.is_some() { + let hit_x: Range = (bounds.x + bounds.width / 6.0)..(bounds.x + bounds.width / 2.0); + let hit_y: Range = (bounds.y + bounds.height / 2.0 + 10.0)..(bounds.y + bounds.height / 2.0 + 30.0); + + if hit_x.contains(&cursor_position.x) && hit_y.contains(&cursor_position.y) { + shell.publish(self.actions[1].on_pressed.clone().unwrap()); + } + } + } + + // Check for clicking on the overlay // + let hit_x: Range = bounds.x..(bounds.x + bounds.width); + let hit_y: Range = (bounds.y - bounds.height / 4.0)..(bounds.y + bounds.height * 0.85); + + if !hit_x.contains(&cursor_position.x) || !hit_y.contains(&cursor_position.y) { + if self.backdrop.is_some() { + shell.publish(self.backdrop.clone().unwrap()); + } + + // Default behaviour: hide the modal after clicking on the backdrop // + self.is_hidden = true; + } + + }, + + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + if key_code == keyboard::KeyCode::Escape && self.on_escape.is_some() { + self.is_hidden = true; + + shell.publish(self.on_escape.clone().unwrap()); + return Status::Captured; + } else { + return Status::Ignored; + } + }, + + _ => return Status::Ignored, + } + + return Status::Ignored; + } +} + +impl<'a, Message, Renderer: 'a> From> +for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + fn from(alert: CupertinoAlert<'a, Message, Renderer>) -> Self { + Self::new(alert) + } +} + diff --git a/src/native/cupertino/cupertino_button.rs b/src/native/cupertino/cupertino_button.rs new file mode 100644 index 00000000..f0754e46 --- /dev/null +++ b/src/native/cupertino/cupertino_button.rs @@ -0,0 +1,246 @@ +use iced_native::{ + application::StyleSheet, Clipboard, Color, Element, Event, event::Status, Font, Layout, + layout::{Limits, Node}, Length, mouse, Point, Rectangle, renderer::{Quad, Style}, Size, Shell, + touch, Widget, widget::{Text, Tree}, +}; + +// INTERNAL // +use crate::native::cupertino::cupertino_colours::{secondary_system_fill, system_blue}; +use crate::native::cupertino::fonts::SF_UI_ROUNDED; +// + +/** + * `CupertinoButton` + * + * See + * + * + * + * for constants, and + * + * + * + * for the Flutter example / expected usage. + * + */ +#[allow(missing_debug_implementations)] +pub struct CupertinoButton<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + on_pressed: Option, + is_filled: bool, + body: Element<'a, Message, Renderer>, + + /// `colour` is an option here because there is already logic to set the colour + /// depending on if the button is enabled/disabled. But if the button causes a + /// "destructive" behavior (e.g. a delete action), allow the user to override the + /// colour to e.g. red. + colour: Option, +} + +impl<'a, Message, Renderer> Default for CupertinoButton<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: iced_native::application::StyleSheet + iced_style::text::StyleSheet, + + ::Font: From, +{ + fn default() -> Self { + Self { + on_pressed: None, + is_filled: false, + body: Text::new("Hello").font(SF_UI_ROUNDED).into(), + colour: None, + } + } +} + +impl<'a, Message, Renderer> CupertinoButton<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: StyleSheet + iced_style::text::StyleSheet, + + ::Font: From +{ + /// Creates a new [`CupertinoButton`] widget. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the `on_pressed` callback of the [`CupertinoButton`](CupertinoButton). + #[must_use] + pub fn on_pressed(mut self, on_pressed: Option) -> Self { + self.on_pressed = on_pressed; + self + } + + /// Sets the `is_filled` of the [`CupertinoButton`](CupertinoButton). + #[must_use] + pub fn is_filled(mut self, is_filled: bool) -> Self { + self.is_filled = is_filled; + self + } + + /// Sets the `body` of the [`CupertinoButton`](CupertinoButton). + #[must_use] + pub fn body(mut self, body: T) -> Self + where + Message: Clone, + ::Font: From, + T: Into>, + { + let as_text = body.into().font(SF_UI_ROUNDED); + + self.body = Element::from(as_text); + self + } + + /// Sets the `colour` of the [`CupertinoButton`](CupertinoButton). + pub fn colour(mut self, colour: Option) -> Self { + self.colour = colour; + self + } +} + +const VERTICAL_PADDING: f32 = 14.0; +// const HORIZONTAL_PADDING: f32 = 64.0; + +impl<'a, Message, Renderer> Widget +for CupertinoButton<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { self.body.as_widget().width() } + fn height(&self) -> Length { self.body.as_widget().height() } + + fn layout( + &self, + renderer: &Renderer, + limits: &Limits, + ) -> Node { + return self.body.as_widget().layout(renderer, limits); + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + if self.is_filled { + let colour: Color = if self.on_pressed.is_none() { + secondary_system_fill() + } else { + system_blue(1.0) + }; + + let bounds: Rectangle = layout.bounds(); + let center: Point = bounds.center(); + + let rectangle: Rectangle = Rectangle::new( + Point { x: bounds.x, y: center.y - 3.0 * VERTICAL_PADDING }, + Size { + width: bounds.width, + height: bounds.height + VERTICAL_PADDING + }, + ); + + renderer.fill_quad( + Quad { + bounds: rectangle, + border_radius: [16.0, 16.0, 16.0, 16.0].into(), + border_width: 5.0, + border_color: Color::TRANSPARENT, + }, + iced_graphics::Background::Color(colour) + ); + } + + let new_style: &mut Style = &mut Style::default(); + + new_style.clone_from(style); + + if self.colour.is_some() { + new_style.text_color = self.colour.unwrap(); + } else if self.is_filled && self.on_pressed.is_some() { + new_style.text_color = Color::WHITE; + } else if !self.is_filled && self.on_pressed.is_some() { + new_style.text_color = system_blue(1.0); + } else if self.is_filled && self.on_pressed.is_none() { + new_style.text_color = Color::WHITE; + } else { + new_style.text_color = secondary_system_fill(); + } + + self.body.as_widget().draw( + state, + renderer, + theme, + new_style, + layout, + cursor_position, + viewport, + ) + } + + 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>, + ) -> Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds: Rectangle = layout.bounds(); + + if self.on_pressed.as_ref().is_some() { + let hit_x: bool = ((bounds.x + 10.0)..(bounds.x + bounds.width)) + .contains(&cursor_position.x); + + let hit_y: bool = ( + (bounds.y - 14.0)..(bounds.y + bounds.height - 10.0) + ).contains(&cursor_position.y); + + if hit_x && hit_y { + shell.publish(self.on_pressed.clone().unwrap()); + return Status::Captured; + } + } + }, + + _ => {}, + } + + return Status::Ignored; + } +} + +impl<'a, Message, Renderer> From> +for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(alert: CupertinoButton<'a, Message, Renderer>) -> Self { + Self::new(alert) + } +} + diff --git a/src/native/cupertino/cupertino_colours.rs b/src/native/cupertino/cupertino_colours.rs new file mode 100644 index 00000000..50e77330 --- /dev/null +++ b/src/native/cupertino/cupertino_colours.rs @@ -0,0 +1,50 @@ +use iced_native::Color; + +/// +/// +/// +/// +/// Because iced expects `r`, `g`, and `b` to be between 0 and 1, divide by 255 everywhere. +// System Colours // + +/// System Blue +#[must_use] +pub fn system_blue(alpha: f32) -> Color { Color::from_rgba(0.0 / 255.0, 122.0 / 255.0, 1.0, alpha) } + +/// System Green +#[must_use] +pub fn system_green(alpha: f32) -> Color { Color::from_rgba(52.0 / 255.0, 199.0 / 255.0, 89.0 / 255.0, alpha) } + +/// System Indigo +#[must_use] +pub fn system_indigo(alpha: f32) -> Color { Color::from_rgba(88.0 / 255.0, 86.0 / 255.0, 214.0 / 255.0, alpha) } + +/// System Orange +#[must_use] +pub fn system_orange(alpha: f32) -> Color { Color::from_rgba(1.0, 149.0 / 255.0, 0.0 / 255.0, alpha) } + +/// System Pink +#[must_use] +pub fn system_pink(alpha: f32) -> Color { Color::from_rgba(1.0, 45.0 / 255.0, 85.0 / 255.0, alpha) } + +/// System Purple +#[must_use] +pub fn system_purple(alpha: f32) -> Color { Color::from_rgba(175.0 / 255.0, 82.0 / 255.0, 222.0 / 255.0, alpha) } + +/// System Red +#[must_use] +pub fn system_red(alpha: f32) -> Color { Color::from_rgba(1.0, 59.0 / 255.0, 48.0 / 255.0, alpha) } + +/// System Teal +#[must_use] +pub fn system_teal(alpha: f32) -> Color { Color::from_rgba(90.0 / 255.0, 200.0 / 255.0, 250.0 / 255.0, alpha) } + +/// System Yellow +#[must_use] +pub fn system_yellow(alpha: f32) -> Color { Color::from_rgba(1.0, 204.0 / 255.0, 0.0 / 255.0, alpha) } +// + +/// Secondary System Fill +#[must_use] +pub fn secondary_system_fill() -> Color { Color::from_rgb(209.0 / 255.0, 209.0 / 255.0, 214.0 / 255.0) } + diff --git a/src/native/cupertino/cupertino_spinner.rs b/src/native/cupertino/cupertino_spinner.rs index a3530895..2056b40e 100644 --- a/src/native/cupertino/cupertino_spinner.rs +++ b/src/native/cupertino/cupertino_spinner.rs @@ -131,14 +131,14 @@ where let mut hands: Vec<(Path, _)> = vec![]; - for i in ALPHAS { + for alpha in &ALPHAS { 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, f32::from(i) / (60.0 + 147.0)), + Color::from_rgba(0.0, 0.0, 0.0, f32::from(*alpha) / (60.0 + 147.0)) ) }, )); @@ -173,8 +173,9 @@ where fn state(&self) -> State { State::new(SpinnerState { - now: time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + now: time::OffsetDateTime::now_local().unwrap_or_else( + |_| time::OffsetDateTime::now_utc() + ), spinner: Cache::default(), }) } diff --git a/src/native/cupertino/cupertino_switch.rs b/src/native/cupertino/cupertino_switch.rs new file mode 100644 index 00000000..cd478229 --- /dev/null +++ b/src/native/cupertino/cupertino_switch.rs @@ -0,0 +1,377 @@ +use iced_graphics::{Backend, Renderer}; + +use iced_native::{ + Clipboard, Color, Element, Event, event::Status, Layout, layout::{Limits, Node}, Length, mouse, + Point, Rectangle, renderer::Style, Size, Shell, touch, Vector, Widget, window, + widget::{tree::{State, Tag}, Tree}, +}; + +use iced_graphics::widget::canvas::{Cache, fill::Fill, Geometry, Path}; + +// INTERNAL // +use crate::native::cupertino::cupertino_colours::{secondary_system_fill, system_green}; +// + +/** + * `CupertinoSwitch` + * + * See + * + * 1. [Flutter Cupertino Switch](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/switch.dart) + * 2. [Flutter Cupertino Widgets](https://docs.flutter.dev/development/ui/widgets/cupertino) + * + * (1) for a couple constants, and colours. + * + * The examples folder (`examples/cupertino/cupertino_switch`) has a full example of usage. + * + */ +#[allow(missing_debug_implementations)] +pub struct CupertinoSwitch where Message: Clone { + width: Length, + height: Length, + active_colour: Color, + focus_colour: Color, + thumb_colour: Color, + track_colour: Color, + apply_theme: bool, // TODO // + on_changed: Option Message>>, + + /// The `CupertinoSwitch`'s value (true or false) + pub value: bool, + + // drag_start_behaviour: bool, // TODO // +} + +// A note about constants: +// ----------------------- +// Currently, this widget is not dynamic in `width` and `height`. Making it +// dynamic in size would affect the `draw` and `on_event` methods. +// +// 1) The sizes of the rectangle and circles would have to change ( `draw` ) +// 2) The frame count may need to change ( `draw` ) +// 3) The "hit box" for the button would have to change ( `on_event` ) +// +const ANIMATION_FRAME_COUNT: usize = 40; + +#[derive(Debug)] +struct SwitchState { + animation_frame: usize, + bounds: Rectangle, + prev_value: bool, + published: bool, + switch: Cache, + toggle_staged: bool, +} + +impl Default +for CupertinoSwitch +where Message: Clone { + fn default() -> Self { + Self { + width: Length::Fixed(180.0), + height: Length::Fixed(180.0), + active_colour: system_green(1.0), + focus_colour: system_green(0.8), + thumb_colour: Color::WHITE, + track_colour: secondary_system_fill(), + apply_theme: false, + on_changed: None, + value: true, + + // drag_start_behaviour: false, // TODO // + } + } +} + +impl CupertinoSwitch where Message: Clone { + /// Creates a new [`CupertinoSwitch`] widget. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the width of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the active colour of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn active_colour(mut self, colour: Color) -> Self { + self.active_colour = colour; + self + } + + /// Sets the focus colour of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn focus_colour(mut self, colour: Color) -> Self { + self.focus_colour = colour; + self + } + + /// Sets the thumb colour of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn thumb_colour(mut self, colour: Color) -> Self { + self.thumb_colour = colour; + self + } + + /// Sets `apply_theme` of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn apply_theme(mut self, theme: bool) -> Self { + self.apply_theme = theme; + self + } + + // /// Sets `drag_start_behaviour` of the [`CupertinoSwitch`](CupertinoSwitch). + // #[must_use] + // pub fn drag_start_behaviour(mut self, behaviour: bool) -> Self { + // self.drag_start_behaviour = behaviour; + // self + // } + + /// Sets `on_changed` of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn on_changed(mut self, on_changed: Option Message>>) -> Self { + self.on_changed = on_changed; + self + } + + /// Sets the value of the [`CupertinoSwitch`](CupertinoSwitch). + #[must_use] + pub fn value(mut self, value: bool) -> Self { + self.value = value; + self + } +} + +impl<'a, Message, B, T> Widget> +for CupertinoSwitch +where B: Backend, Message: Clone { + fn width(&self) -> Length { self.width } + fn height(&self) -> Length { self.height } + + fn layout( + &self, + _renderer: &Renderer, + limits: &Limits, + ) -> Node { + return Node::new(limits + .width(self.width) + .height(self.height) + .resolve(Size::new(f32::INFINITY, f32::INFINITY)) + ); + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + _theme: &T, + _style: &Style, + layout: Layout<'_>, + _cursor_position: Point, + viewport: &Rectangle, + ) { + let state: &SwitchState = state.state.downcast_ref::(); + + // TODO // + // let width: f32 = self.width; + // let height: f32 = self.height; + let width: f32 = 40.0; + let height: f32 = 40.0; + let radius: f32 = width / 2.0; + let padding: f32 = 1.5; + + let bounds: Rectangle = layout.bounds(); + + let switch: Geometry = state.switch.draw(Size { + width: viewport.width, + height: viewport.height + }, |frame| { + frame.translate(Vector::new( + bounds.x + 2.0 * width - 10.0, + bounds.y + 2.0 * width - 15.0 + )); + + let new_index: usize = state.animation_frame; + + if self.value { + frame.fill_rectangle( + Point::ORIGIN, + Size { width, height }, + Fill::from(self.active_colour), + ); + + frame.fill( + &Path::circle(Point { x: width, y: height / 2.0 }, radius), + Fill::from(self.active_colour), + ); + + frame.fill( + &Path::circle(Point { x: 0.0, y: height / 2.0 }, radius), + Fill::from(self.active_colour), + ); + + // Subtract `padding` to leave a slight gap // + frame.fill( + &Path::circle( + Point { x: width - padding - new_index as f32, y: height / 2.0 }, + radius - padding + ), + Fill::from(Color::WHITE) + ); + } else { + frame.fill_rectangle( + Point::ORIGIN, + Size { width, height }, + Fill::from(self.track_colour), + ); + + frame.fill( + &Path::circle(Point { x: width, y: height / 2.0 }, radius), + Fill::from(self.track_colour), + ); + + frame.fill( + &Path::circle(Point { x: 0.0, y: height / 2.0 }, radius), + Fill::from(self.track_colour), + ); + + // Subtract `padding` to leave a slight gap // + frame.fill( + &Path::circle( + Point { x: 0.0 + padding + new_index as f32, y: height / 2.0 }, + radius - padding + ), + Fill::from(Color::WHITE) + ); + } + }); + + // A useful debugging tool for element position... // + // renderer.draw_primitive(Primitive::Quad { + // bounds: state.bounds, + // background: iced_graphics::Background::Color(Color::TRANSPARENT), + // border_radius: [1.0, 1.0, 1.0, 1.0], + // border_width: 5.0, + // border_color: Color::BLACK, + // }); + // + + renderer.draw_primitive(switch.into_primitive()); + } + + fn tag(&self) -> Tag { Tag::of::() } + + fn state(&self) -> State { + State::new(SwitchState { + animation_frame: 0, + bounds: Rectangle::default(), + prev_value: self.value, + published: false, + switch: Cache::default(), + toggle_staged: false, + }) + } + + 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>, + ) -> Status { + let state: &mut SwitchState = state.state.downcast_mut::(); + + match event { + Event::Window(window::Event::RedrawRequested(_now)) => { + if state.toggle_staged { + state.animation_frame += 1; + + // This machinery is built to accommodate for the most bizarre + // behaviour that only happens when `shell.publish` is called... + if state.published && self.value != state.prev_value { + self.value = !self.value; + } + + if state.animation_frame >= ANIMATION_FRAME_COUNT { + self.value = !self.value; + state.toggle_staged = false; + state.animation_frame = 0; + state.published = false; + } + + state.switch.clear(); + shell.request_redraw(window::RedrawRequest::NextFrame); + } + + return Status::Captured; + }, + + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + // TODO: Make these calculations not hard-coded // + let hit_x: bool = ((state.bounds.x + 50.0)..(state.bounds.x + 125.0)) + .contains(&cursor_position.x); + + let hit_y: bool = ((state.bounds.y + 70.0)..(state.bounds.y + 100.0)) + .contains(&cursor_position.y); + + if hit_x && hit_y { + state.toggle_staged = true; + state.animation_frame = 0; + + if self.on_changed.as_ref().is_some() { + shell.publish( + (self.on_changed.as_ref().unwrap())(!self.value) + ); + + state.prev_value = self.value; + state.published = true; + } + + return Status::Captured; + } + } + + _ => {} + } + + return Status::Ignored; + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option>> { + let state: &mut SwitchState = state.state.downcast_mut::(); + + state.bounds = layout.bounds(); + + return None; + } +} + +impl<'a, Message, B, T> From> +for Element<'a, Message, Renderer> +where B: Backend, Message: Clone + 'a { + fn from(switch: CupertinoSwitch) -> Self { + Self::new(switch) + } +} + diff --git a/src/native/cupertino/fonts/SFUIMono.ttf b/src/native/cupertino/fonts/SFUIMono.ttf new file mode 100644 index 00000000..24083a15 Binary files /dev/null and b/src/native/cupertino/fonts/SFUIMono.ttf differ diff --git a/src/native/cupertino/fonts/SFUIRounded.ttf b/src/native/cupertino/fonts/SFUIRounded.ttf new file mode 100644 index 00000000..ddd11349 Binary files /dev/null and b/src/native/cupertino/fonts/SFUIRounded.ttf differ diff --git a/src/native/cupertino/fonts/mod.rs b/src/native/cupertino/fonts/mod.rs new file mode 100644 index 00000000..e7caceba --- /dev/null +++ b/src/native/cupertino/fonts/mod.rs @@ -0,0 +1,14 @@ +use iced_native::Font; + +/// `SFUIRounded` font +pub const SF_UI_ROUNDED: Font = Font::External { + name: "SFUIRounded", + bytes: include_bytes!("SFUIRounded.ttf"), +}; + +/// `SFUIMono` font +pub const SF_UI_MONO: Font = Font::External { + name: "SFUIMono", + bytes: include_bytes!("SFUIMono.ttf"), +}; + diff --git a/src/native/cupertino/mod.rs b/src/native/cupertino/mod.rs index 86dd048f..e467a914 100644 --- a/src/native/cupertino/mod.rs +++ b/src/native/cupertino/mod.rs @@ -1,4 +1,30 @@ +/// Use a Cupertino-style alert. +/// +/// *This API requires the following crate features to be activated: `cupertino`* +pub mod cupertino_alert; + +/// Use a Cupertino-style button. +/// +/// *This API requires the following crate features to be activated: `cupertino`* +pub mod cupertino_button; + +/// Use Cupertino-style colours. +/// +/// *This API requires the following crate features to be activated: `cupertino`* +pub mod cupertino_colours; + /// Use a Cupertino-style spinner element. /// /// *This API requires the following crate features to be activated: `cupertino`* pub mod cupertino_spinner; + +/// Use a Cupertino-style switch element. +/// +/// *This API requires the following crate features to be activated: `cupertino`* +pub mod cupertino_switch; + +/// Use a Cupertino-style fonts. +/// +/// *This API requires the following crate features to be activated: `cupertino`* +pub mod fonts; +