From c22269bff3085012d326a0df77bf27ad5bcb41b7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Mar 2024 05:33:47 +0100 Subject: Introduce `Program` API --- src/lib.rs | 48 ++++ src/program.rs | 669 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/settings.rs | 2 +- 3 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 src/program.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index c596f2a6..f42a845e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,6 +175,7 @@ mod error; mod sandbox; pub mod application; +pub mod program; pub mod settings; pub mod time; pub mod window; @@ -308,6 +309,7 @@ pub use error::Error; pub use event::Event; pub use executor::Executor; pub use font::Font; +pub use program::Program; pub use renderer::Renderer; pub use sandbox::Sandbox; pub use settings::Settings; @@ -327,3 +329,49 @@ pub type Element< /// /// [`Application`]: crate::Application pub type Result = std::result::Result<(), Error>; + +/// Runs a basic iced application with default [`Settings`] given +/// - its window title, +/// - its update logic, +/// - and its view logic. +/// +/// # Example +/// ```no_run +/// use iced::widget::{button, column, text, Column}; +/// +/// pub fn main() -> iced::Result { +/// iced::run("A counter", update, view) +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } +/// +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, +/// } +/// } +/// +/// fn view(value: &u64) -> Column { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] +/// } +/// ``` +pub fn run( + title: &'static str, + update: impl Fn(&mut State, Message) + 'static, + view: impl for<'a> program::View<'a, State, Message> + 'static, +) -> Result +where + State: Default + 'static, + Message: std::fmt::Debug + Send + 'static, +{ + sandbox(update, view).title(title).run() +} + +#[doc(inline)] +pub use program::{application, sandbox}; diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 00000000..d78fa505 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,669 @@ +//! Create iced applications out of simple functions. +//! +//! You can use this API to create and run iced applications +//! step by step—without coupling your logic to a trait +//! or a specific type. +//! +//! This API is meant to be a more convenient—although less +//! powerful—alternative to the [`Sandbox`] and [`Application`] traits. +//! +//! [`Sandbox`]: crate::Sandbox +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::sandbox(update, view) +//! .title("A counter") +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::application::{self, Application}; +use crate::executor::{self, Executor}; +use crate::window; +use crate::{Command, Element, Font, Result, Settings, Subscription}; + +use std::borrow::Cow; + +/// Creates the most basic kind of [`Program`] from some update and view logic. +/// +/// # Example +/// ```no_run +/// use iced::widget::{button, column, text, Column}; +/// +/// pub fn main() -> iced::Result { +/// iced::sandbox(update, view).title("A counter").run() +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } +/// +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, +/// } +/// } +/// +/// fn view(value: &u64) -> Column { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] +/// } +/// ``` +pub fn sandbox( + update: impl Fn(&mut State, Message), + view: impl for<'a> self::View<'a, State, Message>, +) -> Program< + impl Definition, +> +where + State: Default + 'static, + Message: Send + std::fmt::Debug, +{ + use std::marker::PhantomData; + + struct Sandbox { + update: Update, + view: View, + _state: PhantomData, + _message: PhantomData, + } + + impl Definition + for Sandbox + where + State: Default + 'static, + Message: Send + std::fmt::Debug, + Update: Fn(&mut State, Message), + View: for<'a> self::View<'a, State, Message>, + { + type State = State; + type Message = Message; + type Theme = crate::Theme; + type Executor = iced_futures::backend::null::Executor; + + fn new(&self) -> (Self::State, Command) { + (State::default(), Command::none()) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command { + (self.update)(state, message); + + Command::none() + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.view.view(state).into() + } + } + + Program { + raw: Sandbox { + update, + view, + _state: PhantomData, + _message: PhantomData, + }, + settings: Settings::default(), + } +} + +/// Creates a [`Program`] that can leverage the [`Command`] API for +/// concurrent operations. +pub fn application( + new: impl Fn() -> (State, Command), + update: impl Fn(&mut State, Message) -> Command, + view: impl for<'a> self::View<'a, State, Message>, +) -> Program< + impl Definition, +> +where + State: 'static, + Message: Send + std::fmt::Debug, +{ + use std::marker::PhantomData; + + struct Application { + new: New, + update: Update, + view: View, + _state: PhantomData, + _message: PhantomData, + } + + impl Definition + for Application + where + Message: Send + std::fmt::Debug, + New: Fn() -> (State, Command), + Update: Fn(&mut State, Message) -> Command, + View: for<'a> self::View<'a, State, Message>, + { + type State = State; + type Message = Message; + type Theme = crate::Theme; + type Executor = executor::Default; + + fn new(&self) -> (Self::State, Command) { + (self.new)() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command { + (self.update)(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.view.view(state).into() + } + } + + Program { + raw: Application { + new, + update, + view, + _state: PhantomData, + _message: PhantomData, + }, + settings: Settings::default(), + } +} + +/// A fully functioning and configured iced application. +/// +/// It can be [`run`]! +/// +/// Create one with either the [`sandbox`] or [`application`] helpers. +/// +/// [`run`]: Program::run +/// [`application`]: self::application() +#[derive(Debug)] +pub struct Program { + raw: P, + settings: Settings, +} + +impl Program

{ + /// Runs the [`Program`]. + pub fn run(self) -> Result + where + Self: 'static, + { + struct Instance { + program: P, + state: P::State, + } + + impl Application for Instance

{ + type Message = P::Message; + type Theme = P::Theme; + type Flags = P; + type Executor = P::Executor; + + fn new(program: Self::Flags) -> (Self, Command) { + let (state, command) = P::new(&program); + + (Self { program, state }, command) + } + + fn title(&self) -> String { + self.program.title(&self.state) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Command { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + ) -> crate::Element<'_, Self::Message, Self::Theme, crate::Renderer> + { + self.program.view(&self.state) + } + + fn theme(&self) -> Self::Theme { + self.program.theme(&self.state) + } + + fn subscription(&self) -> Subscription { + self.program.subscription(&self.state) + } + } + + let Self { raw, settings } = self; + + Instance::run(Settings { + flags: raw, + id: settings.id, + window: settings.window, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + }) + } + + /// Sets the [`Settings`] that will be used to run the [`Program`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Toggles the [`Settings::antialiasing`] to `true` for the [`Program`]. + pub fn antialiased(self) -> Self { + Self { + settings: Settings { + antialiasing: true, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Program`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Sets the fonts that will be loaded at the start of the [`Program`]. + pub fn fonts( + self, + fonts: impl IntoIterator>, + ) -> Self { + Self { + settings: Settings { + fonts: fonts.into_iter().collect(), + ..self.settings + }, + ..self + } + } + + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Program`]. + pub fn centered(self) -> Self { + Self { + settings: Settings { + window: window::Settings { + position: window::Position::Centered, + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`window::Settings::exit_on_close_request`] to `false` in the [`Program`]. + pub fn ignore_close_request(self) -> Self { + Self { + settings: Settings { + window: window::Settings { + exit_on_close_request: false, + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`Title`] of the [`Program`]. + pub fn title( + self, + title: impl Title, + ) -> Program< + impl Definition, + > { + Program { + raw: with_title(self.raw, title), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Program`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription, + ) -> Program< + impl Definition, + > { + Program { + raw: with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Program`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Program< + impl Definition, + > { + Program { + raw: with_theme(self.raw, f), + settings: self.settings, + } + } +} + +/// The internal definition of a [`Program`]. +/// +/// You should not need to implement this trait directly. Instead, use the +/// helper functions available in the [`program`] module and the [`Program`] struct. +/// +/// [`program`]: crate::program +#[allow(missing_docs)] +pub trait Definition: Sized { + /// The state of the program. + type State; + + /// The message of the program. + type Message: Send + std::fmt::Debug; + + /// The theme of the program. + type Theme: Default + application::DefaultStyle; + + /// The executor of the program. + type Executor: Executor; + + fn new(&self) -> (Self::State, Command); + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command; + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme>; + + fn title(&self, _state: &Self::State) -> String { + String::from("A cool iced application!") + } + + fn subscription( + &self, + _state: &Self::State, + ) -> Subscription { + Subscription::none() + } + + fn theme(&self, _state: &Self::State) -> Self::Theme { + Self::Theme::default() + } +} + +fn with_title( + program: P, + title: impl Title, +) -> impl Definition { + struct WithTitle { + program: P, + title: Title, + } + + impl Definition for WithTitle + where + P: Definition, + Title: self::Title, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = P::Executor; + + fn new(&self) -> (Self::State, Command) { + self.program.new() + } + + fn title(&self, state: &Self::State) -> String { + self.title.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + } + + WithTitle { program, title } +} + +fn with_subscription( + program: P, + f: impl Fn(&P::State) -> Subscription, +) -> impl Definition { + struct WithSubscription { + program: P, + subscription: F, + } + + impl Definition for WithSubscription + where + F: Fn(&P::State) -> Subscription, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = executor::Default; + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + (self.subscription)(state) + } + + fn new(&self) -> (Self::State, Command) { + self.program.new() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + } + + WithSubscription { + program, + subscription: f, + } +} + +fn with_theme( + program: P, + f: impl Fn(&P::State) -> P::Theme, +) -> impl Definition { + struct WithTheme { + program: P, + theme: F, + } + + impl Definition for WithTheme + where + F: Fn(&P::State) -> P::Theme, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = P::Executor; + + fn theme(&self, state: &Self::State) -> Self::Theme { + (self.theme)(state) + } + + fn new(&self) -> (Self::State, Command) { + self.program.new() + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + } + + WithTheme { program, theme: f } +} + +/// The title logic of some [`Program`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// You can use any of these in [`Program::title`]. +pub trait Title { + /// Produces the title of the [`Program`]. + fn title(&self, state: &State) -> String; +} + +impl Title for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() + } +} + +impl Title for T +where + T: Fn(&State) -> String, +{ + fn title(&self, state: &State) -> String { + self(state) + } +} + +/// The view logic of some [`Program`]. +/// +/// This trait allows [`sandbox`] and [`application`] to +/// take any closure that returns any `Into>`. +/// +/// [`application`]: self::application() +pub trait View<'a, State, Message> { + /// The widget returned by the view logic. + type Widget: Into>; + + /// Produces the widget of the [`Program`]. + fn view(&self, state: &'a State) -> Self::Widget; +} + +impl<'a, T, State, Message, Widget> View<'a, State, Message> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into>, +{ + type Widget = Widget; + + fn view(&self, state: &'a State) -> Self::Widget { + self(state) + } +} diff --git a/src/settings.rs b/src/settings.rs index d9476b61..92204847 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; /// The settings of an application. #[derive(Debug, Clone)] -pub struct Settings { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or -- cgit