diff options
59 files changed, 7832 insertions, 1373 deletions
@@ -44,6 +44,8 @@ async-std = ["iced_futures/async-std"]  smol = ["iced_futures/smol"]  # Enables advanced color conversion via `palette`  palette = ["iced_core/palette"] +# Enables pure, virtual widgets in the `pure` module +pure = ["iced_pure"]  [badges]  maintenance = { status = "actively-developed" } @@ -57,6 +59,7 @@ members = [      "glutin",      "lazy",      "native", +    "pure",      "style",      "wgpu",      "winit", @@ -87,6 +90,11 @@ members = [      "examples/tooltip",      "examples/tour",      "examples/url_handler", +    "examples/pure/component", +    "examples/pure/counter", +    "examples/pure/pick_list", +    "examples/pure/todos", +    "examples/pure/tour",      "examples/websocket",  ] @@ -96,6 +104,7 @@ iced_futures = { version = "0.3", path = "futures" }  iced_winit = { version = "0.3", path = "winit" }  iced_glutin = { version = "0.2", path = "glutin", optional = true }  iced_glow = { version = "0.2", path = "glow", optional = true } +iced_pure = { version = "0.1", path = "pure", optional = true }  thiserror = "1.0"  [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml new file mode 100644 index 00000000..a15f134f --- /dev/null +++ b/examples/pure/component/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pure_component" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } +iced_native = { path = "../../../native" } +iced_lazy = { path = "../../../lazy", features = ["pure"] } diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs new file mode 100644 index 00000000..0de7bdd9 --- /dev/null +++ b/examples/pure/component/src/main.rs @@ -0,0 +1,168 @@ +use iced::pure::widget::container; +use iced::pure::{Element, Sandbox}; +use iced::{Length, Settings}; + +use numeric_input::numeric_input; + +pub fn main() -> iced::Result { +    Component::run(Settings::default()) +} + +#[derive(Default)] +struct Component { +    value: Option<u32>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { +    NumericInputChanged(Option<u32>), +} + +impl Sandbox for Component { +    type Message = Message; + +    fn new() -> Self { +        Self::default() +    } + +    fn title(&self) -> String { +        String::from("Component - Iced") +    } + +    fn update(&mut self, message: Message) { +        match message { +            Message::NumericInputChanged(value) => { +                self.value = value; +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        container(numeric_input(self.value, Message::NumericInputChanged)) +            .padding(20) +            .height(Length::Fill) +            .center_y() +            .into() +    } +} + +mod numeric_input { +    use iced::pure::widget::Element; +    use iced::pure::widget::{row, text, text_input}; +    use iced_lazy::pure::component::{self, Component}; +    use iced_native::alignment::{self, Alignment}; +    use iced_native::text; +    use iced_native::Length; + +    pub struct NumericInput<Message> { +        value: Option<u32>, +        on_change: Box<dyn Fn(Option<u32>) -> Message>, +    } + +    pub fn numeric_input<Message>( +        value: Option<u32>, +        on_change: impl Fn(Option<u32>) -> Message + 'static, +    ) -> NumericInput<Message> { +        NumericInput::new(value, on_change) +    } + +    #[derive(Debug, Clone)] +    pub enum Event { +        InputChanged(String), +        IncrementPressed, +        DecrementPressed, +    } + +    impl<Message> NumericInput<Message> { +        pub fn new( +            value: Option<u32>, +            on_change: impl Fn(Option<u32>) -> Message + 'static, +        ) -> Self { +            Self { +                value, +                on_change: Box::new(on_change), +            } +        } +    } + +    impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message> +    where +        Renderer: text::Renderer + 'static, +    { +        type State = (); +        type Event = Event; + +        fn update( +            &mut self, +            _state: &mut Self::State, +            event: Event, +        ) -> Option<Message> { +            match event { +                Event::IncrementPressed => Some((self.on_change)(Some( +                    self.value.unwrap_or_default().saturating_add(1), +                ))), +                Event::DecrementPressed => Some((self.on_change)(Some( +                    self.value.unwrap_or_default().saturating_sub(1), +                ))), +                Event::InputChanged(value) => { +                    if value.is_empty() { +                        Some((self.on_change)(None)) +                    } else { +                        value +                            .parse() +                            .ok() +                            .map(Some) +                            .map(self.on_change.as_ref()) +                    } +                } +            } +        } + +        fn view(&self, _state: &Self::State) -> Element<Event, Renderer> { +            let button = |label, on_press| { +                use iced::pure::widget::button; + +                button( +                    text(label) +                        .width(Length::Fill) +                        .height(Length::Fill) +                        .horizontal_alignment(alignment::Horizontal::Center) +                        .vertical_alignment(alignment::Vertical::Center), +                ) +                .width(Length::Units(50)) +                .on_press(on_press) +            }; + +            row() +                .push(button("-", Event::DecrementPressed)) +                .push( +                    text_input( +                        "Type a number", +                        self.value +                            .as_ref() +                            .map(u32::to_string) +                            .as_ref() +                            .map(String::as_str) +                            .unwrap_or(""), +                        Event::InputChanged, +                    ) +                    .padding(10), +                ) +                .push(button("+", Event::IncrementPressed)) +                .align_items(Alignment::Fill) +                .spacing(10) +                .into() +        } +    } + +    impl<'a, Message, Renderer> From<NumericInput<Message>> +        for Element<'a, Message, Renderer> +    where +        Message: 'a, +        Renderer: 'static + text::Renderer, +    { +        fn from(numeric_input: NumericInput<Message>) -> Self { +            component::view(numeric_input) +        } +    } +} diff --git a/examples/pure/counter/Cargo.toml b/examples/pure/counter/Cargo.toml new file mode 100644 index 00000000..2fcd22d4 --- /dev/null +++ b/examples/pure/counter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pure_counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/counter/README.md b/examples/pure/counter/README.md new file mode 100644 index 00000000..4d9fc5b9 --- /dev/null +++ b/examples/pure/counter/README.md @@ -0,0 +1,18 @@ +## Counter + +The classic counter example explained in the [`README`](../../README.md). + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> +  <a href="https://gfycat.com/fairdeadcatbird"> +    <img src="https://thumbs.gfycat.com/FairDeadCatbird-small.gif"> +  </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package counter +``` + +[`main`]: src/main.rs diff --git a/examples/pure/counter/index.html b/examples/pure/counter/index.html new file mode 100644 index 00000000..d2e368e4 --- /dev/null +++ b/examples/pure/counter/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +    <meta charset="utf-8" content="text/html; charset=utf-8" /> +    <meta name="viewport" content="width=device-width, initial-scale=1" /> +    <title>Counter - Iced</title> +    <base data-trunk-public-url /> +</head> +<body> +<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="counter" /> +</body> +</html> diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs new file mode 100644 index 00000000..9b520347 --- /dev/null +++ b/examples/pure/counter/src/main.rs @@ -0,0 +1,50 @@ +use iced::pure::widget::{button, column, text}; +use iced::pure::{Element, Sandbox}; +use iced::{Alignment, Settings}; + +pub fn main() -> iced::Result { +    Counter::run(Settings::default()) +} + +struct Counter { +    value: i32, +} + +#[derive(Debug, Clone, Copy)] +enum Message { +    IncrementPressed, +    DecrementPressed, +} + +impl Sandbox for Counter { +    type Message = Message; + +    fn new() -> Self { +        Self { value: 0 } +    } + +    fn title(&self) -> String { +        String::from("Counter - Iced") +    } + +    fn update(&mut self, message: Message) { +        match message { +            Message::IncrementPressed => { +                self.value += 1; +            } +            Message::DecrementPressed => { +                self.value -= 1; +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        column() +            .padding(20) +            .align_items(Alignment::Center) +            .push(button("Increment").on_press(Message::IncrementPressed)) +            .push(text(self.value.to_string()).size(50)) +            .push(button("Decrement").on_press(Message::DecrementPressed)) +            .into() +    } +} diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml new file mode 100644 index 00000000..c0fcac3c --- /dev/null +++ b/examples/pure/pick_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pure_pick_list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } diff --git a/examples/pure/pick_list/README.md b/examples/pure/pick_list/README.md new file mode 100644 index 00000000..6dc80bf4 --- /dev/null +++ b/examples/pure/pick_list/README.md @@ -0,0 +1,18 @@ +## Pick-list + +A dropdown list of selectable options. + +It displays and positions an overlay based on the window position of the widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> +  <img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package pick_list +``` + +[`main`]: src/main.rs diff --git a/examples/pure/pick_list/src/main.rs b/examples/pure/pick_list/src/main.rs new file mode 100644 index 00000000..f9d55dd0 --- /dev/null +++ b/examples/pure/pick_list/src/main.rs @@ -0,0 +1,111 @@ +use iced::pure::widget::{ +    column, container, pick_list, scrollable, vertical_space, +}; +use iced::pure::{Element, Sandbox}; +use iced::{Alignment, Length, Settings}; + +pub fn main() -> iced::Result { +    Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { +    selected_language: Option<Language>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { +    LanguageSelected(Language), +} + +impl Sandbox for Example { +    type Message = Message; + +    fn new() -> Self { +        Self::default() +    } + +    fn title(&self) -> String { +        String::from("Pick list - Iced") +    } + +    fn update(&mut self, message: Message) { +        match message { +            Message::LanguageSelected(language) => { +                self.selected_language = Some(language); +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        let pick_list = pick_list( +            &Language::ALL[..], +            self.selected_language, +            Message::LanguageSelected, +        ) +        .placeholder("Choose a language..."); + +        let content = column() +            .width(Length::Fill) +            .align_items(Alignment::Center) +            .spacing(10) +            .push(vertical_space(Length::Units(600))) +            .push("Which is your favorite language?") +            .push(pick_list) +            .push(vertical_space(Length::Units(600))); + +        container(scrollable(content)) +            .width(Length::Fill) +            .height(Length::Fill) +            .center_x() +            .center_y() +            .into() +    } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { +    Rust, +    Elm, +    Ruby, +    Haskell, +    C, +    Javascript, +    Other, +} + +impl Language { +    const ALL: [Language; 7] = [ +        Language::C, +        Language::Elm, +        Language::Ruby, +        Language::Haskell, +        Language::Rust, +        Language::Javascript, +        Language::Other, +    ]; +} + +impl Default for Language { +    fn default() -> Language { +        Language::Rust +    } +} + +impl std::fmt::Display for Language { +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +        write!( +            f, +            "{}", +            match self { +                Language::Rust => "Rust", +                Language::Elm => "Elm", +                Language::Ruby => "Ruby", +                Language::Haskell => "Haskell", +                Language::C => "C", +                Language::Javascript => "Javascript", +                Language::Other => "Some other language", +            } +        ) +    } +} diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml new file mode 100644 index 00000000..217179e8 --- /dev/null +++ b/examples/pure/todos/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pure_todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["async-std", "debug", "default_system_font", "pure"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = "1.0" +directories-next = "2.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3", features = ["Window", "Storage"] } +wasm-timer = "0.2" diff --git a/examples/pure/todos/README.md b/examples/pure/todos/README.md new file mode 100644 index 00000000..9c2598b9 --- /dev/null +++ b/examples/pure/todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + +<div align="center"> +  <a href="https://gfycat.com/littlesanehalicore"> +    <img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px"> +  </a> +</div> + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ diff --git a/examples/pure/todos/index.html b/examples/pure/todos/index.html new file mode 100644 index 00000000..ee5570fb --- /dev/null +++ b/examples/pure/todos/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +    <meta charset="utf-8" content="text/html; charset=utf-8" /> +    <meta name="viewport" content="width=device-width, initial-scale=1" /> +    <title>Todos - Iced</title> +    <base data-trunk-public-url /> +</head> +<body> +<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="todos" /> +</body> +</html> diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs new file mode 100644 index 00000000..e993c598 --- /dev/null +++ b/examples/pure/todos/src/main.rs @@ -0,0 +1,607 @@ +use iced::alignment::{self, Alignment}; +use iced::pure::widget::{ +    button, checkbox, column, container, row, scrollable, text, text_input, +}; +use iced::pure::{Application, Element, Text}; +use iced::window; +use iced::{Command, Font, Length, Settings}; +use serde::{Deserialize, Serialize}; + +pub fn main() -> iced::Result { +    Todos::run(Settings { +        window: window::Settings { +            size: (500, 800), +            ..window::Settings::default() +        }, +        ..Settings::default() +    }) +} + +#[derive(Debug)] +enum Todos { +    Loading, +    Loaded(State), +} + +#[derive(Debug, Default)] +struct State { +    input_value: String, +    filter: Filter, +    tasks: Vec<Task>, +    dirty: bool, +    saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { +    Loaded(Result<SavedState, LoadError>), +    Saved(Result<(), SaveError>), +    InputChanged(String), +    CreateTask, +    FilterChanged(Filter), +    TaskMessage(usize, TaskMessage), +} + +impl Application for Todos { +    type Executor = iced::executor::Default; +    type Message = Message; +    type Flags = (); + +    fn new(_flags: ()) -> (Todos, Command<Message>) { +        ( +            Todos::Loading, +            Command::perform(SavedState::load(), Message::Loaded), +        ) +    } + +    fn title(&self) -> String { +        let dirty = match self { +            Todos::Loading => false, +            Todos::Loaded(state) => state.dirty, +        }; + +        format!("Todos{} - Iced", if dirty { "*" } else { "" }) +    } + +    fn update(&mut self, message: Message) -> Command<Message> { +        match self { +            Todos::Loading => { +                match message { +                    Message::Loaded(Ok(state)) => { +                        *self = Todos::Loaded(State { +                            input_value: state.input_value, +                            filter: state.filter, +                            tasks: state.tasks, +                            ..State::default() +                        }); +                    } +                    Message::Loaded(Err(_)) => { +                        *self = Todos::Loaded(State::default()); +                    } +                    _ => {} +                } + +                Command::none() +            } +            Todos::Loaded(state) => { +                let mut saved = false; + +                match message { +                    Message::InputChanged(value) => { +                        state.input_value = value; +                    } +                    Message::CreateTask => { +                        if !state.input_value.is_empty() { +                            state +                                .tasks +                                .push(Task::new(state.input_value.clone())); +                            state.input_value.clear(); +                        } +                    } +                    Message::FilterChanged(filter) => { +                        state.filter = filter; +                    } +                    Message::TaskMessage(i, TaskMessage::Delete) => { +                        state.tasks.remove(i); +                    } +                    Message::TaskMessage(i, task_message) => { +                        if let Some(task) = state.tasks.get_mut(i) { +                            task.update(task_message); +                        } +                    } +                    Message::Saved(_) => { +                        state.saving = false; +                        saved = true; +                    } +                    _ => {} +                } + +                if !saved { +                    state.dirty = true; +                } + +                if state.dirty && !state.saving { +                    state.dirty = false; +                    state.saving = true; + +                    Command::perform( +                        SavedState { +                            input_value: state.input_value.clone(), +                            filter: state.filter, +                            tasks: state.tasks.clone(), +                        } +                        .save(), +                        Message::Saved, +                    ) +                } else { +                    Command::none() +                } +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        match self { +            Todos::Loading => loading_message(), +            Todos::Loaded(State { +                input_value, +                filter, +                tasks, +                .. +            }) => { +                let title = text("todos") +                    .width(Length::Fill) +                    .size(100) +                    .color([0.5, 0.5, 0.5]) +                    .horizontal_alignment(alignment::Horizontal::Center); + +                let input = text_input( +                    "What needs to be done?", +                    input_value, +                    Message::InputChanged, +                ) +                .padding(15) +                .size(30) +                .on_submit(Message::CreateTask); + +                let controls = view_controls(&tasks, *filter); +                let filtered_tasks = +                    tasks.iter().filter(|task| filter.matches(task)); + +                let tasks: Element<_> = if filtered_tasks.count() > 0 { +                    tasks +                        .iter() +                        .enumerate() +                        .filter(|(_, task)| filter.matches(task)) +                        .fold(column().spacing(20), |column, (i, task)| { +                            column.push(task.view().map(move |message| { +                                Message::TaskMessage(i, message) +                            })) +                        }) +                        .into() +                } else { +                    empty_message(match filter { +                        Filter::All => "You have not created a task yet...", +                        Filter::Active => "All your tasks are done! :D", +                        Filter::Completed => { +                            "You have not completed a task yet..." +                        } +                    }) +                }; + +                let content = column() +                    .spacing(20) +                    .max_width(800) +                    .push(title) +                    .push(input) +                    .push(controls) +                    .push(tasks); + +                scrollable( +                    container(content) +                        .width(Length::Fill) +                        .padding(40) +                        .center_x(), +                ) +                .into() +            } +        } +    } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { +    description: String, +    completed: bool, + +    #[serde(skip)] +    state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { +    Idle, +    Editing, +} + +impl Default for TaskState { +    fn default() -> Self { +        Self::Idle +    } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { +    Completed(bool), +    Edit, +    DescriptionEdited(String), +    FinishEdition, +    Delete, +} + +impl Task { +    fn new(description: String) -> Self { +        Task { +            description, +            completed: false, +            state: TaskState::Idle, +        } +    } + +    fn update(&mut self, message: TaskMessage) { +        match message { +            TaskMessage::Completed(completed) => { +                self.completed = completed; +            } +            TaskMessage::Edit => { +                self.state = TaskState::Editing; +            } +            TaskMessage::DescriptionEdited(new_description) => { +                self.description = new_description; +            } +            TaskMessage::FinishEdition => { +                if !self.description.is_empty() { +                    self.state = TaskState::Idle; +                } +            } +            TaskMessage::Delete => {} +        } +    } + +    fn view(&self) -> Element<TaskMessage> { +        match &self.state { +            TaskState::Idle => { +                let checkbox = checkbox( +                    &self.description, +                    self.completed, +                    TaskMessage::Completed, +                ) +                .width(Length::Fill); + +                row() +                    .spacing(20) +                    .align_items(Alignment::Center) +                    .push(checkbox) +                    .push( +                        button(edit_icon()) +                            .on_press(TaskMessage::Edit) +                            .padding(10) +                            .style(style::Button::Icon), +                    ) +                    .into() +            } +            TaskState::Editing => { +                let text_input = text_input( +                    "Describe your task...", +                    &self.description, +                    TaskMessage::DescriptionEdited, +                ) +                .on_submit(TaskMessage::FinishEdition) +                .padding(10); + +                row() +                    .spacing(20) +                    .align_items(Alignment::Center) +                    .push(text_input) +                    .push( +                        button( +                            row() +                                .spacing(10) +                                .push(delete_icon()) +                                .push("Delete"), +                        ) +                        .on_press(TaskMessage::Delete) +                        .padding(10) +                        .style(style::Button::Destructive), +                    ) +                    .into() +            } +        } +    } +} + +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { +    let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + +    let filter_button = |label, filter, current_filter| { +        let label = text(label).size(16); + +        let button = button(label).style(if filter == current_filter { +            style::Button::FilterSelected +        } else { +            style::Button::FilterActive +        }); + +        button.on_press(Message::FilterChanged(filter)).padding(8) +    }; + +    row() +        .spacing(20) +        .align_items(Alignment::Center) +        .push( +            text(format!( +                "{} {} left", +                tasks_left, +                if tasks_left == 1 { "task" } else { "tasks" } +            )) +            .width(Length::Fill) +            .size(16), +        ) +        .push( +            row() +                .width(Length::Shrink) +                .spacing(10) +                .push(filter_button("All", Filter::All, current_filter)) +                .push(filter_button("Active", Filter::Active, current_filter)) +                .push(filter_button( +                    "Completed", +                    Filter::Completed, +                    current_filter, +                )), +        ) +        .into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { +    All, +    Active, +    Completed, +} + +impl Default for Filter { +    fn default() -> Self { +        Filter::All +    } +} + +impl Filter { +    fn matches(&self, task: &Task) -> bool { +        match self { +            Filter::All => true, +            Filter::Active => !task.completed, +            Filter::Completed => task.completed, +        } +    } +} + +fn loading_message<'a>() -> Element<'a, Message> { +    container( +        text("Loading...") +            .horizontal_alignment(alignment::Horizontal::Center) +            .size(50), +    ) +    .width(Length::Fill) +    .height(Length::Fill) +    .center_y() +    .into() +} + +fn empty_message(message: &str) -> Element<'_, Message> { +    container( +        text(message) +            .width(Length::Fill) +            .size(25) +            .horizontal_alignment(alignment::Horizontal::Center) +            .color([0.7, 0.7, 0.7]), +    ) +    .width(Length::Fill) +    .height(Length::Units(200)) +    .center_y() +    .into() +} + +// Fonts +const ICONS: Font = Font::External { +    name: "Icons", +    bytes: include_bytes!("../../../todos/fonts/icons.ttf"), +}; + +fn icon(unicode: char) -> Text { +    Text::new(unicode.to_string()) +        .font(ICONS) +        .width(Length::Units(20)) +        .horizontal_alignment(alignment::Horizontal::Center) +        .size(20) +} + +fn edit_icon() -> Text { +    icon('\u{F303}') +} + +fn delete_icon() -> Text { +    icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { +    input_value: String, +    filter: Filter, +    tasks: Vec<Task>, +} + +#[derive(Debug, Clone)] +enum LoadError { +    FileError, +    FormatError, +} + +#[derive(Debug, Clone)] +enum SaveError { +    FileError, +    WriteError, +    FormatError, +} + +#[cfg(not(target_arch = "wasm32"))] +impl SavedState { +    fn path() -> std::path::PathBuf { +        let mut path = if let Some(project_dirs) = +            directories_next::ProjectDirs::from("rs", "Iced", "Todos") +        { +            project_dirs.data_dir().into() +        } else { +            std::env::current_dir().unwrap_or(std::path::PathBuf::new()) +        }; + +        path.push("todos.json"); + +        path +    } + +    async fn load() -> Result<SavedState, LoadError> { +        use async_std::prelude::*; + +        let mut contents = String::new(); + +        let mut file = async_std::fs::File::open(Self::path()) +            .await +            .map_err(|_| LoadError::FileError)?; + +        file.read_to_string(&mut contents) +            .await +            .map_err(|_| LoadError::FileError)?; + +        serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) +    } + +    async fn save(self) -> Result<(), SaveError> { +        use async_std::prelude::*; + +        let json = serde_json::to_string_pretty(&self) +            .map_err(|_| SaveError::FormatError)?; + +        let path = Self::path(); + +        if let Some(dir) = path.parent() { +            async_std::fs::create_dir_all(dir) +                .await +                .map_err(|_| SaveError::FileError)?; +        } + +        { +            let mut file = async_std::fs::File::create(path) +                .await +                .map_err(|_| SaveError::FileError)?; + +            file.write_all(json.as_bytes()) +                .await +                .map_err(|_| SaveError::WriteError)?; +        } + +        // This is a simple way to save at most once every couple seconds +        async_std::task::sleep(std::time::Duration::from_secs(2)).await; + +        Ok(()) +    } +} + +#[cfg(target_arch = "wasm32")] +impl SavedState { +    fn storage() -> Option<web_sys::Storage> { +        let window = web_sys::window()?; + +        window.local_storage().ok()? +    } + +    async fn load() -> Result<SavedState, LoadError> { +        let storage = Self::storage().ok_or(LoadError::FileError)?; + +        let contents = storage +            .get_item("state") +            .map_err(|_| LoadError::FileError)? +            .ok_or(LoadError::FileError)?; + +        serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) +    } + +    async fn save(self) -> Result<(), SaveError> { +        let storage = Self::storage().ok_or(SaveError::FileError)?; + +        let json = serde_json::to_string_pretty(&self) +            .map_err(|_| SaveError::FormatError)?; + +        storage +            .set_item("state", &json) +            .map_err(|_| SaveError::WriteError)?; + +        let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + +        Ok(()) +    } +} + +mod style { +    use iced::{button, Background, Color, Vector}; + +    pub enum Button { +        FilterActive, +        FilterSelected, +        Icon, +        Destructive, +    } + +    impl button::StyleSheet for Button { +        fn active(&self) -> button::Style { +            match self { +                Button::FilterActive => button::Style::default(), +                Button::FilterSelected => button::Style { +                    background: Some(Background::Color(Color::from_rgb( +                        0.2, 0.2, 0.7, +                    ))), +                    border_radius: 10.0, +                    text_color: Color::WHITE, +                    ..button::Style::default() +                }, +                Button::Icon => button::Style { +                    text_color: Color::from_rgb(0.5, 0.5, 0.5), +                    ..button::Style::default() +                }, +                Button::Destructive => button::Style { +                    background: Some(Background::Color(Color::from_rgb( +                        0.8, 0.2, 0.2, +                    ))), +                    border_radius: 5.0, +                    text_color: Color::WHITE, +                    shadow_offset: Vector::new(1.0, 1.0), +                    ..button::Style::default() +                }, +            } +        } + +        fn hovered(&self) -> button::Style { +            let active = self.active(); + +            button::Style { +                text_color: match self { +                    Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), +                    Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7), +                    _ => active.text_color, +                }, +                shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), +                ..active +            } +        } +    } +} diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml new file mode 100644 index 00000000..8ce5f198 --- /dev/null +++ b/examples/pure/tour/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pure_tour" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["image", "debug", "pure"] } +env_logger = "0.8" diff --git a/examples/pure/tour/README.md b/examples/pure/tour/README.md new file mode 100644 index 00000000..e7cd2d5c --- /dev/null +++ b/examples/pure/tour/README.md @@ -0,0 +1,28 @@ +## Tour + +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. + +The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. + +<div align="center"> +  <a href="https://gfycat.com/politeadorableiberianmole"> +    <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> +  </a> +</div> + +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: https://github.com/iced-rs/iced_web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! + +[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage diff --git a/examples/pure/tour/index.html b/examples/pure/tour/index.html new file mode 100644 index 00000000..c64af912 --- /dev/null +++ b/examples/pure/tour/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +    <meta charset="utf-8" content="text/html; charset=utf-8" /> +    <meta name="viewport" content="width=device-width, initial-scale=1" /> +    <title>Tour - Iced</title> +    <base data-trunk-public-url /> +</head> +<body> +<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="tour" /> +</body> +</html> diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs new file mode 100644 index 00000000..7a50bcdc --- /dev/null +++ b/examples/pure/tour/src/main.rs @@ -0,0 +1,702 @@ +use iced::alignment; +use iced::pure::widget::{ +    checkbox, column, container, horizontal_space, image, radio, row, +    scrollable, slider, text, text_input, toggler, vertical_space, +}; +use iced::pure::{Button, Column, Container, Element, Sandbox, Slider}; +use iced::{Color, Length, Settings}; + +pub fn main() -> iced::Result { +    env_logger::init(); + +    Tour::run(Settings::default()) +} + +pub struct Tour { +    steps: Steps, +    debug: bool, +} + +impl Sandbox for Tour { +    type Message = Message; + +    fn new() -> Tour { +        Tour { +            steps: Steps::new(), +            debug: false, +        } +    } + +    fn title(&self) -> String { +        format!("{} - Iced", self.steps.title()) +    } + +    fn update(&mut self, event: Message) { +        match event { +            Message::BackPressed => { +                self.steps.go_back(); +            } +            Message::NextPressed => { +                self.steps.advance(); +            } +            Message::StepMessage(step_msg) => { +                self.steps.update(step_msg, &mut self.debug); +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        let Tour { steps, .. } = self; + +        let mut controls = row(); + +        if steps.has_previous() { +            controls = controls.push( +                button("Back") +                    .on_press(Message::BackPressed) +                    .style(style::Button::Secondary), +            ); +        } + +        controls = controls.push(horizontal_space(Length::Fill)); + +        if steps.can_continue() { +            controls = controls.push( +                button("Next") +                    .on_press(Message::NextPressed) +                    .style(style::Button::Primary), +            ); +        } + +        let content: Element<_> = column() +            .max_width(540) +            .spacing(20) +            .padding(20) +            .push(steps.view(self.debug).map(Message::StepMessage)) +            .push(controls) +            .into(); + +        let content = if self.debug { +            // TODO +            //content.explain(Color::BLACK) +            content +        } else { +            content +        }; + +        let scrollable = +            scrollable(container(content).width(Length::Fill).center_x()); + +        container(scrollable).height(Length::Fill).center_y().into() +    } +} + +#[derive(Debug, Clone)] +pub enum Message { +    BackPressed, +    NextPressed, +    StepMessage(StepMessage), +} + +struct Steps { +    steps: Vec<Step>, +    current: usize, +} + +impl Steps { +    fn new() -> Steps { +        Steps { +            steps: vec![ +                Step::Welcome, +                Step::Slider { value: 50 }, +                Step::RowsAndColumns { +                    layout: Layout::Row, +                    spacing: 20, +                }, +                Step::Text { +                    size: 30, +                    color: Color::BLACK, +                }, +                Step::Radio { selection: None }, +                Step::Toggler { +                    can_continue: false, +                }, +                Step::Image { width: 300 }, +                Step::Scrollable, +                Step::TextInput { +                    value: String::new(), +                    is_secure: false, +                }, +                Step::Debugger, +                Step::End, +            ], +            current: 0, +        } +    } + +    fn update(&mut self, msg: StepMessage, debug: &mut bool) { +        self.steps[self.current].update(msg, debug); +    } + +    fn view(&self, debug: bool) -> Element<StepMessage> { +        self.steps[self.current].view(debug) +    } + +    fn advance(&mut self) { +        if self.can_continue() { +            self.current += 1; +        } +    } + +    fn go_back(&mut self) { +        if self.has_previous() { +            self.current -= 1; +        } +    } + +    fn has_previous(&self) -> bool { +        self.current > 0 +    } + +    fn can_continue(&self) -> bool { +        self.current + 1 < self.steps.len() +            && self.steps[self.current].can_continue() +    } + +    fn title(&self) -> &str { +        self.steps[self.current].title() +    } +} + +enum Step { +    Welcome, +    Slider { value: u8 }, +    RowsAndColumns { layout: Layout, spacing: u16 }, +    Text { size: u16, color: Color }, +    Radio { selection: Option<Language> }, +    Toggler { can_continue: bool }, +    Image { width: u16 }, +    Scrollable, +    TextInput { value: String, is_secure: bool }, +    Debugger, +    End, +} + +#[derive(Debug, Clone)] +pub enum StepMessage { +    SliderChanged(u8), +    LayoutChanged(Layout), +    SpacingChanged(u16), +    TextSizeChanged(u16), +    TextColorChanged(Color), +    LanguageSelected(Language), +    ImageWidthChanged(u16), +    InputChanged(String), +    ToggleSecureInput(bool), +    DebugToggled(bool), +    TogglerChanged(bool), +} + +impl<'a> Step { +    fn update(&mut self, msg: StepMessage, debug: &mut bool) { +        match msg { +            StepMessage::DebugToggled(value) => { +                if let Step::Debugger = self { +                    *debug = value; +                } +            } +            StepMessage::LanguageSelected(language) => { +                if let Step::Radio { selection } = self { +                    *selection = Some(language); +                } +            } +            StepMessage::SliderChanged(new_value) => { +                if let Step::Slider { value, .. } = self { +                    *value = new_value; +                } +            } +            StepMessage::TextSizeChanged(new_size) => { +                if let Step::Text { size, .. } = self { +                    *size = new_size; +                } +            } +            StepMessage::TextColorChanged(new_color) => { +                if let Step::Text { color, .. } = self { +                    *color = new_color; +                } +            } +            StepMessage::LayoutChanged(new_layout) => { +                if let Step::RowsAndColumns { layout, .. } = self { +                    *layout = new_layout; +                } +            } +            StepMessage::SpacingChanged(new_spacing) => { +                if let Step::RowsAndColumns { spacing, .. } = self { +                    *spacing = new_spacing; +                } +            } +            StepMessage::ImageWidthChanged(new_width) => { +                if let Step::Image { width, .. } = self { +                    *width = new_width; +                } +            } +            StepMessage::InputChanged(new_value) => { +                if let Step::TextInput { value, .. } = self { +                    *value = new_value; +                } +            } +            StepMessage::ToggleSecureInput(toggle) => { +                if let Step::TextInput { is_secure, .. } = self { +                    *is_secure = toggle; +                } +            } +            StepMessage::TogglerChanged(value) => { +                if let Step::Toggler { can_continue, .. } = self { +                    *can_continue = value; +                } +            } +        }; +    } + +    fn title(&self) -> &str { +        match self { +            Step::Welcome => "Welcome", +            Step::Radio { .. } => "Radio button", +            Step::Toggler { .. } => "Toggler", +            Step::Slider { .. } => "Slider", +            Step::Text { .. } => "Text", +            Step::Image { .. } => "Image", +            Step::RowsAndColumns { .. } => "Rows and columns", +            Step::Scrollable => "Scrollable", +            Step::TextInput { .. } => "Text input", +            Step::Debugger => "Debugger", +            Step::End => "End", +        } +    } + +    fn can_continue(&self) -> bool { +        match self { +            Step::Welcome => true, +            Step::Radio { selection } => *selection == Some(Language::Rust), +            Step::Toggler { can_continue } => *can_continue, +            Step::Slider { .. } => true, +            Step::Text { .. } => true, +            Step::Image { .. } => true, +            Step::RowsAndColumns { .. } => true, +            Step::Scrollable => true, +            Step::TextInput { value, .. } => !value.is_empty(), +            Step::Debugger => true, +            Step::End => false, +        } +    } + +    fn view(&self, debug: bool) -> Element<StepMessage> { +        match self { +            Step::Welcome => Self::welcome(), +            Step::Radio { selection } => Self::radio(*selection), +            Step::Toggler { can_continue } => Self::toggler(*can_continue), +            Step::Slider { value } => Self::slider(*value), +            Step::Text { size, color } => Self::text(*size, *color), +            Step::Image { width } => Self::image(*width), +            Step::RowsAndColumns { layout, spacing } => { +                Self::rows_and_columns(*layout, *spacing) +            } +            Step::Scrollable => Self::scrollable(), +            Step::TextInput { value, is_secure } => { +                Self::text_input(value, *is_secure) +            } +            Step::Debugger => Self::debugger(debug), +            Step::End => Self::end(), +        } +        .into() +    } + +    fn container(title: &str) -> Column<'a, StepMessage> { +        column().spacing(20).push(text(title).size(50)) +    } + +    fn welcome() -> Column<'a, StepMessage> { +        Self::container("Welcome!") +            .push( +                "This is a simple tour meant to showcase a bunch of widgets \ +                 that can be easily implemented on top of Iced.", +            ) +            .push( +                "Iced is a cross-platform GUI library for Rust focused on \ +                 simplicity and type-safety. It is heavily inspired by Elm.", +            ) +            .push( +                "It was originally born as part of Coffee, an opinionated \ +                 2D game engine for Rust.", +            ) +            .push( +                "On native platforms, Iced provides by default a renderer \ +                 built on top of wgpu, a graphics library supporting Vulkan, \ +                 Metal, DX11, and DX12.", +            ) +            .push( +                "Additionally, this tour can also run on WebAssembly thanks \ +                 to dodrio, an experimental VDOM library for Rust.", +            ) +            .push( +                "You will need to interact with the UI in order to reach the \ +                 end!", +            ) +    } + +    fn slider(value: u8) -> Column<'a, StepMessage> { +        Self::container("Slider") +            .push( +                "A slider allows you to smoothly select a value from a range \ +                 of values.", +            ) +            .push( +                "The following slider lets you choose an integer from \ +                 0 to 100:", +            ) +            .push(slider(0..=100, value, StepMessage::SliderChanged)) +            .push( +                text(value.to_string()) +                    .width(Length::Fill) +                    .horizontal_alignment(alignment::Horizontal::Center), +            ) +    } + +    fn rows_and_columns( +        layout: Layout, +        spacing: u16, +    ) -> Column<'a, StepMessage> { +        let row_radio = +            radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); + +        let column_radio = radio( +            "Column", +            Layout::Column, +            Some(layout), +            StepMessage::LayoutChanged, +        ); + +        let layout_section: Element<_> = match layout { +            Layout::Row => row() +                .spacing(spacing) +                .push(row_radio) +                .push(column_radio) +                .into(), +            Layout::Column => column() +                .spacing(spacing) +                .push(row_radio) +                .push(column_radio) +                .into(), +        }; + +        let spacing_section = column() +            .spacing(10) +            .push(slider(0..=80, spacing, StepMessage::SpacingChanged)) +            .push( +                text(format!("{} px", spacing)) +                    .width(Length::Fill) +                    .horizontal_alignment(alignment::Horizontal::Center), +            ); + +        Self::container("Rows and columns") +            .spacing(spacing) +            .push( +                "Iced uses a layout model based on flexbox to position UI \ +                 elements.", +            ) +            .push( +                "Rows and columns can be used to distribute content \ +                 horizontally or vertically, respectively.", +            ) +            .push(layout_section) +            .push("You can also easily change the spacing between elements:") +            .push(spacing_section) +    } + +    fn text(size: u16, color: Color) -> Column<'a, StepMessage> { +        let size_section = column() +            .padding(20) +            .spacing(20) +            .push("You can change its size:") +            .push(text(format!("This text is {} pixels", size)).size(size)) +            .push(Slider::new(10..=70, size, StepMessage::TextSizeChanged)); + +        let color_sliders = row() +            .spacing(10) +            .push(color_slider(color.r, move |r| Color { r, ..color })) +            .push(color_slider(color.g, move |g| Color { g, ..color })) +            .push(color_slider(color.b, move |b| Color { b, ..color })); + +        let color_section = column() +            .padding(20) +            .spacing(20) +            .push("And its color:") +            .push(text(format!("{:?}", color)).color(color)) +            .push(color_sliders); + +        Self::container("Text") +            .push( +                "Text is probably the most essential widget for your UI. \ +                 It will try to adapt to the dimensions of its container.", +            ) +            .push(size_section) +            .push(color_section) +    } + +    fn radio(selection: Option<Language>) -> Column<'a, StepMessage> { +        let question = column() +            .padding(20) +            .spacing(10) +            .push(text("Iced is written in...").size(24)) +            .push(Language::all().iter().cloned().fold( +                column().padding(10).spacing(20), +                |choices, language| { +                    choices.push(radio( +                        language, +                        language, +                        selection, +                        StepMessage::LanguageSelected, +                    )) +                }, +            )); + +        Self::container("Radio button") +            .push( +                "A radio button is normally used to represent a choice... \ +                 Surprise test!", +            ) +            .push(question) +            .push( +                "Iced works very well with iterators! The list above is \ +                 basically created by folding a column over the different \ +                 choices, creating a radio button for each one of them!", +            ) +    } + +    fn toggler(can_continue: bool) -> Column<'a, StepMessage> { +        Self::container("Toggler") +            .push("A toggler is mostly used to enable or disable something.") +            .push( +                Container::new(toggler( +                    "Toggle me to continue...".to_owned(), +                    can_continue, +                    StepMessage::TogglerChanged, +                )) +                .padding([0, 40]), +            ) +    } + +    fn image(width: u16) -> Column<'a, StepMessage> { +        Self::container("Image") +            .push("An image that tries to keep its aspect ratio.") +            .push(ferris(width)) +            .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) +            .push( +                text(format!("Width: {} px", width.to_string())) +                    .width(Length::Fill) +                    .horizontal_alignment(alignment::Horizontal::Center), +            ) +    } + +    fn scrollable() -> Column<'a, StepMessage> { +        Self::container("Scrollable") +            .push( +                "Iced supports scrollable content. Try it out! Find the \ +                 button further below.", +            ) +            .push( +                text("Tip: You can use the scrollbar to scroll down faster!") +                    .size(16), +            ) +            .push(vertical_space(Length::Units(4096))) +            .push( +                text("You are halfway there!") +                    .width(Length::Fill) +                    .size(30) +                    .horizontal_alignment(alignment::Horizontal::Center), +            ) +            .push(vertical_space(Length::Units(4096))) +            .push(ferris(300)) +            .push( +                text("You made it!") +                    .width(Length::Fill) +                    .size(50) +                    .horizontal_alignment(alignment::Horizontal::Center), +            ) +    } + +    fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { +        let text_input = text_input( +            "Type something to continue...", +            value, +            StepMessage::InputChanged, +        ) +        .padding(10) +        .size(30); + +        Self::container("Text input") +            .push("Use a text input to ask for different kinds of information.") +            .push(if is_secure { +                text_input.password() +            } else { +                text_input +            }) +            .push(checkbox( +                "Enable password mode", +                is_secure, +                StepMessage::ToggleSecureInput, +            )) +            .push( +                "A text input produces a message every time it changes. It is \ +                 very easy to keep track of its contents:", +            ) +            .push( +                text(if value.is_empty() { +                    "You have not typed anything yet..." +                } else { +                    value +                }) +                .width(Length::Fill) +                .horizontal_alignment(alignment::Horizontal::Center), +            ) +    } + +    fn debugger(debug: bool) -> Column<'a, StepMessage> { +        Self::container("Debugger") +            .push( +                "You can ask Iced to visually explain the layouting of the \ +                 different elements comprising your UI!", +            ) +            .push( +                "Give it a shot! Check the following checkbox to be able to \ +                 see element boundaries.", +            ) +            .push(if cfg!(target_arch = "wasm32") { +                Element::new( +                    text("Not available on web yet!") +                        .color([0.7, 0.7, 0.7]) +                        .horizontal_alignment(alignment::Horizontal::Center), +                ) +            } else { +                checkbox("Explain layout", debug, StepMessage::DebugToggled) +                    .into() +            }) +            .push("Feel free to go back and take a look.") +    } + +    fn end() -> Column<'a, StepMessage> { +        Self::container("You reached the end!") +            .push("This tour will be updated as more features are added.") +            .push("Make sure to keep an eye on it!") +    } +} + +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { +    container( +        // This should go away once we unify resource loading on native +        // platforms +        if cfg!(target_arch = "wasm32") { +            image("tour/images/ferris.png") +        } else { +            image(format!( +                "{}/../../tour/images/ferris.png", +                env!("CARGO_MANIFEST_DIR") +            )) +        } +        .width(Length::Units(width)), +    ) +    .width(Length::Fill) +    .center_x() +} + +fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { +    iced::pure::button( +        text(label).horizontal_alignment(alignment::Horizontal::Center), +    ) +    .padding(12) +    .width(Length::Units(100)) +} + +fn color_slider<'a>( +    component: f32, +    update: impl Fn(f32) -> Color + 'a, +) -> Slider<'a, f64, StepMessage> { +    slider(0.0..=1.0, f64::from(component), move |c| { +        StepMessage::TextColorChanged(update(c as f32)) +    }) +    .step(0.01) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { +    Rust, +    Elm, +    Ruby, +    Haskell, +    C, +    Other, +} + +impl Language { +    fn all() -> [Language; 6] { +        [ +            Language::C, +            Language::Elm, +            Language::Ruby, +            Language::Haskell, +            Language::Rust, +            Language::Other, +        ] +    } +} + +impl From<Language> for String { +    fn from(language: Language) -> String { +        String::from(match language { +            Language::Rust => "Rust", +            Language::Elm => "Elm", +            Language::Ruby => "Ruby", +            Language::Haskell => "Haskell", +            Language::C => "C", +            Language::Other => "Other", +        }) +    } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layout { +    Row, +    Column, +} + +mod style { +    use iced::{button, Background, Color, Vector}; + +    pub enum Button { +        Primary, +        Secondary, +    } + +    impl button::StyleSheet for Button { +        fn active(&self) -> button::Style { +            button::Style { +                background: Some(Background::Color(match self { +                    Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), +                    Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), +                })), +                border_radius: 12.0, +                shadow_offset: Vector::new(1.0, 1.0), +                text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), +                ..button::Style::default() +            } +        } + +        fn hovered(&self) -> button::Style { +            button::Style { +                text_color: Color::WHITE, +                shadow_offset: Vector::new(1.0, 2.0), +                ..self.active() +            } +        } +    } +} diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index dc8a4de7..377d7a2d 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -105,8 +105,8 @@ impl Application for Stopwatch {                  Text::new(label)                      .horizontal_alignment(alignment::Horizontal::Center),              ) -            .min_width(80)              .padding(10) +            .width(Length::Units(80))              .style(style)          }; diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index e199c88c..2024d25a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -763,7 +763,7 @@ fn button<'a, Message: Clone>(          Text::new(label).horizontal_alignment(alignment::Horizontal::Center),      )      .padding(12) -    .min_width(100) +    .width(Length::Units(100))  }  fn color_slider( diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index b840de50..2d7451f3 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -3,9 +3,17 @@ name = "iced_lazy"  version = "0.1.0"  edition = "2021" +[features] +pure = ["iced_pure"] +  [dependencies]  ouroboros = "0.13"  [dependencies.iced_native]  version = "0.4"  path = "../native" + +[dependencies.iced_pure] +version = "0.1" +path = "../pure" +optional = true diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 05fce765..5d7d10e4 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -1,6 +1,9 @@  pub mod component;  pub mod responsive; +#[cfg(feature = "pure")] +pub mod pure; +  pub use component::Component;  pub use responsive::Responsive; diff --git a/lazy/src/pure.rs b/lazy/src/pure.rs new file mode 100644 index 00000000..9cea807e --- /dev/null +++ b/lazy/src/pure.rs @@ -0,0 +1 @@ +pub mod component; diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs new file mode 100644 index 00000000..c79e34f8 --- /dev/null +++ b/lazy/src/pure/component.rs @@ -0,0 +1,481 @@ +//! Build and reuse custom widgets using The Elm Architecture. +use iced_native::event; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size}; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use ouroboros::self_referencing; +use std::cell::{Ref, RefCell}; +use std::marker::PhantomData; + +/// A reusable, custom widget that uses The Elm Architecture. +/// +/// A [`Component`] allows you to implement custom widgets as if they were +/// `iced` applications with encapsulated state. +/// +/// In other words, a [`Component`] allows you to turn `iced` applications into +/// custom widgets and embed them without cumbersome wiring. +/// +/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) +/// and update the internal state of the [`Component`]. +/// +/// Additionally, a [`Component`] is capable of producing a `Message` to notify +/// the parent application of any relevant interactions. +pub trait Component<Message, Renderer> { +    /// The internal state of this [`Component`]. +    type State: Default; + +    /// The type of event this [`Component`] handles internally. +    type Event; + +    /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. +    /// +    /// It can produce a `Message` for the parent application. +    fn update( +        &mut self, +        state: &mut Self::State, +        event: Self::Event, +    ) -> Option<Message>; + +    /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) +    /// on user interaction. +    fn view(&self, state: &Self::State) -> Element<Self::Event, Renderer>; +} + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn view<'a, C, Message, Renderer>( +    component: C, +) -> Element<'a, Message, Renderer> +where +    C: Component<Message, Renderer> + 'a, +    C::State: 'static, +    Message: 'a, +    Renderer: iced_native::Renderer + 'a, +{ +    Element::new(Instance { +        state: RefCell::new(Some( +            StateBuilder { +                component: Box::new(component), +                message: PhantomData, +                state: PhantomData, +                element_builder: |_| None, +            } +            .build(), +        )), +    }) +} + +struct Instance<'a, Message, Renderer, Event, S> { +    state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, +} + +#[self_referencing] +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { +    component: +        Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>, +    message: PhantomData<Message>, +    state: PhantomData<S>, + +    #[borrows(component)] +    #[covariant] +    element: Option<Element<'this, Event, Renderer>>, +} + +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where +    S: Default, +{ +    fn with_element<T>( +        &self, +        f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, +    ) -> T { +        self.with_element_mut(|element| f(element)) +    } + +    fn with_element_mut<T>( +        &self, +        f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, +    ) -> T { +        if self +            .state +            .borrow() +            .as_ref() +            .unwrap() +            .borrow_element() +            .is_none() +        { +            let heads = self.state.borrow_mut().take().unwrap().into_heads(); + +            *self.state.borrow_mut() = Some( +                StateBuilder { +                    component: heads.component, +                    message: PhantomData, +                    state: PhantomData, +                    element_builder: |state| Some(state.view(&S::default())), +                } +                .build(), +            ); +        } + +        self.state +            .borrow_mut() +            .as_mut() +            .unwrap() +            .with_element_mut(|element| f(element.as_mut().unwrap())) +    } +} + +impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer> +    for Instance<'a, Message, Renderer, Event, S> +where +    S: 'static + Default, +    Renderer: iced_native::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<S>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(S::default()) +    } + +    fn children(&self) -> Vec<Tree> { +        self.with_element(|element| vec![Tree::new(element)]) +    } + +    fn diff(&self, tree: &mut Tree) { +        self.with_element(|element| { +            tree.diff_children(std::slice::from_ref(&element)) +        }) +    } + +    fn width(&self) -> Length { +        self.with_element(|element| element.as_widget().width()) +    } + +    fn height(&self) -> Length { +        self.with_element(|element| element.as_widget().height()) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        self.with_element(|element| { +            element.as_widget().layout(renderer, limits) +        }) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: iced_native::Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        let mut local_messages = Vec::new(); +        let mut local_shell = Shell::new(&mut local_messages); + +        let event_status = self.with_element_mut(|element| { +            element.as_widget_mut().on_event( +                &mut tree.children[0], +                event, +                layout, +                cursor_position, +                renderer, +                clipboard, +                &mut local_shell, +            ) +        }); + +        local_shell.revalidate_layout(|| shell.invalidate_layout()); + +        if !local_messages.is_empty() { +            let mut heads = self.state.take().unwrap().into_heads(); + +            for message in local_messages.into_iter().filter_map(|message| { +                heads +                    .component +                    .update(tree.state.downcast_mut::<S>(), message) +            }) { +                shell.publish(message); +            } + +            self.state = RefCell::new(Some( +                StateBuilder { +                    component: heads.component, +                    message: PhantomData, +                    state: PhantomData, +                    element_builder: |state| { +                        Some(state.view(tree.state.downcast_ref::<S>())) +                    }, +                } +                .build(), +            )); + +            shell.invalidate_layout(); +        } + +        event_status +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        self.with_element(|element| { +            element.as_widget().draw( +                &tree.children[0], +                renderer, +                style, +                layout, +                cursor_position, +                viewport, +            ); +        }); +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.with_element(|element| { +            element.as_widget().mouse_interaction( +                &tree.children[0], +                layout, +                cursor_position, +                viewport, +                renderer, +            ) +        }) +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        let overlay = OverlayBuilder { +            instance: self, +            instance_ref_builder: |instance| instance.state.borrow(), +            tree, +            types: PhantomData, +            overlay_builder: |instance, tree| { +                instance +                    .as_ref() +                    .unwrap() +                    .borrow_element() +                    .as_ref() +                    .unwrap() +                    .as_widget() +                    .overlay(&mut tree.children[0], layout, renderer) +            }, +        } +        .build(); + +        let has_overlay = overlay.with_overlay(|overlay| { +            overlay.as_ref().map(overlay::Element::position) +        }); + +        has_overlay.map(|position| { +            overlay::Element::new( +                position, +                Box::new(OverlayInstance { +                    overlay: Some(overlay), +                }), +            ) +        }) +    } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Event, S> { +    instance: &'a Instance<'b, Message, Renderer, Event, S>, +    tree: &'a mut Tree, +    types: PhantomData<(Message, Event, S)>, + +    #[borrows(instance)] +    #[covariant] +    instance_ref: Ref<'this, Option<State<'a, Message, Renderer, Event, S>>>, + +    #[borrows(instance_ref, mut tree)] +    #[covariant] +    overlay: Option<overlay::Element<'this, Event, Renderer>>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { +    overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>, +} + +impl<'a, 'b, Message, Renderer, Event, S> +    OverlayInstance<'a, 'b, Message, Renderer, Event, S> +{ +    fn with_overlay_maybe<T>( +        &self, +        f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, +    ) -> Option<T> { +        self.overlay +            .as_ref() +            .unwrap() +            .borrow_overlay() +            .as_ref() +            .map(f) +    } + +    fn with_overlay_mut_maybe<T>( +        &mut self, +        f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, +    ) -> Option<T> { +        self.overlay +            .as_mut() +            .unwrap() +            .with_overlay_mut(|overlay| overlay.as_mut().map(f)) +    } +} + +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer> +    for OverlayInstance<'a, 'b, Message, Renderer, Event, S> +where +    Renderer: iced_native::Renderer, +    S: 'static + Default, +{ +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node { +        self.with_overlay_maybe(|overlay| { +            let vector = position - overlay.position(); + +            overlay.layout(renderer, bounds).translate(vector) +        }) +        .unwrap_or_default() +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +    ) { +        self.with_overlay_maybe(|overlay| { +            overlay.draw(renderer, style, layout, cursor_position); +        }); +    } + +    fn mouse_interaction( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.with_overlay_maybe(|overlay| { +            overlay.mouse_interaction( +                layout, +                cursor_position, +                viewport, +                renderer, +            ) +        }) +        .unwrap_or_default() +    } + +    fn on_event( +        &mut self, +        event: iced_native::Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> iced_native::event::Status { +        let mut local_messages = Vec::new(); +        let mut local_shell = Shell::new(&mut local_messages); + +        let event_status = self +            .with_overlay_mut_maybe(|overlay| { +                overlay.on_event( +                    event, +                    layout, +                    cursor_position, +                    renderer, +                    clipboard, +                    &mut local_shell, +                ) +            }) +            .unwrap_or_else(|| iced_native::event::Status::Ignored); + +        local_shell.revalidate_layout(|| shell.invalidate_layout()); + +        if !local_messages.is_empty() { +            let overlay = self.overlay.take().unwrap().into_heads(); +            let mut heads = overlay.instance.state.take().unwrap().into_heads(); + +            for message in local_messages.into_iter().filter_map(|message| { +                heads +                    .component +                    .update(overlay.tree.state.downcast_mut::<S>(), message) +            }) { +                shell.publish(message); +            } + +            *overlay.instance.state.borrow_mut() = Some( +                StateBuilder { +                    component: heads.component, +                    message: PhantomData, +                    state: PhantomData, +                    element_builder: |state| { +                        Some(state.view(overlay.tree.state.downcast_ref::<S>())) +                    }, +                } +                .build(), +            ); + +            self.overlay = Some( +                OverlayBuilder { +                    instance: overlay.instance, +                    instance_ref_builder: |instance| instance.state.borrow(), +                    tree: overlay.tree, +                    types: PhantomData, +                    overlay_builder: |instance, tree| { +                        instance +                            .as_ref() +                            .unwrap() +                            .borrow_element() +                            .as_ref() +                            .unwrap() +                            .as_widget() +                            .overlay(tree, layout, renderer) +                    }, +                } +                .build(), +            ); + +            shell.invalidate_layout(); +        } + +        event_status +    } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 57fdd7d4..b03d9f27 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -61,8 +61,6 @@ pub struct Button<'a, Message, Renderer> {      on_press: Option<Message>,      width: Length,      height: Length, -    min_width: u32, -    min_height: u32,      padding: Padding,      style_sheet: Box<dyn StyleSheet + 'a>,  } @@ -84,8 +82,6 @@ where              on_press: None,              width: Length::Shrink,              height: Length::Shrink, -            min_width: 0, -            min_height: 0,              padding: Padding::new(5),              style_sheet: Default::default(),          } @@ -103,18 +99,6 @@ where          self      } -    /// Sets the minimum width of the [`Button`]. -    pub fn min_width(mut self, min_width: u32) -> Self { -        self.min_width = min_width; -        self -    } - -    /// Sets the minimum height of the [`Button`]. -    pub fn min_height(mut self, min_height: u32) -> Self { -        self.min_height = min_height; -        self -    } -      /// Sets the [`Padding`] of the [`Button`].      pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {          self.padding = padding.into(); @@ -151,6 +135,153 @@ impl State {      }  } +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +pub fn update<'a, Message: Clone>( +    event: Event, +    layout: Layout<'_>, +    cursor_position: Point, +    shell: &mut Shell<'_, Message>, +    on_press: &Option<Message>, +    state: impl FnOnce() -> &'a mut State, +) -> event::Status { +    match event { +        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerPressed { .. }) => { +            if on_press.is_some() { +                let bounds = layout.bounds(); + +                if bounds.contains(cursor_position) { +                    let state = state(); + +                    state.is_pressed = true; + +                    return event::Status::Captured; +                } +            } +        } +        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerLifted { .. }) => { +            if let Some(on_press) = on_press.clone() { +                let state = state(); + +                if state.is_pressed { +                    state.is_pressed = false; + +                    let bounds = layout.bounds(); + +                    if bounds.contains(cursor_position) { +                        shell.publish(on_press); +                    } + +                    return event::Status::Captured; +                } +            } +        } +        Event::Touch(touch::Event::FingerLost { .. }) => { +            let state = state(); + +            state.is_pressed = false; +        } +        _ => {} +    } + +    event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: crate::Renderer>( +    renderer: &mut Renderer, +    bounds: Rectangle, +    cursor_position: Point, +    is_enabled: bool, +    style_sheet: &dyn StyleSheet, +    state: impl FnOnce() -> &'a State, +) -> Style { +    let is_mouse_over = bounds.contains(cursor_position); + +    let styling = if !is_enabled { +        style_sheet.disabled() +    } else if is_mouse_over { +        let state = state(); + +        if state.is_pressed { +            style_sheet.pressed() +        } else { +            style_sheet.hovered() +        } +    } else { +        style_sheet.active() +    }; + +    if styling.background.is_some() || styling.border_width > 0.0 { +        if styling.shadow_offset != Vector::default() { +            // TODO: Implement proper shadow support +            renderer.fill_quad( +                renderer::Quad { +                    bounds: Rectangle { +                        x: bounds.x + styling.shadow_offset.x, +                        y: bounds.y + styling.shadow_offset.y, +                        ..bounds +                    }, +                    border_radius: styling.border_radius, +                    border_width: 0.0, +                    border_color: Color::TRANSPARENT, +                }, +                Background::Color([0.0, 0.0, 0.0, 0.5].into()), +            ); +        } + +        renderer.fill_quad( +            renderer::Quad { +                bounds, +                border_radius: styling.border_radius, +                border_width: styling.border_width, +                border_color: styling.border_color, +            }, +            styling +                .background +                .unwrap_or(Background::Color(Color::TRANSPARENT)), +        ); +    } + +    styling +} + +/// Computes the layout of a [`Button`]. +pub fn layout<Renderer>( +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    height: Length, +    padding: Padding, +    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { +    let limits = limits.width(width).height(height).pad(padding); + +    let mut content = layout_content(renderer, &limits); +    content.move_to(Point::new(padding.left.into(), padding.top.into())); + +    let size = limits.resolve(content.size()).pad(padding); + +    layout::Node::with_children(size, vec![content]) +} + +/// Returns the [`mouse::Interaction`] of a [`Button`]. +pub fn mouse_interaction( +    layout: Layout<'_>, +    cursor_position: Point, +    is_enabled: bool, +) -> mouse::Interaction { +    let is_mouse_over = layout.bounds().contains(cursor_position); + +    if is_mouse_over && is_enabled { +        mouse::Interaction::Pointer +    } else { +        mouse::Interaction::default() +    } +} +  impl<'a, Message, Renderer> Widget<Message, Renderer>      for Button<'a, Message, Renderer>  where @@ -170,22 +301,14 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let limits = limits -            .min_width(self.min_width) -            .min_height(self.min_height) -            .width(self.width) -            .height(self.height) -            .pad(self.padding); - -        let mut content = self.content.layout(renderer, &limits); -        content.move_to(Point::new( -            self.padding.left.into(), -            self.padding.top.into(), -        )); - -        let size = limits.resolve(content.size()).pad(self.padding); - -        layout::Node::with_children(size, vec![content]) +        layout( +            renderer, +            limits, +            self.width, +            self.height, +            self.padding, +            |renderer, limits| self.content.layout(renderer, limits), +        )      }      fn on_event( @@ -208,42 +331,14 @@ where              return event::Status::Captured;          } -        match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerPressed { .. }) => { -                if self.on_press.is_some() { -                    let bounds = layout.bounds(); - -                    if bounds.contains(cursor_position) { -                        self.state.is_pressed = true; - -                        return event::Status::Captured; -                    } -                } -            } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerLifted { .. }) => { -                if let Some(on_press) = self.on_press.clone() { -                    let bounds = layout.bounds(); - -                    if self.state.is_pressed { -                        self.state.is_pressed = false; - -                        if bounds.contains(cursor_position) { -                            shell.publish(on_press); -                        } - -                        return event::Status::Captured; -                    } -                } -            } -            Event::Touch(touch::Event::FingerLost { .. }) => { -                self.state.is_pressed = false; -            } -            _ => {} -        } - -        event::Status::Ignored +        update( +            event, +            layout, +            cursor_position, +            shell, +            &self.on_press, +            || &mut self.state, +        )      }      fn mouse_interaction( @@ -253,14 +348,7 @@ where          _viewport: &Rectangle,          _renderer: &Renderer,      ) -> mouse::Interaction { -        let is_mouse_over = layout.bounds().contains(cursor_position); -        let is_disabled = self.on_press.is_none(); - -        if is_mouse_over && !is_disabled { -            mouse::Interaction::Pointer -        } else { -            mouse::Interaction::default() -        } +        mouse_interaction(layout, cursor_position, self.on_press.is_some())      }      fn draw( @@ -274,51 +362,14 @@ where          let bounds = layout.bounds();          let content_layout = layout.children().next().unwrap(); -        let is_mouse_over = bounds.contains(cursor_position); -        let is_disabled = self.on_press.is_none(); - -        let styling = if is_disabled { -            self.style_sheet.disabled() -        } else if is_mouse_over { -            if self.state.is_pressed { -                self.style_sheet.pressed() -            } else { -                self.style_sheet.hovered() -            } -        } else { -            self.style_sheet.active() -        }; - -        if styling.background.is_some() || styling.border_width > 0.0 { -            if styling.shadow_offset != Vector::default() { -                // TODO: Implement proper shadow support -                renderer.fill_quad( -                    renderer::Quad { -                        bounds: Rectangle { -                            x: bounds.x + styling.shadow_offset.x, -                            y: bounds.y + styling.shadow_offset.y, -                            ..bounds -                        }, -                        border_radius: styling.border_radius, -                        border_width: 0.0, -                        border_color: Color::TRANSPARENT, -                    }, -                    Background::Color([0.0, 0.0, 0.0, 0.5].into()), -                ); -            } - -            renderer.fill_quad( -                renderer::Quad { -                    bounds, -                    border_radius: styling.border_radius, -                    border_width: styling.border_width, -                    border_color: styling.border_color, -                }, -                styling -                    .background -                    .unwrap_or(Background::Color(Color::TRANSPARENT)), -            ); -        } +        let styling = draw( +            renderer, +            bounds, +            cursor_position, +            self.on_press.is_some(), +            self.style_sheet.as_ref(), +            || &self.state, +        );          self.content.draw(              renderer, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 15cbf93a..122c5e52 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -34,7 +34,7 @@ pub use iced_style::checkbox::{Style, StyleSheet};  #[allow(missing_debug_implementations)]  pub struct Checkbox<'a, Message, Renderer: text::Renderer> {      is_checked: bool, -    on_toggle: Box<dyn Fn(bool) -> Message>, +    on_toggle: Box<dyn Fn(bool) -> Message + 'a>,      label: String,      width: Length,      size: u16, @@ -61,7 +61,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> {      ///     `Message`.      pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self      where -        F: 'static + Fn(bool) -> Message, +        F: 'a + Fn(bool) -> Message,      {          Checkbox {              is_checked, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index ca85a425..0e7c301e 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -116,6 +116,32 @@ where      }  } +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    height: Length, +    padding: Padding, +    horizontal_alignment: alignment::Horizontal, +    vertical_alignment: alignment::Vertical, +    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { +    let limits = limits.loose().width(width).height(height).pad(padding); + +    let mut content = layout_content(renderer, &limits.loose()); +    let size = limits.resolve(content.size()); + +    content.move_to(Point::new(padding.left.into(), padding.top.into())); +    content.align( +        Alignment::from(horizontal_alignment), +        Alignment::from(vertical_alignment), +        size, +    ); + +    layout::Node::with_children(size.pad(padding), vec![content]) +} +  impl<'a, Message, Renderer> Widget<Message, Renderer>      for Container<'a, Message, Renderer>  where @@ -134,28 +160,16 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let limits = limits -            .loose() -            .max_width(self.max_width) -            .max_height(self.max_height) -            .width(self.width) -            .height(self.height) -            .pad(self.padding); - -        let mut content = self.content.layout(renderer, &limits.loose()); -        let size = limits.resolve(content.size()); - -        content.move_to(Point::new( -            self.padding.left.into(), -            self.padding.top.into(), -        )); -        content.align( -            Alignment::from(self.horizontal_alignment), -            Alignment::from(self.vertical_alignment), -            size, -        ); - -        layout::Node::with_children(size.pad(self.padding), vec![content]) +        layout( +            renderer, +            limits, +            self.width, +            self.height, +            self.padding, +            self.horizontal_alignment, +            self.vertical_alignment, +            |renderer, limits| self.content.layout(renderer, limits), +        )      }      fn on_event( diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index de0ffbc0..8e7a28e5 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -65,6 +65,46 @@ impl<Handle> Image<Handle> {      }  } +/// Computes the layout of an [`Image`]. +pub fn layout<Renderer, Handle>( +    renderer: &Renderer, +    limits: &layout::Limits, +    handle: &Handle, +    width: Length, +    height: Length, +    content_fit: ContentFit, +) -> layout::Node +where +    Renderer: image::Renderer<Handle = Handle>, +{ +    // The raw w/h of the underlying image +    let image_size = { +        let (width, height) = renderer.dimensions(handle); + +        Size::new(width as f32, height as f32) +    }; + +    // The size to be available to the widget prior to `Shrink`ing +    let raw_size = limits.width(width).height(height).resolve(image_size); + +    // The uncropped size of the image when fit to the bounds above +    let full_size = content_fit.fit(image_size, raw_size); + +    // Shrink the widget to fit the resized image, if requested +    let final_size = Size { +        width: match width { +            Length::Shrink => f32::min(raw_size.width, full_size.width), +            _ => raw_size.width, +        }, +        height: match height { +            Length::Shrink => f32::min(raw_size.height, full_size.height), +            _ => raw_size.height, +        }, +    }; + +    layout::Node::new(final_size) +} +  impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle>  where      Renderer: image::Renderer<Handle = Handle>, @@ -83,32 +123,14 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        // The raw w/h of the underlying image -        let (width, height) = renderer.dimensions(&self.handle); -        let image_size = Size::new(width as f32, height as f32); - -        // The size to be available to the widget prior to `Shrink`ing -        let raw_size = limits -            .width(self.width) -            .height(self.height) -            .resolve(image_size); - -        // The uncropped size of the image when fit to the bounds above -        let full_size = self.content_fit.fit(image_size, raw_size); - -        // Shrink the widget to fit the resized image, if requested -        let final_size = Size { -            width: match self.width { -                Length::Shrink => f32::min(raw_size.width, full_size.width), -                _ => raw_size.width, -            }, -            height: match self.height { -                Length::Shrink => f32::min(raw_size.height, full_size.height), -                _ => raw_size.height, -            }, -        }; - -        layout::Node::new(final_size) +        layout( +            renderer, +            limits, +            &self.handle, +            self.width, +            self.height, +            self.content_fit, +        )      }      fn draw( diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 3be6c20c..e050ada5 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -23,11 +23,7 @@ pub struct PickList<'a, T, Message, Renderer: text::Renderer>  where      [T]: ToOwned<Owned = Vec<T>>,  { -    menu: &'a mut menu::State, -    keyboard_modifiers: &'a mut keyboard::Modifiers, -    is_open: &'a mut bool, -    hovered_option: &'a mut Option<usize>, -    last_selection: &'a mut Option<T>, +    state: &'a mut State<T>,      on_selected: Box<dyn Fn(T) -> Message>,      options: Cow<'a, [T]>,      placeholder: Option<String>, @@ -49,8 +45,9 @@ pub struct State<T> {      last_selection: Option<T>,  } -impl<T> Default for State<T> { -    fn default() -> Self { +impl<T> State<T> { +    /// Creates a new [`State`] for a [`PickList`]. +    pub fn new() -> Self {          Self {              menu: menu::State::default(),              keyboard_modifiers: keyboard::Modifiers::default(), @@ -61,6 +58,12 @@ impl<T> Default for State<T> {      }  } +impl<T> Default for State<T> { +    fn default() -> Self { +        Self::new() +    } +} +  impl<'a, T: 'a, Message, Renderer: text::Renderer>      PickList<'a, T, Message, Renderer>  where @@ -79,20 +82,8 @@ where          selected: Option<T>,          on_selected: impl Fn(T) -> Message + 'static,      ) -> Self { -        let State { -            menu, -            keyboard_modifiers, -            is_open, -            hovered_option, -            last_selection, -        } = state; -          Self { -            menu, -            keyboard_modifiers, -            is_open, -            hovered_option, -            last_selection, +            state,              on_selected: Box::new(on_selected),              options: options.into(),              placeholder: None, @@ -145,128 +136,118 @@ where      }  } -impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> -    for PickList<'a, T, Message, Renderer> +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    padding: Padding, +    text_size: Option<u16>, +    font: &Renderer::Font, +    placeholder: Option<&str>, +    options: &[T], +) -> layout::Node  where -    T: Clone + ToString + Eq, -    [T]: ToOwned<Owned = Vec<T>>, -    Message: 'static, -    Renderer: text::Renderer + 'a, +    Renderer: text::Renderer, +    T: ToString,  { -    fn width(&self) -> Length { -        self.width -    } +    use std::f32; -    fn height(&self) -> Length { -        Length::Shrink -    } +    let limits = limits.width(width).height(Length::Shrink).pad(padding); -    fn layout( -        &self, -        renderer: &Renderer, -        limits: &layout::Limits, -    ) -> layout::Node { -        use std::f32; - -        let limits = limits -            .width(self.width) -            .height(Length::Shrink) -            .pad(self.padding); - -        let text_size = self.text_size.unwrap_or(renderer.default_size()); -        let font = self.font.clone(); - -        let max_width = match self.width { -            Length::Shrink => { -                let measure = |label: &str| -> u32 { -                    let (width, _) = renderer.measure( -                        label, -                        text_size, -                        font.clone(), -                        Size::new(f32::INFINITY, f32::INFINITY), -                    ); - -                    width.round() as u32 -                }; +    let text_size = text_size.unwrap_or(renderer.default_size()); -                let labels = self.options.iter().map(ToString::to_string); +    let max_width = match width { +        Length::Shrink => { +            let measure = |label: &str| -> u32 { +                let (width, _) = renderer.measure( +                    label, +                    text_size, +                    font.clone(), +                    Size::new(f32::INFINITY, f32::INFINITY), +                ); -                let labels_width = -                    labels.map(|label| measure(&label)).max().unwrap_or(100); +                width.round() as u32 +            }; -                let placeholder_width = self -                    .placeholder -                    .as_ref() -                    .map(String::as_str) -                    .map(measure) -                    .unwrap_or(100); +            let labels = options.iter().map(ToString::to_string); -                labels_width.max(placeholder_width) -            } -            _ => 0, -        }; +            let labels_width = +                labels.map(|label| measure(&label)).max().unwrap_or(100); -        let size = { -            let intrinsic = Size::new( -                max_width as f32 -                    + f32::from(text_size) -                    + f32::from(self.padding.left), -                f32::from(text_size), -            ); +            let placeholder_width = placeholder.map(measure).unwrap_or(100); -            limits.resolve(intrinsic).pad(self.padding) -        }; +            labels_width.max(placeholder_width) +        } +        _ => 0, +    }; -        layout::Node::new(size) -    } +    let size = { +        let intrinsic = Size::new( +            max_width as f32 + f32::from(text_size) + f32::from(padding.left), +            f32::from(text_size), +        ); -    fn on_event( -        &mut self, -        event: Event, -        layout: Layout<'_>, -        cursor_position: Point, -        _renderer: &Renderer, -        _clipboard: &mut dyn Clipboard, -        shell: &mut Shell<'_, Message>, -    ) -> event::Status { -        match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerPressed { .. }) => { -                let event_status = if *self.is_open { -                    // TODO: Encode cursor availability in the type system -                    *self.is_open = -                        cursor_position.x < 0.0 || cursor_position.y < 0.0; - -                    event::Status::Captured -                } else if layout.bounds().contains(cursor_position) { -                    let selected = self.selected.as_ref(); - -                    *self.is_open = true; -                    *self.hovered_option = self -                        .options -                        .iter() -                        .position(|option| Some(option) == selected); - -                    event::Status::Captured -                } else { -                    event::Status::Ignored -                }; +        limits.resolve(intrinsic).pad(padding) +    }; + +    layout::Node::new(size) +} -                if let Some(last_selection) = self.last_selection.take() { -                    shell.publish((self.on_selected)(last_selection)); +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( +    event: Event, +    layout: Layout<'_>, +    cursor_position: Point, +    shell: &mut Shell<'_, Message>, +    on_selected: &dyn Fn(T) -> Message, +    selected: Option<&T>, +    options: &[T], +    state: impl FnOnce() -> &'a mut State<T>, +) -> event::Status +where +    T: PartialEq + Clone + 'a, +{ +    match event { +        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerPressed { .. }) => { +            let state = state(); -                    *self.is_open = false; +            let event_status = if state.is_open { +                // TODO: Encode cursor availability in the type system +                state.is_open = +                    cursor_position.x < 0.0 || cursor_position.y < 0.0; -                    event::Status::Captured -                } else { -                    event_status -                } +                event::Status::Captured +            } else if layout.bounds().contains(cursor_position) { +                state.is_open = true; +                state.hovered_option = +                    options.iter().position(|option| Some(option) == selected); + +                event::Status::Captured +            } else { +                event::Status::Ignored +            }; + +            if let Some(last_selection) = state.last_selection.take() { +                shell.publish((on_selected)(last_selection)); + +                state.is_open = false; + +                event::Status::Captured +            } else { +                event_status              } -            Event::Mouse(mouse::Event::WheelScrolled { -                delta: mouse::ScrollDelta::Lines { y, .. }, -            }) if self.keyboard_modifiers.command() +        } +        Event::Mouse(mouse::Event::WheelScrolled { +            delta: mouse::ScrollDelta::Lines { y, .. }, +        }) => { +            let state = state(); + +            if state.keyboard_modifiers.command()                  && layout.bounds().contains(cursor_position) -                && !*self.is_open => +                && !state.is_open              {                  fn find_next<'a, T: PartialEq>(                      selected: &'a T, @@ -278,34 +259,219 @@ where                  }                  let next_option = if y < 0.0 { -                    if let Some(selected) = self.selected.as_ref() { -                        find_next(selected, self.options.iter()) +                    if let Some(selected) = selected { +                        find_next(selected, options.iter())                      } else { -                        self.options.first() +                        options.first()                      }                  } else if y > 0.0 { -                    if let Some(selected) = self.selected.as_ref() { -                        find_next(selected, self.options.iter().rev()) +                    if let Some(selected) = selected { +                        find_next(selected, options.iter().rev())                      } else { -                        self.options.last() +                        options.last()                      }                  } else {                      None                  };                  if let Some(next_option) = next_option { -                    shell.publish((self.on_selected)(next_option.clone())); +                    shell.publish((on_selected)(next_option.clone()));                  }                  event::Status::Captured -            } -            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { -                *self.keyboard_modifiers = modifiers; - +            } else {                  event::Status::Ignored              } -            _ => event::Status::Ignored,          } +        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { +            let state = state(); + +            state.keyboard_modifiers = modifiers; + +            event::Status::Ignored +        } +        _ => event::Status::Ignored, +    } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( +    layout: Layout<'_>, +    cursor_position: Point, +) -> mouse::Interaction { +    let bounds = layout.bounds(); +    let is_mouse_over = bounds.contains(cursor_position); + +    if is_mouse_over { +        mouse::Interaction::Pointer +    } else { +        mouse::Interaction::default() +    } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( +    layout: Layout<'_>, +    state: &'a mut State<T>, +    padding: Padding, +    text_size: Option<u16>, +    font: Renderer::Font, +    options: &'a [T], +    style_sheet: &dyn StyleSheet, +) -> Option<overlay::Element<'a, Message, Renderer>> +where +    Message: 'a, +    Renderer: text::Renderer + 'a, +    T: Clone + ToString, +{ +    if state.is_open { +        let bounds = layout.bounds(); + +        let mut menu = Menu::new( +            &mut state.menu, +            options, +            &mut state.hovered_option, +            &mut state.last_selection, +        ) +        .width(bounds.width.round() as u16) +        .padding(padding) +        .font(font) +        .style(style_sheet.menu()); + +        if let Some(text_size) = text_size { +            menu = menu.text_size(text_size); +        } + +        Some(menu.overlay(layout.position(), bounds.height)) +    } else { +        None +    } +} + +/// Draws a [`PickList`]. +pub fn draw<T, Renderer>( +    renderer: &mut Renderer, +    layout: Layout<'_>, +    cursor_position: Point, +    padding: Padding, +    text_size: Option<u16>, +    font: &Renderer::Font, +    placeholder: Option<&str>, +    selected: Option<&T>, +    style_sheet: &dyn StyleSheet, +) where +    Renderer: text::Renderer, +    T: ToString, +{ +    let bounds = layout.bounds(); +    let is_mouse_over = bounds.contains(cursor_position); +    let is_selected = selected.is_some(); + +    let style = if is_mouse_over { +        style_sheet.hovered() +    } else { +        style_sheet.active() +    }; + +    renderer.fill_quad( +        renderer::Quad { +            bounds, +            border_color: style.border_color, +            border_width: style.border_width, +            border_radius: style.border_radius, +        }, +        style.background, +    ); + +    renderer.fill_text(Text { +        content: &Renderer::ARROW_DOWN_ICON.to_string(), +        font: Renderer::ICON_FONT, +        size: bounds.height * style.icon_size, +        bounds: Rectangle { +            x: bounds.x + bounds.width - f32::from(padding.horizontal()), +            y: bounds.center_y(), +            ..bounds +        }, +        color: style.text_color, +        horizontal_alignment: alignment::Horizontal::Right, +        vertical_alignment: alignment::Vertical::Center, +    }); + +    let label = selected.map(ToString::to_string); + +    if let Some(label) = +        label.as_ref().map(String::as_str).or_else(|| placeholder) +    { +        renderer.fill_text(Text { +            content: label, +            size: f32::from(text_size.unwrap_or(renderer.default_size())), +            font: font.clone(), +            color: is_selected +                .then(|| style.text_color) +                .unwrap_or(style.placeholder_color), +            bounds: Rectangle { +                x: bounds.x + f32::from(padding.left), +                y: bounds.center_y(), +                ..bounds +            }, +            horizontal_alignment: alignment::Horizontal::Left, +            vertical_alignment: alignment::Vertical::Center, +        }) +    } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> +    for PickList<'a, T, Message, Renderer> +where +    T: Clone + ToString + Eq, +    [T]: ToOwned<Owned = Vec<T>>, +    Message: 'static, +    Renderer: text::Renderer + 'a, +{ +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        layout( +            renderer, +            limits, +            self.width, +            self.padding, +            self.text_size, +            &self.font, +            self.placeholder.as_ref().map(String::as_str), +            &self.options, +        ) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        _renderer: &Renderer, +        _clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        update( +            event, +            layout, +            cursor_position, +            shell, +            self.on_selected.as_ref(), +            self.selected.as_ref(), +            &self.options, +            || &mut self.state, +        )      }      fn mouse_interaction( @@ -315,14 +481,7 @@ where          _viewport: &Rectangle,          _renderer: &Renderer,      ) -> mouse::Interaction { -        let bounds = layout.bounds(); -        let is_mouse_over = bounds.contains(cursor_position); - -        if is_mouse_over { -            mouse::Interaction::Pointer -        } else { -            mouse::Interaction::default() -        } +        mouse_interaction(layout, cursor_position)      }      fn draw( @@ -333,66 +492,17 @@ where          cursor_position: Point,          _viewport: &Rectangle,      ) { -        let bounds = layout.bounds(); -        let is_mouse_over = bounds.contains(cursor_position); -        let is_selected = self.selected.is_some(); - -        let style = if is_mouse_over { -            self.style_sheet.hovered() -        } else { -            self.style_sheet.active() -        }; - -        renderer.fill_quad( -            renderer::Quad { -                bounds, -                border_color: style.border_color, -                border_width: style.border_width, -                border_radius: style.border_radius, -            }, -            style.background, -        ); - -        renderer.fill_text(Text { -            content: &Renderer::ARROW_DOWN_ICON.to_string(), -            font: Renderer::ICON_FONT, -            size: bounds.height * style.icon_size, -            bounds: Rectangle { -                x: bounds.x + bounds.width -                    - f32::from(self.padding.horizontal()), -                y: bounds.center_y(), -                ..bounds -            }, -            color: style.text_color, -            horizontal_alignment: alignment::Horizontal::Right, -            vertical_alignment: alignment::Vertical::Center, -        }); - -        if let Some(label) = self -            .selected -            .as_ref() -            .map(ToString::to_string) -            .as_ref() -            .or_else(|| self.placeholder.as_ref()) -        { -            renderer.fill_text(Text { -                content: label, -                size: f32::from( -                    self.text_size.unwrap_or(renderer.default_size()), -                ), -                font: self.font.clone(), -                color: is_selected -                    .then(|| style.text_color) -                    .unwrap_or(style.placeholder_color), -                bounds: Rectangle { -                    x: bounds.x + f32::from(self.padding.left), -                    y: bounds.center_y(), -                    ..bounds -                }, -                horizontal_alignment: alignment::Horizontal::Left, -                vertical_alignment: alignment::Vertical::Center, -            }) -        } +        draw( +            renderer, +            layout, +            cursor_position, +            self.padding, +            self.text_size, +            &self.font, +            self.placeholder.as_ref().map(String::as_str), +            self.selected.as_ref(), +            self.style_sheet.as_ref(), +        )      }      fn overlay( @@ -400,28 +510,15 @@ where          layout: Layout<'_>,          _renderer: &Renderer,      ) -> Option<overlay::Element<'_, Message, Renderer>> { -        if *self.is_open { -            let bounds = layout.bounds(); - -            let mut menu = Menu::new( -                &mut self.menu, -                &self.options, -                &mut self.hovered_option, -                &mut self.last_selection, -            ) -            .width(bounds.width.round() as u16) -            .padding(self.padding) -            .font(self.font.clone()) -            .style(self.style_sheet.menu()); - -            if let Some(text_size) = self.text_size { -                menu = menu.text_size(text_size); -            } - -            Some(menu.overlay(layout.position(), bounds.height)) -        } else { -            None -        } +        overlay( +            layout, +            &mut self.state, +            self.padding, +            self.text_size, +            self.font.clone(), +            &self.options, +            self.style_sheet.as_ref(), +        )      }  } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index fed2925b..657ae786 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -79,7 +79,7 @@ where      ) -> Self      where          V: Eq + Copy, -        F: 'static + Fn(V) -> Message, +        F: FnOnce(V) -> Message,      {          Radio {              is_selected: Some(value) == selected, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ce734ad8..3752fd71 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -139,235 +139,201 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> {          self.content = self.content.push(child);          self      } - -    fn notify_on_scroll( -        &self, -        bounds: Rectangle, -        content_bounds: Rectangle, -        shell: &mut Shell<'_, Message>, -    ) { -        if content_bounds.height <= bounds.height { -            return; -        } - -        if let Some(on_scroll) = &self.on_scroll { -            shell.publish(on_scroll( -                self.state.offset.absolute(bounds, content_bounds) -                    / (content_bounds.height - bounds.height), -            )); -        } -    } - -    fn scrollbar( -        &self, -        bounds: Rectangle, -        content_bounds: Rectangle, -    ) -> Option<Scrollbar> { -        let offset = self.state.offset(bounds, content_bounds); - -        if content_bounds.height > bounds.height { -            let outer_width = self.scrollbar_width.max(self.scroller_width) -                + 2 * self.scrollbar_margin; - -            let outer_bounds = Rectangle { -                x: bounds.x + bounds.width - outer_width as f32, -                y: bounds.y, -                width: outer_width as f32, -                height: bounds.height, -            }; - -            let scrollbar_bounds = Rectangle { -                x: bounds.x + bounds.width -                    - f32::from(outer_width / 2 + self.scrollbar_width / 2), -                y: bounds.y, -                width: self.scrollbar_width as f32, -                height: bounds.height, -            }; - -            let ratio = bounds.height / content_bounds.height; -            let scroller_height = bounds.height * ratio; -            let y_offset = offset as f32 * ratio; - -            let scroller_bounds = Rectangle { -                x: bounds.x + bounds.width -                    - f32::from(outer_width / 2 + self.scroller_width / 2), -                y: scrollbar_bounds.y + y_offset, -                width: self.scroller_width as f32, -                height: scroller_height, -            }; - -            Some(Scrollbar { -                outer_bounds, -                bounds: scrollbar_bounds, -                scroller: Scroller { -                    bounds: scroller_bounds, -                }, -            }) -        } else { -            None -        } -    }  } -impl<'a, Message, Renderer> Widget<Message, Renderer> -    for Scrollable<'a, Message, Renderer> -where -    Renderer: crate::Renderer, -{ -    fn width(&self) -> Length { -        Widget::<Message, Renderer>::width(&self.content) -    } - -    fn height(&self) -> Length { -        self.height -    } +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    height: Length, +    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { +    let limits = limits.width(width).height(height); -    fn layout( -        &self, -        renderer: &Renderer, -        limits: &layout::Limits, -    ) -> layout::Node { -        let limits = limits -            .max_height(self.max_height) -            .width(Widget::<Message, Renderer>::width(&self.content)) -            .height(self.height); - -        let child_limits = layout::Limits::new( -            Size::new(limits.min().width, 0.0), -            Size::new(limits.max().width, f32::INFINITY), -        ); +    let child_limits = layout::Limits::new( +        Size::new(limits.min().width, 0.0), +        Size::new(limits.max().width, f32::INFINITY), +    ); -        let content = self.content.layout(renderer, &child_limits); -        let size = limits.resolve(content.size()); +    let content = layout_content(renderer, &child_limits); +    let size = limits.resolve(content.size()); -        layout::Node::with_children(size, vec![content]) -    } +    layout::Node::with_children(size, vec![content]) +} -    fn on_event( -        &mut self, -        event: Event, -        layout: Layout<'_>, -        cursor_position: Point, -        renderer: &Renderer, -        clipboard: &mut dyn Clipboard, -        shell: &mut Shell<'_, Message>, -    ) -> event::Status { -        let bounds = layout.bounds(); -        let is_mouse_over = bounds.contains(cursor_position); - -        let content = layout.children().next().unwrap(); -        let content_bounds = content.bounds(); - -        let scrollbar = self.scrollbar(bounds, content_bounds); -        let is_mouse_over_scrollbar = scrollbar -            .as_ref() -            .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) -            .unwrap_or(false); - -        let event_status = { -            let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { -                Point::new( -                    cursor_position.x, -                    cursor_position.y -                        + self.state.offset(bounds, content_bounds) as f32, -                ) -            } else { -                // TODO: Make `cursor_position` an `Option<Point>` so we can encode -                // cursor availability. -                // This will probably happen naturally once we add multi-window -                // support. -                Point::new(cursor_position.x, -1.0) -            }; - -            self.content.on_event( -                event.clone(), -                content, -                cursor_position, -                renderer, -                clipboard, -                shell, +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update<Message>( +    state: &mut State, +    event: Event, +    layout: Layout<'_>, +    cursor_position: Point, +    clipboard: &mut dyn Clipboard, +    shell: &mut Shell<'_, Message>, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16, +    on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, +    update_content: impl FnOnce( +        Event, +        Layout<'_>, +        Point, +        &mut dyn Clipboard, +        &mut Shell<'_, Message>, +    ) -> event::Status, +) -> event::Status { +    let bounds = layout.bounds(); +    let is_mouse_over = bounds.contains(cursor_position); + +    let content = layout.children().next().unwrap(); +    let content_bounds = content.bounds(); + +    let scrollbar = scrollbar( +        state, +        scrollbar_width, +        scrollbar_margin, +        scroller_width, +        bounds, +        content_bounds, +    ); +    let is_mouse_over_scrollbar = scrollbar +        .as_ref() +        .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) +        .unwrap_or(false); + +    let event_status = { +        let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { +            Point::new( +                cursor_position.x, +                cursor_position.y + state.offset(bounds, content_bounds) as f32,              ) +        } else { +            // TODO: Make `cursor_position` an `Option<Point>` so we can encode +            // cursor availability. +            // This will probably happen naturally once we add multi-window +            // support. +            Point::new(cursor_position.x, -1.0)          }; -        if let event::Status::Captured = event_status { -            return event::Status::Captured; -        } +        update_content( +            event.clone(), +            content, +            cursor_position, +            clipboard, +            shell, +        ) +    }; + +    if let event::Status::Captured = event_status { +        return event::Status::Captured; +    } + +    if is_mouse_over { +        match event { +            Event::Mouse(mouse::Event::WheelScrolled { delta }) => { +                match delta { +                    mouse::ScrollDelta::Lines { y, .. } => { +                        // TODO: Configurable speed (?) +                        state.scroll(y * 60.0, bounds, content_bounds); +                    } +                    mouse::ScrollDelta::Pixels { y, .. } => { +                        state.scroll(y, bounds, content_bounds); +                    } +                } -        if is_mouse_over { -            match event { -                Event::Mouse(mouse::Event::WheelScrolled { delta }) => { -                    match delta { -                        mouse::ScrollDelta::Lines { y, .. } => { -                            // TODO: Configurable speed (?) -                            self.state.scroll(y * 60.0, bounds, content_bounds); -                        } -                        mouse::ScrollDelta::Pixels { y, .. } => { -                            self.state.scroll(y, bounds, content_bounds); -                        } +                notify_on_scroll( +                    state, +                    on_scroll, +                    bounds, +                    content_bounds, +                    shell, +                ); + +                return event::Status::Captured; +            } +            Event::Touch(event) => { +                match event { +                    touch::Event::FingerPressed { .. } => { +                        state.scroll_box_touched_at = Some(cursor_position);                      } +                    touch::Event::FingerMoved { .. } => { +                        if let Some(scroll_box_touched_at) = +                            state.scroll_box_touched_at +                        { +                            let delta = +                                cursor_position.y - scroll_box_touched_at.y; -                    self.notify_on_scroll(bounds, content_bounds, shell); +                            state.scroll(delta, bounds, content_bounds); -                    return event::Status::Captured; -                } -                Event::Touch(event) => { -                    match event { -                        touch::Event::FingerPressed { .. } => { -                            self.state.scroll_box_touched_at = -                                Some(cursor_position); -                        } -                        touch::Event::FingerMoved { .. } => { -                            if let Some(scroll_box_touched_at) = -                                self.state.scroll_box_touched_at -                            { -                                let delta = -                                    cursor_position.y - scroll_box_touched_at.y; - -                                self.state.scroll( -                                    delta, -                                    bounds, -                                    content_bounds, -                                ); - -                                self.state.scroll_box_touched_at = -                                    Some(cursor_position); - -                                self.notify_on_scroll( -                                    bounds, -                                    content_bounds, -                                    shell, -                                ); -                            } -                        } -                        touch::Event::FingerLifted { .. } -                        | touch::Event::FingerLost { .. } => { -                            self.state.scroll_box_touched_at = None; +                            state.scroll_box_touched_at = Some(cursor_position); + +                            notify_on_scroll( +                                state, +                                on_scroll, +                                bounds, +                                content_bounds, +                                shell, +                            );                          }                      } - -                    return event::Status::Captured; +                    touch::Event::FingerLifted { .. } +                    | touch::Event::FingerLost { .. } => { +                        state.scroll_box_touched_at = None; +                    }                  } -                _ => {} + +                return event::Status::Captured;              } +            _ => {}          } +    } + +    if state.is_scroller_grabbed() { +        match event { +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerLifted { .. }) +            | Event::Touch(touch::Event::FingerLost { .. }) => { +                state.scroller_grabbed_at = None; -        if self.state.is_scroller_grabbed() { -            match event { -                Event::Mouse(mouse::Event::ButtonReleased( -                    mouse::Button::Left, -                )) -                | Event::Touch(touch::Event::FingerLifted { .. }) -                | Event::Touch(touch::Event::FingerLost { .. }) => { -                    self.state.scroller_grabbed_at = None; +                return event::Status::Captured; +            } +            Event::Mouse(mouse::Event::CursorMoved { .. }) +            | Event::Touch(touch::Event::FingerMoved { .. }) => { +                if let (Some(scrollbar), Some(scroller_grabbed_at)) = +                    (scrollbar, state.scroller_grabbed_at) +                { +                    state.scroll_to( +                        scrollbar.scroll_percentage( +                            scroller_grabbed_at, +                            cursor_position, +                        ), +                        bounds, +                        content_bounds, +                    ); + +                    notify_on_scroll( +                        state, +                        on_scroll, +                        bounds, +                        content_bounds, +                        shell, +                    );                      return event::Status::Captured;                  } -                Event::Mouse(mouse::Event::CursorMoved { .. }) -                | Event::Touch(touch::Event::FingerMoved { .. }) => { -                    if let (Some(scrollbar), Some(scroller_grabbed_at)) = -                        (scrollbar, self.state.scroller_grabbed_at) +            } +            _ => {} +        } +    } else if is_mouse_over_scrollbar { +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => { +                if let Some(scrollbar) = scrollbar { +                    if let Some(scroller_grabbed_at) = +                        scrollbar.grab_scroller(cursor_position)                      { -                        self.state.scroll_to( +                        state.scroll_to(                              scrollbar.scroll_percentage(                                  scroller_grabbed_at,                                  cursor_position, @@ -376,50 +342,329 @@ where                              content_bounds,                          ); -                        self.notify_on_scroll(bounds, content_bounds, shell); +                        state.scroller_grabbed_at = Some(scroller_grabbed_at); + +                        notify_on_scroll( +                            state, +                            on_scroll, +                            bounds, +                            content_bounds, +                            shell, +                        );                          return event::Status::Captured;                      }                  } -                _ => {}              } -        } else if is_mouse_over_scrollbar { -            match event { -                Event::Mouse(mouse::Event::ButtonPressed( -                    mouse::Button::Left, -                )) -                | Event::Touch(touch::Event::FingerPressed { .. }) => { -                    if let Some(scrollbar) = scrollbar { -                        if let Some(scroller_grabbed_at) = -                            scrollbar.grab_scroller(cursor_position) -                        { -                            self.state.scroll_to( -                                scrollbar.scroll_percentage( -                                    scroller_grabbed_at, -                                    cursor_position, -                                ), -                                bounds, -                                content_bounds, -                            ); +            _ => {} +        } +    } -                            self.state.scroller_grabbed_at = -                                Some(scroller_grabbed_at); +    event::Status::Ignored +} -                            self.notify_on_scroll( -                                bounds, -                                content_bounds, -                                shell, -                            ); +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( +    state: &State, +    layout: Layout<'_>, +    cursor_position: Point, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16, +    content_interaction: impl FnOnce( +        Layout<'_>, +        Point, +        &Rectangle, +    ) -> mouse::Interaction, +) -> mouse::Interaction { +    let bounds = layout.bounds(); +    let content_layout = layout.children().next().unwrap(); +    let content_bounds = content_layout.bounds(); +    let scrollbar = scrollbar( +        state, +        scrollbar_width, +        scrollbar_margin, +        scroller_width, +        bounds, +        content_bounds, +    ); + +    let is_mouse_over = bounds.contains(cursor_position); +    let is_mouse_over_scrollbar = scrollbar +        .as_ref() +        .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) +        .unwrap_or(false); + +    if is_mouse_over_scrollbar || state.is_scroller_grabbed() { +        mouse::Interaction::Idle +    } else { +        let offset = state.offset(bounds, content_bounds); -                            return event::Status::Captured; -                        } -                    } +        let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { +            Point::new(cursor_position.x, cursor_position.y + offset as f32) +        } else { +            Point::new(cursor_position.x, -1.0) +        }; + +        content_interaction( +            content_layout, +            cursor_position, +            &Rectangle { +                y: bounds.y + offset as f32, +                ..bounds +            }, +        ) +    } +} + +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( +    state: &State, +    renderer: &mut Renderer, +    layout: Layout<'_>, +    cursor_position: Point, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16, +    style_sheet: &dyn StyleSheet, +    draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), +) where +    Renderer: crate::Renderer, +{ +    let bounds = layout.bounds(); +    let content_layout = layout.children().next().unwrap(); +    let content_bounds = content_layout.bounds(); +    let offset = state.offset(bounds, content_bounds); +    let scrollbar = scrollbar( +        state, +        scrollbar_width, +        scrollbar_margin, +        scroller_width, +        bounds, +        content_bounds, +    ); + +    let is_mouse_over = bounds.contains(cursor_position); +    let is_mouse_over_scrollbar = scrollbar +        .as_ref() +        .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) +        .unwrap_or(false); + +    let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { +        Point::new(cursor_position.x, cursor_position.y + offset as f32) +    } else { +        Point::new(cursor_position.x, -1.0) +    }; + +    if let Some(scrollbar) = scrollbar { +        renderer.with_layer(bounds, |renderer| { +            renderer.with_translation( +                Vector::new(0.0, -(offset as f32)), +                |renderer| { +                    draw_content( +                        renderer, +                        content_layout, +                        cursor_position, +                        &Rectangle { +                            y: bounds.y + offset as f32, +                            ..bounds +                        }, +                    ); +                }, +            ); +        }); + +        let style = if state.is_scroller_grabbed() { +            style_sheet.dragging() +        } else if is_mouse_over_scrollbar { +            style_sheet.hovered() +        } else { +            style_sheet.active() +        }; + +        let is_scrollbar_visible = +            style.background.is_some() || style.border_width > 0.0; + +        renderer.with_layer( +            Rectangle { +                width: bounds.width + 2.0, +                height: bounds.height + 2.0, +                ..bounds +            }, +            |renderer| { +                if is_scrollbar_visible { +                    renderer.fill_quad( +                        renderer::Quad { +                            bounds: scrollbar.bounds, +                            border_radius: style.border_radius, +                            border_width: style.border_width, +                            border_color: style.border_color, +                        }, +                        style +                            .background +                            .unwrap_or(Background::Color(Color::TRANSPARENT)), +                    );                  } -                _ => {} -            } -        } -        event::Status::Ignored +                if is_mouse_over +                    || state.is_scroller_grabbed() +                    || is_scrollbar_visible +                { +                    renderer.fill_quad( +                        renderer::Quad { +                            bounds: scrollbar.scroller.bounds, +                            border_radius: style.scroller.border_radius, +                            border_width: style.scroller.border_width, +                            border_color: style.scroller.border_color, +                        }, +                        style.scroller.color, +                    ); +                } +            }, +        ); +    } else { +        draw_content( +            renderer, +            content_layout, +            cursor_position, +            &Rectangle { +                y: bounds.y + offset as f32, +                ..bounds +            }, +        ); +    } +} + +fn scrollbar( +    state: &State, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16, +    bounds: Rectangle, +    content_bounds: Rectangle, +) -> Option<Scrollbar> { +    let offset = state.offset(bounds, content_bounds); + +    if content_bounds.height > bounds.height { +        let outer_width = +            scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + +        let outer_bounds = Rectangle { +            x: bounds.x + bounds.width - outer_width as f32, +            y: bounds.y, +            width: outer_width as f32, +            height: bounds.height, +        }; + +        let scrollbar_bounds = Rectangle { +            x: bounds.x + bounds.width +                - f32::from(outer_width / 2 + scrollbar_width / 2), +            y: bounds.y, +            width: scrollbar_width as f32, +            height: bounds.height, +        }; + +        let ratio = bounds.height / content_bounds.height; +        let scroller_height = bounds.height * ratio; +        let y_offset = offset as f32 * ratio; + +        let scroller_bounds = Rectangle { +            x: bounds.x + bounds.width +                - f32::from(outer_width / 2 + scroller_width / 2), +            y: scrollbar_bounds.y + y_offset, +            width: scroller_width as f32, +            height: scroller_height, +        }; + +        Some(Scrollbar { +            outer_bounds, +            bounds: scrollbar_bounds, +            scroller: Scroller { +                bounds: scroller_bounds, +            }, +        }) +    } else { +        None +    } +} + +fn notify_on_scroll<Message>( +    state: &State, +    on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, +    bounds: Rectangle, +    content_bounds: Rectangle, +    shell: &mut Shell<'_, Message>, +) { +    if content_bounds.height <= bounds.height { +        return; +    } + +    if let Some(on_scroll) = on_scroll { +        shell.publish(on_scroll( +            state.offset.absolute(bounds, content_bounds) +                / (content_bounds.height - bounds.height), +        )); +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Scrollable<'a, Message, Renderer> +where +    Renderer: crate::Renderer, +{ +    fn width(&self) -> Length { +        Widget::<Message, Renderer>::width(&self.content) +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        layout( +            renderer, +            limits, +            Widget::<Message, Renderer>::width(self), +            self.height, +            |renderer, limits| self.content.layout(renderer, limits), +        ) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        update( +            &mut self.state, +            event, +            layout, +            cursor_position, +            clipboard, +            shell, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            &self.on_scroll, +            |event, layout, cursor_position, clipboard, shell| { +                self.content.on_event( +                    event, +                    layout, +                    cursor_position, +                    renderer, +                    clipboard, +                    shell, +                ) +            }, +        )      }      fn mouse_interaction( @@ -429,38 +674,22 @@ where          _viewport: &Rectangle,          renderer: &Renderer,      ) -> mouse::Interaction { -        let bounds = layout.bounds(); -        let content_layout = layout.children().next().unwrap(); -        let content_bounds = content_layout.bounds(); -        let scrollbar = self.scrollbar(bounds, content_bounds); - -        let is_mouse_over = bounds.contains(cursor_position); -        let is_mouse_over_scrollbar = scrollbar -            .as_ref() -            .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) -            .unwrap_or(false); - -        if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() { -            mouse::Interaction::Idle -        } else { -            let offset = self.state.offset(bounds, content_bounds); - -            let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { -                Point::new(cursor_position.x, cursor_position.y + offset as f32) -            } else { -                Point::new(cursor_position.x, -1.0) -            }; - -            self.content.mouse_interaction( -                content_layout, -                cursor_position, -                &Rectangle { -                    y: bounds.y + offset as f32, -                    ..bounds -                }, -                renderer, -            ) -        } +        mouse_interaction( +            &self.state, +            layout, +            cursor_position, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            |layout, cursor_position, viewport| { +                self.content.mouse_interaction( +                    layout, +                    cursor_position, +                    viewport, +                    renderer, +                ) +            }, +        )      }      fn draw( @@ -471,103 +700,25 @@ where          cursor_position: Point,          _viewport: &Rectangle,      ) { -        let bounds = layout.bounds(); -        let content_layout = layout.children().next().unwrap(); -        let content_bounds = content_layout.bounds(); -        let offset = self.state.offset(bounds, content_bounds); -        let scrollbar = self.scrollbar(bounds, content_bounds); - -        let is_mouse_over = bounds.contains(cursor_position); -        let is_mouse_over_scrollbar = scrollbar -            .as_ref() -            .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) -            .unwrap_or(false); - -        let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { -            Point::new(cursor_position.x, cursor_position.y + offset as f32) -        } else { -            Point::new(cursor_position.x, -1.0) -        }; - -        if let Some(scrollbar) = scrollbar { -            renderer.with_layer(bounds, |renderer| { -                renderer.with_translation( -                    Vector::new(0.0, -(offset as f32)), -                    |renderer| { -                        self.content.draw( -                            renderer, -                            style, -                            content_layout, -                            cursor_position, -                            &Rectangle { -                                y: bounds.y + offset as f32, -                                ..bounds -                            }, -                        ); -                    }, -                ); -            }); - -            let style = if self.state.is_scroller_grabbed() { -                self.style_sheet.dragging() -            } else if is_mouse_over_scrollbar { -                self.style_sheet.hovered() -            } else { -                self.style_sheet.active() -            }; - -            let is_scrollbar_visible = -                style.background.is_some() || style.border_width > 0.0; - -            renderer.with_layer( -                Rectangle { -                    width: bounds.width + 2.0, -                    height: bounds.height + 2.0, -                    ..bounds -                }, -                |renderer| { -                    if is_scrollbar_visible { -                        renderer.fill_quad( -                            renderer::Quad { -                                bounds: scrollbar.bounds, -                                border_radius: style.border_radius, -                                border_width: style.border_width, -                                border_color: style.border_color, -                            }, -                            style.background.unwrap_or(Background::Color( -                                Color::TRANSPARENT, -                            )), -                        ); -                    } - -                    if is_mouse_over -                        || self.state.is_scroller_grabbed() -                        || is_scrollbar_visible -                    { -                        renderer.fill_quad( -                            renderer::Quad { -                                bounds: scrollbar.scroller.bounds, -                                border_radius: style.scroller.border_radius, -                                border_width: style.scroller.border_width, -                                border_color: style.scroller.border_color, -                            }, -                            style.scroller.color, -                        ); -                    } -                }, -            ); -        } else { -            self.content.draw( -                renderer, -                style, -                content_layout, -                cursor_position, -                &Rectangle { -                    y: bounds.y + offset as f32, -                    ..bounds -                }, -            ); -        } +        draw( +            &self.state, +            renderer, +            layout, +            cursor_position, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            self.style_sheet.as_ref(), +            |renderer, layout, cursor_position, viewport| { +                self.content.draw( +                    renderer, +                    style, +                    layout, +                    cursor_position, +                    viewport, +                ) +            }, +        )      }      fn overlay( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 289f75f5..4c56083e 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -142,6 +142,207 @@ where      }  } +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( +    event: Event, +    layout: Layout<'_>, +    cursor_position: Point, +    shell: &mut Shell<'_, Message>, +    state: &mut State, +    value: &mut T, +    range: &RangeInclusive<T>, +    step: T, +    on_change: &dyn Fn(T) -> Message, +    on_release: &Option<Message>, +) -> event::Status +where +    T: Copy + Into<f64> + num_traits::FromPrimitive, +    Message: Clone, +{ +    let is_dragging = state.is_dragging; + +    let mut change = || { +        let bounds = layout.bounds(); +        let new_value = if cursor_position.x <= bounds.x { +            *range.start() +        } else if cursor_position.x >= bounds.x + bounds.width { +            *range.end() +        } else { +            let step = step.into(); +            let start = (*range.start()).into(); +            let end = (*range.end()).into(); + +            let percent = f64::from(cursor_position.x - bounds.x) +                / f64::from(bounds.width); + +            let steps = (percent * (end - start) / step).round(); +            let value = steps * step + start; + +            if let Some(value) = T::from_f64(value) { +                value +            } else { +                return; +            } +        }; + +        if ((*value).into() - new_value.into()).abs() > f64::EPSILON { +            shell.publish((on_change)(new_value)); + +            *value = new_value; +        } +    }; + +    match event { +        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerPressed { .. }) => { +            if layout.bounds().contains(cursor_position) { +                change(); +                state.is_dragging = true; + +                return event::Status::Captured; +            } +        } +        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerLifted { .. }) +        | Event::Touch(touch::Event::FingerLost { .. }) => { +            if is_dragging { +                if let Some(on_release) = on_release.clone() { +                    shell.publish(on_release); +                } +                state.is_dragging = false; + +                return event::Status::Captured; +            } +        } +        Event::Mouse(mouse::Event::CursorMoved { .. }) +        | Event::Touch(touch::Event::FingerMoved { .. }) => { +            if is_dragging { +                change(); + +                return event::Status::Captured; +            } +        } +        _ => {} +    } + +    event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T>( +    renderer: &mut impl crate::Renderer, +    layout: Layout<'_>, +    cursor_position: Point, +    state: &State, +    value: T, +    range: &RangeInclusive<T>, +    style_sheet: &dyn StyleSheet, +) where +    T: Into<f64> + Copy, +{ +    let bounds = layout.bounds(); +    let is_mouse_over = bounds.contains(cursor_position); + +    let style = if state.is_dragging { +        style_sheet.dragging() +    } else if is_mouse_over { +        style_sheet.hovered() +    } else { +        style_sheet.active() +    }; + +    let rail_y = bounds.y + (bounds.height / 2.0).round(); + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: bounds.x, +                y: rail_y, +                width: bounds.width, +                height: 2.0, +            }, +            border_radius: 0.0, +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        style.rail_colors.0, +    ); + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: bounds.x, +                y: rail_y + 2.0, +                width: bounds.width, +                height: 2.0, +            }, +            border_radius: 0.0, +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        Background::Color(style.rail_colors.1), +    ); + +    let (handle_width, handle_height, handle_border_radius) = match style +        .handle +        .shape +    { +        HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), +        HandleShape::Rectangle { +            width, +            border_radius, +        } => (f32::from(width), f32::from(bounds.height), border_radius), +    }; + +    let value = value.into() as f32; +    let (range_start, range_end) = { +        let (start, end) = range.clone().into_inner(); + +        (start.into() as f32, end.into() as f32) +    }; + +    let handle_offset = if range_start >= range_end { +        0.0 +    } else { +        (bounds.width - handle_width) * (value - range_start) +            / (range_end - range_start) +    }; + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: bounds.x + handle_offset.round(), +                y: rail_y - handle_height / 2.0, +                width: handle_width, +                height: handle_height, +            }, +            border_radius: handle_border_radius, +            border_width: style.handle.border_width, +            border_color: style.handle.border_color, +        }, +        style.handle.color, +    ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( +    layout: Layout<'_>, +    cursor_position: Point, +    state: &State, +) -> mouse::Interaction { +    let bounds = layout.bounds(); +    let is_mouse_over = bounds.contains(cursor_position); + +    if state.is_dragging { +        mouse::Interaction::Grabbing +    } else if is_mouse_over { +        mouse::Interaction::Grab +    } else { +        mouse::Interaction::default() +    } +} +  /// The local state of a [`Slider`].  #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]  pub struct State { @@ -192,73 +393,18 @@ where          _clipboard: &mut dyn Clipboard,          shell: &mut Shell<'_, Message>,      ) -> event::Status { -        let is_dragging = self.state.is_dragging; - -        let mut change = || { -            let bounds = layout.bounds(); -            let new_value = if cursor_position.x <= bounds.x { -                *self.range.start() -            } else if cursor_position.x >= bounds.x + bounds.width { -                *self.range.end() -            } else { -                let step = self.step.into(); -                let start = (*self.range.start()).into(); -                let end = (*self.range.end()).into(); - -                let percent = f64::from(cursor_position.x - bounds.x) -                    / f64::from(bounds.width); - -                let steps = (percent * (end - start) / step).round(); -                let value = steps * step + start; - -                if let Some(value) = T::from_f64(value) { -                    value -                } else { -                    return; -                } -            }; - -            if (self.value.into() - new_value.into()).abs() > f64::EPSILON { -                shell.publish((self.on_change)(new_value)); - -                self.value = new_value; -            } -        }; - -        match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerPressed { .. }) => { -                if layout.bounds().contains(cursor_position) { -                    change(); -                    self.state.is_dragging = true; - -                    return event::Status::Captured; -                } -            } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerLifted { .. }) -            | Event::Touch(touch::Event::FingerLost { .. }) => { -                if is_dragging { -                    if let Some(on_release) = self.on_release.clone() { -                        shell.publish(on_release); -                    } -                    self.state.is_dragging = false; - -                    return event::Status::Captured; -                } -            } -            Event::Mouse(mouse::Event::CursorMoved { .. }) -            | Event::Touch(touch::Event::FingerMoved { .. }) => { -                if is_dragging { -                    change(); - -                    return event::Status::Captured; -                } -            } -            _ => {} -        } - -        event::Status::Ignored +        update( +            event, +            layout, +            cursor_position, +            shell, +            &mut self.state, +            &mut self.value, +            &self.range, +            self.step, +            self.on_change.as_ref(), +            &self.on_release, +        )      }      fn draw( @@ -269,90 +415,15 @@ where          cursor_position: Point,          _viewport: &Rectangle,      ) { -        let bounds = layout.bounds(); -        let is_mouse_over = bounds.contains(cursor_position); - -        let style = if self.state.is_dragging { -            self.style_sheet.dragging() -        } else if is_mouse_over { -            self.style_sheet.hovered() -        } else { -            self.style_sheet.active() -        }; - -        let rail_y = bounds.y + (bounds.height / 2.0).round(); - -        renderer.fill_quad( -            renderer::Quad { -                bounds: Rectangle { -                    x: bounds.x, -                    y: rail_y, -                    width: bounds.width, -                    height: 2.0, -                }, -                border_radius: 0.0, -                border_width: 0.0, -                border_color: Color::TRANSPARENT, -            }, -            style.rail_colors.0, -        ); - -        renderer.fill_quad( -            renderer::Quad { -                bounds: Rectangle { -                    x: bounds.x, -                    y: rail_y + 2.0, -                    width: bounds.width, -                    height: 2.0, -                }, -                border_radius: 0.0, -                border_width: 0.0, -                border_color: Color::TRANSPARENT, -            }, -            Background::Color(style.rail_colors.1), -        ); - -        let (handle_width, handle_height, handle_border_radius) = match style -            .handle -            .shape -        { -            HandleShape::Circle { radius } => { -                (radius * 2.0, radius * 2.0, radius) -            } -            HandleShape::Rectangle { -                width, -                border_radius, -            } => (f32::from(width), f32::from(bounds.height), border_radius), -        }; - -        let value = self.value.into() as f32; -        let (range_start, range_end) = { -            let (start, end) = self.range.clone().into_inner(); - -            (start.into() as f32, end.into() as f32) -        }; - -        let handle_offset = if range_start >= range_end { -            0.0 -        } else { -            (bounds.width - handle_width) * (value - range_start) -                / (range_end - range_start) -        }; - -        renderer.fill_quad( -            renderer::Quad { -                bounds: Rectangle { -                    x: bounds.x + handle_offset.round(), -                    y: rail_y - handle_height / 2.0, -                    width: handle_width, -                    height: handle_height, -                }, -                border_radius: handle_border_radius, -                border_width: style.handle.border_width, -                border_color: style.handle.border_color, -            }, -            style.handle.color, -        ); +        draw( +            renderer, +            layout, +            cursor_position, +            &self.state, +            self.value, +            &self.range, +            self.style_sheet.as_ref(), +        )      }      fn mouse_interaction( @@ -362,16 +433,7 @@ where          _viewport: &Rectangle,          _renderer: &Renderer,      ) -> mouse::Interaction { -        let bounds = layout.bounds(); -        let is_mouse_over = bounds.contains(cursor_position); - -        if self.state.is_dragging { -            mouse::Interaction::Grabbing -        } else if is_mouse_over { -            mouse::Interaction::Grab -        } else { -            mouse::Interaction::default() -        } +        mouse_interaction(layout, cursor_position, &self.state)      }  } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e30e2343..d13d6ef1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -24,8 +24,6 @@ use crate::{      Shell, Size, Vector, Widget,  }; -use std::u32; -  pub use iced_style::text_input::{Style, StyleSheet};  /// A field that can be filled with text. @@ -61,10 +59,9 @@ pub struct TextInput<'a, Message, Renderer: text::Renderer> {      is_secure: bool,      font: Renderer::Font,      width: Length, -    max_width: u32,      padding: Padding,      size: Option<u16>, -    on_change: Box<dyn Fn(String) -> Message>, +    on_change: Box<dyn Fn(String) -> Message + 'a>,      on_submit: Option<Message>,      style_sheet: Box<dyn StyleSheet + 'a>,  } @@ -88,7 +85,7 @@ where          on_change: F,      ) -> Self      where -        F: 'static + Fn(String) -> Message, +        F: 'a + Fn(String) -> Message,      {          TextInput {              state, @@ -97,7 +94,6 @@ where              is_secure: false,              font: Default::default(),              width: Length::Fill, -            max_width: u32::MAX,              padding: Padding::ZERO,              size: None,              on_change: Box::new(on_change), @@ -126,12 +122,6 @@ where          self      } -    /// Sets the maximum width of the [`TextInput`]. -    pub fn max_width(mut self, max_width: u32) -> Self { -        self.max_width = max_width; -        self -    } -      /// Sets the [`Padding`] of the [`TextInput`].      pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {          self.padding = padding.into(); @@ -166,520 +156,313 @@ where      }  } -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +/// Computes the layout of a [`TextInput`]. +pub fn layout<Renderer>( +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    padding: Padding, +    size: Option<u16>, +) -> layout::Node  where      Renderer: text::Renderer,  { -    /// Draws the [`TextInput`] with the given [`Renderer`], overriding its -    /// [`Value`] if provided. -    pub fn draw( -        &self, -        renderer: &mut Renderer, -        layout: Layout<'_>, -        cursor_position: Point, -        value: Option<&Value>, -    ) { -        let value = value.unwrap_or(&self.value); -        let secure_value = self.is_secure.then(|| value.secure()); -        let value = secure_value.as_ref().unwrap_or(&value); +    let text_size = size.unwrap_or(renderer.default_size()); -        let bounds = layout.bounds(); -        let text_bounds = layout.children().next().unwrap().bounds(); +    let limits = limits +        .pad(padding) +        .width(width) +        .height(Length::Units(text_size)); -        let is_mouse_over = bounds.contains(cursor_position); +    let mut text = layout::Node::new(limits.resolve(Size::ZERO)); +    text.move_to(Point::new(padding.left.into(), padding.top.into())); -        let style = if self.state.is_focused() { -            self.style_sheet.focused() -        } else if is_mouse_over { -            self.style_sheet.hovered() -        } else { -            self.style_sheet.active() -        }; - -        renderer.fill_quad( -            renderer::Quad { -                bounds, -                border_radius: style.border_radius, -                border_width: style.border_width, -                border_color: style.border_color, -            }, -            style.background, -        ); - -        let text = value.to_string(); -        let size = self.size.unwrap_or(renderer.default_size()); - -        let (cursor, offset) = if self.state.is_focused() { -            match self.state.cursor.state(&value) { -                cursor::State::Index(position) => { -                    let (text_value_width, offset) = -                        measure_cursor_and_scroll_offset( -                            renderer, -                            text_bounds, -                            &value, -                            size, -                            position, -                            self.font.clone(), -                        ); - -                    ( -                        Some(( -                            renderer::Quad { -                                bounds: Rectangle { -                                    x: text_bounds.x + text_value_width, -                                    y: text_bounds.y, -                                    width: 1.0, -                                    height: text_bounds.height, -                                }, -                                border_radius: 0.0, -                                border_width: 0.0, -                                border_color: Color::TRANSPARENT, -                            }, -                            self.style_sheet.value_color(), -                        )), -                        offset, -                    ) -                } -                cursor::State::Selection { start, end } => { -                    let left = start.min(end); -                    let right = end.max(start); - -                    let (left_position, left_offset) = -                        measure_cursor_and_scroll_offset( -                            renderer, -                            text_bounds, -                            &value, -                            size, -                            left, -                            self.font.clone(), -                        ); - -                    let (right_position, right_offset) = -                        measure_cursor_and_scroll_offset( -                            renderer, -                            text_bounds, -                            &value, -                            size, -                            right, -                            self.font.clone(), -                        ); - -                    let width = right_position - left_position; - -                    ( -                        Some(( -                            renderer::Quad { -                                bounds: Rectangle { -                                    x: text_bounds.x + left_position, -                                    y: text_bounds.y, -                                    width, -                                    height: text_bounds.height, -                                }, -                                border_radius: 0.0, -                                border_width: 0.0, -                                border_color: Color::TRANSPARENT, -                            }, -                            self.style_sheet.selection_color(), -                        )), -                        if end == right { -                            right_offset -                        } else { -                            left_offset -                        }, -                    ) -                } -            } -        } else { -            (None, 0.0) -        }; - -        let text_width = renderer.measure_width( -            if text.is_empty() { -                &self.placeholder -            } else { -                &text -            }, -            size, -            self.font.clone(), -        ); - -        let render = |renderer: &mut Renderer| { -            if let Some((cursor, color)) = cursor { -                renderer.fill_quad(cursor, color); -            } - -            renderer.fill_text(Text { -                content: if text.is_empty() { -                    &self.placeholder -                } else { -                    &text -                }, -                color: if text.is_empty() { -                    self.style_sheet.placeholder_color() -                } else { -                    self.style_sheet.value_color() -                }, -                font: self.font.clone(), -                bounds: Rectangle { -                    y: text_bounds.center_y(), -                    width: f32::INFINITY, -                    ..text_bounds -                }, -                size: f32::from(size), -                horizontal_alignment: alignment::Horizontal::Left, -                vertical_alignment: alignment::Vertical::Center, -            }); -        }; - -        if text_width > text_bounds.width { -            renderer.with_layer(text_bounds, |renderer| { -                renderer.with_translation(Vector::new(-offset, 0.0), render) -            }); -        } else { -            render(renderer); -        } -    } +    layout::Node::with_children(text.size().pad(padding), vec![text])  } -impl<'a, Message, Renderer> Widget<Message, Renderer> -    for TextInput<'a, Message, Renderer> +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +pub fn update<'a, Message, Renderer>( +    event: Event, +    layout: Layout<'_>, +    cursor_position: Point, +    renderer: &Renderer, +    clipboard: &mut dyn Clipboard, +    shell: &mut Shell<'_, Message>, +    value: &mut Value, +    size: Option<u16>, +    font: &Renderer::Font, +    is_secure: bool, +    on_change: &dyn Fn(String) -> Message, +    on_submit: &Option<Message>, +    state: impl FnOnce() -> &'a mut State, +) -> event::Status  where      Message: Clone,      Renderer: text::Renderer,  { -    fn width(&self) -> Length { -        self.width -    } - -    fn height(&self) -> Length { -        Length::Shrink -    } - -    fn layout( -        &self, -        renderer: &Renderer, -        limits: &layout::Limits, -    ) -> layout::Node { -        let text_size = self.size.unwrap_or(renderer.default_size()); - -        let limits = limits -            .pad(self.padding) -            .width(self.width) -            .max_width(self.max_width) -            .height(Length::Units(text_size)); - -        let mut text = layout::Node::new(limits.resolve(Size::ZERO)); -        text.move_to(Point::new( -            self.padding.left.into(), -            self.padding.top.into(), -        )); - -        layout::Node::with_children(text.size().pad(self.padding), vec![text]) -    } - -    fn on_event( -        &mut self, -        event: Event, -        layout: Layout<'_>, -        cursor_position: Point, -        renderer: &Renderer, -        clipboard: &mut dyn Clipboard, -        shell: &mut Shell<'_, Message>, -    ) -> event::Status { -        match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerPressed { .. }) => { -                let is_clicked = layout.bounds().contains(cursor_position); - -                self.state.is_focused = is_clicked; - -                if is_clicked { -                    let text_layout = layout.children().next().unwrap(); -                    let target = cursor_position.x - text_layout.bounds().x; - -                    let click = mouse::Click::new( -                        cursor_position, -                        self.state.last_click, -                    ); - -                    match click.kind() { -                        click::Kind::Single => { -                            let position = if target > 0.0 { -                                let value = if self.is_secure { -                                    self.value.secure() -                                } else { -                                    self.value.clone() -                                }; - -                                find_cursor_position( -                                    renderer, -                                    text_layout.bounds(), -                                    self.font.clone(), -                                    self.size, -                                    &value, -                                    &self.state, -                                    target, -                                ) +    match event { +        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerPressed { .. }) => { +            let state = state(); +            let is_clicked = layout.bounds().contains(cursor_position); + +            state.is_focused = is_clicked; + +            if is_clicked { +                let text_layout = layout.children().next().unwrap(); +                let target = cursor_position.x - text_layout.bounds().x; + +                let click = +                    mouse::Click::new(cursor_position, state.last_click); + +                match click.kind() { +                    click::Kind::Single => { +                        let position = if target > 0.0 { +                            let value = if is_secure { +                                value.secure()                              } else { -                                None +                                value.clone()                              }; -                            self.state.cursor.move_to(position.unwrap_or(0)); -                            self.state.is_dragging = true; -                        } -                        click::Kind::Double => { -                            if self.is_secure { -                                self.state.cursor.select_all(&self.value); -                            } else { -                                let position = find_cursor_position( -                                    renderer, -                                    text_layout.bounds(), -                                    self.font.clone(), -                                    self.size, -                                    &self.value, -                                    &self.state, -                                    target, -                                ) -                                .unwrap_or(0); - -                                self.state.cursor.select_range( -                                    self.value.previous_start_of_word(position), -                                    self.value.next_end_of_word(position), -                                ); -                            } +                            find_cursor_position( +                                renderer, +                                text_layout.bounds(), +                                font.clone(), +                                size, +                                &value, +                                state, +                                target, +                            ) +                        } else { +                            None +                        }; -                            self.state.is_dragging = false; -                        } -                        click::Kind::Triple => { -                            self.state.cursor.select_all(&self.value); -                            self.state.is_dragging = false; +                        state.cursor.move_to(position.unwrap_or(0)); +                        state.is_dragging = true; +                    } +                    click::Kind::Double => { +                        if is_secure { +                            state.cursor.select_all(value); +                        } else { +                            let position = find_cursor_position( +                                renderer, +                                text_layout.bounds(), +                                font.clone(), +                                size, +                                value, +                                state, +                                target, +                            ) +                            .unwrap_or(0); + +                            state.cursor.select_range( +                                value.previous_start_of_word(position), +                                value.next_end_of_word(position), +                            );                          } + +                        state.is_dragging = false; +                    } +                    click::Kind::Triple => { +                        state.cursor.select_all(value); +                        state.is_dragging = false;                      } +                } -                    self.state.last_click = Some(click); +                state.last_click = Some(click); -                    return event::Status::Captured; -                } -            } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerLifted { .. }) -            | Event::Touch(touch::Event::FingerLost { .. }) => { -                self.state.is_dragging = false; +                return event::Status::Captured;              } -            Event::Mouse(mouse::Event::CursorMoved { position }) -            | Event::Touch(touch::Event::FingerMoved { position, .. }) => { -                if self.state.is_dragging { -                    let text_layout = layout.children().next().unwrap(); -                    let target = position.x - text_layout.bounds().x; - -                    let value = if self.is_secure { -                        self.value.secure() -                    } else { -                        self.value.clone() -                    }; +        } +        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerLifted { .. }) +        | Event::Touch(touch::Event::FingerLost { .. }) => { +            state().is_dragging = false; +        } +        Event::Mouse(mouse::Event::CursorMoved { position }) +        | Event::Touch(touch::Event::FingerMoved { position, .. }) => { +            let state = state(); -                    let position = find_cursor_position( -                        renderer, -                        text_layout.bounds(), -                        self.font.clone(), -                        self.size, -                        &value, -                        &self.state, -                        target, -                    ) -                    .unwrap_or(0); +            if state.is_dragging { +                let text_layout = layout.children().next().unwrap(); +                let target = position.x - text_layout.bounds().x; -                    self.state.cursor.select_range( -                        self.state.cursor.start(&value), -                        position, -                    ); +                let value = if is_secure { +                    value.secure() +                } else { +                    value.clone() +                }; + +                let position = find_cursor_position( +                    renderer, +                    text_layout.bounds(), +                    font.clone(), +                    size, +                    &value, +                    state, +                    target, +                ) +                .unwrap_or(0); + +                state +                    .cursor +                    .select_range(state.cursor.start(&value), position); -                    return event::Status::Captured; -                } +                return event::Status::Captured;              } -            Event::Keyboard(keyboard::Event::CharacterReceived(c)) -                if self.state.is_focused -                    && self.state.is_pasting.is_none() -                    && !self.state.keyboard_modifiers.command() -                    && !c.is_control() => +        } +        Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { +            let state = state(); + +            if state.is_focused +                && state.is_pasting.is_none() +                && !state.keyboard_modifiers.command() +                && !c.is_control()              { -                let mut editor = -                    Editor::new(&mut self.value, &mut self.state.cursor); +                let mut editor = Editor::new(value, &mut state.cursor);                  editor.insert(c); -                let message = (self.on_change)(editor.contents()); +                let message = (on_change)(editor.contents());                  shell.publish(message);                  return event::Status::Captured;              } -            Event::Keyboard(keyboard::Event::KeyPressed { -                key_code, .. -            }) if self.state.is_focused => { -                let modifiers = self.state.keyboard_modifiers; +        } +        Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { +            let state = state(); + +            if state.is_focused { +                let modifiers = state.keyboard_modifiers;                  match key_code {                      keyboard::KeyCode::Enter                      | keyboard::KeyCode::NumpadEnter => { -                        if let Some(on_submit) = self.on_submit.clone() { +                        if let Some(on_submit) = on_submit.clone() {                              shell.publish(on_submit);                          }                      }                      keyboard::KeyCode::Backspace => {                          if platform::is_jump_modifier_pressed(modifiers) -                            && self -                                .state -                                .cursor -                                .selection(&self.value) -                                .is_none() +                            && state.cursor.selection(value).is_none()                          { -                            if self.is_secure { -                                let cursor_pos = -                                    self.state.cursor.end(&self.value); -                                self.state.cursor.select_range(0, cursor_pos); +                            if is_secure { +                                let cursor_pos = state.cursor.end(value); +                                state.cursor.select_range(0, cursor_pos);                              } else { -                                self.state -                                    .cursor -                                    .select_left_by_words(&self.value); +                                state.cursor.select_left_by_words(value);                              }                          } -                        let mut editor = Editor::new( -                            &mut self.value, -                            &mut self.state.cursor, -                        ); - +                        let mut editor = Editor::new(value, &mut state.cursor);                          editor.backspace(); -                        let message = (self.on_change)(editor.contents()); +                        let message = (on_change)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::Delete => {                          if platform::is_jump_modifier_pressed(modifiers) -                            && self -                                .state -                                .cursor -                                .selection(&self.value) -                                .is_none() +                            && state.cursor.selection(value).is_none()                          { -                            if self.is_secure { -                                let cursor_pos = -                                    self.state.cursor.end(&self.value); -                                self.state +                            if is_secure { +                                let cursor_pos = state.cursor.end(value); +                                state                                      .cursor -                                    .select_range(cursor_pos, self.value.len()); +                                    .select_range(cursor_pos, value.len());                              } else { -                                self.state -                                    .cursor -                                    .select_right_by_words(&self.value); +                                state.cursor.select_right_by_words(value);                              }                          } -                        let mut editor = Editor::new( -                            &mut self.value, -                            &mut self.state.cursor, -                        ); - +                        let mut editor = Editor::new(value, &mut state.cursor);                          editor.delete(); -                        let message = (self.on_change)(editor.contents()); +                        let message = (on_change)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::Left => {                          if platform::is_jump_modifier_pressed(modifiers) -                            && !self.is_secure +                            && !is_secure                          {                              if modifiers.shift() { -                                self.state -                                    .cursor -                                    .select_left_by_words(&self.value); +                                state.cursor.select_left_by_words(value);                              } else { -                                self.state -                                    .cursor -                                    .move_left_by_words(&self.value); +                                state.cursor.move_left_by_words(value);                              }                          } else if modifiers.shift() { -                            self.state.cursor.select_left(&self.value) +                            state.cursor.select_left(value)                          } else { -                            self.state.cursor.move_left(&self.value); +                            state.cursor.move_left(value);                          }                      }                      keyboard::KeyCode::Right => {                          if platform::is_jump_modifier_pressed(modifiers) -                            && !self.is_secure +                            && !is_secure                          {                              if modifiers.shift() { -                                self.state -                                    .cursor -                                    .select_right_by_words(&self.value); +                                state.cursor.select_right_by_words(value);                              } else { -                                self.state -                                    .cursor -                                    .move_right_by_words(&self.value); +                                state.cursor.move_right_by_words(value);                              }                          } else if modifiers.shift() { -                            self.state.cursor.select_right(&self.value) +                            state.cursor.select_right(value)                          } else { -                            self.state.cursor.move_right(&self.value); +                            state.cursor.move_right(value);                          }                      }                      keyboard::KeyCode::Home => {                          if modifiers.shift() { -                            self.state.cursor.select_range( -                                self.state.cursor.start(&self.value), -                                0, -                            ); +                            state +                                .cursor +                                .select_range(state.cursor.start(value), 0);                          } else { -                            self.state.cursor.move_to(0); +                            state.cursor.move_to(0);                          }                      }                      keyboard::KeyCode::End => {                          if modifiers.shift() { -                            self.state.cursor.select_range( -                                self.state.cursor.start(&self.value), -                                self.value.len(), +                            state.cursor.select_range( +                                state.cursor.start(value), +                                value.len(),                              );                          } else { -                            self.state.cursor.move_to(self.value.len()); +                            state.cursor.move_to(value.len());                          }                      }                      keyboard::KeyCode::C -                        if self.state.keyboard_modifiers.command() => +                        if state.keyboard_modifiers.command() =>                      { -                        match self.state.cursor.selection(&self.value) { +                        match state.cursor.selection(value) {                              Some((start, end)) => {                                  clipboard.write( -                                    self.value.select(start, end).to_string(), +                                    value.select(start, end).to_string(),                                  );                              }                              None => {}                          }                      }                      keyboard::KeyCode::X -                        if self.state.keyboard_modifiers.command() => +                        if state.keyboard_modifiers.command() =>                      { -                        match self.state.cursor.selection(&self.value) { +                        match state.cursor.selection(value) {                              Some((start, end)) => {                                  clipboard.write( -                                    self.value.select(start, end).to_string(), +                                    value.select(start, end).to_string(),                                  );                              }                              None => {}                          } -                        let mut editor = Editor::new( -                            &mut self.value, -                            &mut self.state.cursor, -                        ); - +                        let mut editor = Editor::new(value, &mut state.cursor);                          editor.delete(); -                        let message = (self.on_change)(editor.contents()); +                        let message = (on_change)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::V => { -                        if self.state.keyboard_modifiers.command() { -                            let content = match self.state.is_pasting.take() { +                        if state.keyboard_modifiers.command() { +                            let content = match state.is_pasting.take() {                                  Some(content) => content,                                  None => {                                      let content: String = clipboard @@ -693,32 +476,30 @@ where                                  }                              }; -                            let mut editor = Editor::new( -                                &mut self.value, -                                &mut self.state.cursor, -                            ); +                            let mut editor = +                                Editor::new(value, &mut state.cursor);                              editor.paste(content.clone()); -                            let message = (self.on_change)(editor.contents()); +                            let message = (on_change)(editor.contents());                              shell.publish(message); -                            self.state.is_pasting = Some(content); +                            state.is_pasting = Some(content);                          } else { -                            self.state.is_pasting = None; +                            state.is_pasting = None;                          }                      }                      keyboard::KeyCode::A -                        if self.state.keyboard_modifiers.command() => +                        if state.keyboard_modifiers.command() =>                      { -                        self.state.cursor.select_all(&self.value); +                        state.cursor.select_all(value);                      }                      keyboard::KeyCode::Escape => { -                        self.state.is_focused = false; -                        self.state.is_dragging = false; -                        self.state.is_pasting = None; +                        state.is_focused = false; +                        state.is_dragging = false; +                        state.is_pasting = None; -                        self.state.keyboard_modifiers = +                        state.keyboard_modifiers =                              keyboard::Modifiers::default();                      }                      keyboard::KeyCode::Tab @@ -728,15 +509,17 @@ where                      }                      _ => {}                  } - -                return event::Status::Captured;              } -            Event::Keyboard(keyboard::Event::KeyReleased { -                key_code, .. -            }) if self.state.is_focused => { + +            return event::Status::Captured; +        } +        Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { +            let state = state(); + +            if state.is_focused {                  match key_code {                      keyboard::KeyCode::V => { -                        self.state.is_pasting = None; +                        state.is_pasting = None;                      }                      keyboard::KeyCode::Tab                      | keyboard::KeyCode::Up @@ -748,15 +531,246 @@ where                  return event::Status::Captured;              } -            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) -                if self.state.is_focused => -            { -                self.state.keyboard_modifiers = modifiers; +        } +        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { +            let state = state(); + +            if state.is_focused { +                state.keyboard_modifiers = modifiers; +            } +        } +        _ => {} +    } + +    event::Status::Ignored +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +pub fn draw<Renderer>( +    renderer: &mut Renderer, +    layout: Layout<'_>, +    cursor_position: Point, +    state: &State, +    value: &Value, +    placeholder: &str, +    size: Option<u16>, +    font: &Renderer::Font, +    is_secure: bool, +    style_sheet: &dyn StyleSheet, +) where +    Renderer: text::Renderer, +{ +    let secure_value = is_secure.then(|| value.secure()); +    let value = secure_value.as_ref().unwrap_or(&value); + +    let bounds = layout.bounds(); +    let text_bounds = layout.children().next().unwrap().bounds(); + +    let is_mouse_over = bounds.contains(cursor_position); + +    let style = if state.is_focused() { +        style_sheet.focused() +    } else if is_mouse_over { +        style_sheet.hovered() +    } else { +        style_sheet.active() +    }; + +    renderer.fill_quad( +        renderer::Quad { +            bounds, +            border_radius: style.border_radius, +            border_width: style.border_width, +            border_color: style.border_color, +        }, +        style.background, +    ); + +    let text = value.to_string(); +    let size = size.unwrap_or(renderer.default_size()); + +    let (cursor, offset) = if state.is_focused() { +        match state.cursor.state(&value) { +            cursor::State::Index(position) => { +                let (text_value_width, offset) = +                    measure_cursor_and_scroll_offset( +                        renderer, +                        text_bounds, +                        &value, +                        size, +                        position, +                        font.clone(), +                    ); + +                ( +                    Some(( +                        renderer::Quad { +                            bounds: Rectangle { +                                x: text_bounds.x + text_value_width, +                                y: text_bounds.y, +                                width: 1.0, +                                height: text_bounds.height, +                            }, +                            border_radius: 0.0, +                            border_width: 0.0, +                            border_color: Color::TRANSPARENT, +                        }, +                        style_sheet.value_color(), +                    )), +                    offset, +                ) +            } +            cursor::State::Selection { start, end } => { +                let left = start.min(end); +                let right = end.max(start); + +                let (left_position, left_offset) = +                    measure_cursor_and_scroll_offset( +                        renderer, +                        text_bounds, +                        &value, +                        size, +                        left, +                        font.clone(), +                    ); + +                let (right_position, right_offset) = +                    measure_cursor_and_scroll_offset( +                        renderer, +                        text_bounds, +                        &value, +                        size, +                        right, +                        font.clone(), +                    ); + +                let width = right_position - left_position; + +                ( +                    Some(( +                        renderer::Quad { +                            bounds: Rectangle { +                                x: text_bounds.x + left_position, +                                y: text_bounds.y, +                                width, +                                height: text_bounds.height, +                            }, +                            border_radius: 0.0, +                            border_width: 0.0, +                            border_color: Color::TRANSPARENT, +                        }, +                        style_sheet.selection_color(), +                    )), +                    if end == right { +                        right_offset +                    } else { +                        left_offset +                    }, +                )              } -            _ => {}          } +    } else { +        (None, 0.0) +    }; + +    let text_width = renderer.measure_width( +        if text.is_empty() { placeholder } else { &text }, +        size, +        font.clone(), +    ); + +    let render = |renderer: &mut Renderer| { +        if let Some((cursor, color)) = cursor { +            renderer.fill_quad(cursor, color); +        } + +        renderer.fill_text(Text { +            content: if text.is_empty() { placeholder } else { &text }, +            color: if text.is_empty() { +                style_sheet.placeholder_color() +            } else { +                style_sheet.value_color() +            }, +            font: font.clone(), +            bounds: Rectangle { +                y: text_bounds.center_y(), +                width: f32::INFINITY, +                ..text_bounds +            }, +            size: f32::from(size), +            horizontal_alignment: alignment::Horizontal::Left, +            vertical_alignment: alignment::Vertical::Center, +        }); +    }; + +    if text_width > text_bounds.width { +        renderer.with_layer(text_bounds, |renderer| { +            renderer.with_translation(Vector::new(-offset, 0.0), render) +        }); +    } else { +        render(renderer); +    } +} -        event::Status::Ignored +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +pub fn mouse_interaction( +    layout: Layout<'_>, +    cursor_position: Point, +) -> mouse::Interaction { +    if layout.bounds().contains(cursor_position) { +        mouse::Interaction::Text +    } else { +        mouse::Interaction::default() +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for TextInput<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: text::Renderer, +{ +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        layout(renderer, limits, self.width, self.padding, self.size) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        update( +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +            &mut self.value, +            self.size, +            &self.font, +            self.is_secure, +            self.on_change.as_ref(), +            &self.on_submit, +            || &mut self.state, +        )      }      fn mouse_interaction( @@ -766,11 +780,7 @@ where          _viewport: &Rectangle,          _renderer: &Renderer,      ) -> mouse::Interaction { -        if layout.bounds().contains(cursor_position) { -            mouse::Interaction::Text -        } else { -            mouse::Interaction::default() -        } +        mouse_interaction(layout, cursor_position)      }      fn draw( @@ -781,7 +791,18 @@ where          cursor_position: Point,          _viewport: &Rectangle,      ) { -        self.draw(renderer, layout, cursor_position, None) +        draw( +            renderer, +            layout, +            cursor_position, +            &self.state, +            &self.value, +            &self.placeholder, +            self.size, +            &self.font, +            self.is_secure, +            self.style_sheet.as_ref(), +        )      }  } diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 48237edb..a7847871 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -32,7 +32,7 @@ pub use iced_style::toggler::{Style, StyleSheet};  #[allow(missing_debug_implementations)]  pub struct Toggler<'a, Message, Renderer: text::Renderer> {      is_active: bool, -    on_toggle: Box<dyn Fn(bool) -> Message>, +    on_toggle: Box<dyn Fn(bool) -> Message + 'a>,      label: Option<String>,      width: Length,      size: u16, @@ -61,7 +61,7 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> {          f: F,      ) -> Self      where -        F: 'static + Fn(bool) -> Message, +        F: 'a + Fn(bool) -> Message,      {          Toggler {              is_active, diff --git a/pure/Cargo.toml b/pure/Cargo.toml new file mode 100644 index 00000000..317dccdf --- /dev/null +++ b/pure/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "iced_pure" +version = "0.1.0" +edition = "2021" + +[dependencies] +iced_native = { version = "0.4", path = "../native" } +iced_style = { version = "0.3", path = "../style" } +num-traits = "0.2" diff --git a/pure/src/flex.rs b/pure/src/flex.rs new file mode 100644 index 00000000..8d473f08 --- /dev/null +++ b/pure/src/flex.rs @@ -0,0 +1,232 @@ +//! Distribute elements using a flex-based layout. +// This code is heavily inspired by the [`druid`] codebase. +// +// [`druid`]: https://github.com/xi-editor/druid +// +// Copyright 2018 The xi-editor Authors, Héctor Ramón +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::Element; + +use iced_native::layout::{Limits, Node}; +use iced_native::{Alignment, Padding, Point, Size}; + +/// The main axis of a flex layout. +#[derive(Debug)] +pub enum Axis { +    /// The horizontal axis +    Horizontal, + +    /// The vertical axis +    Vertical, +} + +impl Axis { +    fn main(&self, size: Size) -> f32 { +        match self { +            Axis::Horizontal => size.width, +            Axis::Vertical => size.height, +        } +    } + +    fn cross(&self, size: Size) -> f32 { +        match self { +            Axis::Horizontal => size.height, +            Axis::Vertical => size.width, +        } +    } + +    fn pack(&self, main: f32, cross: f32) -> (f32, f32) { +        match self { +            Axis::Horizontal => (main, cross), +            Axis::Vertical => (cross, main), +        } +    } +} + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve<Message, Renderer>( +    axis: Axis, +    renderer: &Renderer, +    limits: &Limits, +    padding: Padding, +    spacing: f32, +    align_items: Alignment, +    items: &[Element<Message, Renderer>], +) -> Node +where +    Renderer: iced_native::Renderer, +{ +    let limits = limits.pad(padding); +    let total_spacing = spacing * items.len().saturating_sub(1) as f32; +    let max_cross = axis.cross(limits.max()); + +    let mut fill_sum = 0; +    let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); +    let mut available = axis.main(limits.max()) - total_spacing; + +    let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); +    nodes.resize(items.len(), Node::default()); + +    if align_items == Alignment::Fill { +        let mut fill_cross = axis.cross(limits.min()); + +        items.iter().for_each(|child| { +            let cross_fill_factor = match axis { +                Axis::Horizontal => child.as_widget().height(), +                Axis::Vertical => child.as_widget().width(), +            } +            .fill_factor(); + +            if cross_fill_factor == 0 { +                let (max_width, max_height) = axis.pack(available, max_cross); + +                let child_limits = +                    Limits::new(Size::ZERO, Size::new(max_width, max_height)); + +                let layout = child.as_widget().layout(renderer, &child_limits); +                let size = layout.size(); + +                fill_cross = fill_cross.max(axis.cross(size)); +            } +        }); + +        cross = fill_cross; +    } + +    for (i, child) in items.iter().enumerate() { +        let fill_factor = match axis { +            Axis::Horizontal => child.as_widget().width(), +            Axis::Vertical => child.as_widget().height(), +        } +        .fill_factor(); + +        if fill_factor == 0 { +            let (min_width, min_height) = if align_items == Alignment::Fill { +                axis.pack(0.0, cross) +            } else { +                axis.pack(0.0, 0.0) +            }; + +            let (max_width, max_height) = if align_items == Alignment::Fill { +                axis.pack(available, cross) +            } else { +                axis.pack(available, max_cross) +            }; + +            let child_limits = Limits::new( +                Size::new(min_width, min_height), +                Size::new(max_width, max_height), +            ); + +            let layout = child.as_widget().layout(renderer, &child_limits); +            let size = layout.size(); + +            available -= axis.main(size); + +            if align_items != Alignment::Fill { +                cross = cross.max(axis.cross(size)); +            } + +            nodes[i] = layout; +        } else { +            fill_sum += fill_factor; +        } +    } + +    let remaining = available.max(0.0); + +    for (i, child) in items.iter().enumerate() { +        let fill_factor = match axis { +            Axis::Horizontal => child.as_widget().width(), +            Axis::Vertical => child.as_widget().height(), +        } +        .fill_factor(); + +        if fill_factor != 0 { +            let max_main = remaining * fill_factor as f32 / fill_sum as f32; +            let min_main = if max_main.is_infinite() { +                0.0 +            } else { +                max_main +            }; + +            let (min_width, min_height) = if align_items == Alignment::Fill { +                axis.pack(min_main, cross) +            } else { +                axis.pack(min_main, axis.cross(limits.min())) +            }; + +            let (max_width, max_height) = if align_items == Alignment::Fill { +                axis.pack(max_main, cross) +            } else { +                axis.pack(max_main, max_cross) +            }; + +            let child_limits = Limits::new( +                Size::new(min_width, min_height), +                Size::new(max_width, max_height), +            ); + +            let layout = child.as_widget().layout(renderer, &child_limits); + +            if align_items != Alignment::Fill { +                cross = cross.max(axis.cross(layout.size())); +            } + +            nodes[i] = layout; +        } +    } + +    let pad = axis.pack(padding.left as f32, padding.top as f32); +    let mut main = pad.0; + +    for (i, node) in nodes.iter_mut().enumerate() { +        if i > 0 { +            main += spacing; +        } + +        let (x, y) = axis.pack(main, pad.1); + +        node.move_to(Point::new(x, y)); + +        match axis { +            Axis::Horizontal => { +                node.align( +                    Alignment::Start, +                    align_items, +                    Size::new(0.0, cross), +                ); +            } +            Axis::Vertical => { +                node.align( +                    align_items, +                    Alignment::Start, +                    Size::new(cross, 0.0), +                ); +            } +        } + +        let size = node.size(); + +        main += axis.main(size); +    } + +    let (width, height) = axis.pack(main - pad.0, cross); +    let size = limits.resolve(Size::new(width, height)); + +    Node::with_children(size.pad(padding), nodes) +} diff --git a/pure/src/lib.rs b/pure/src/lib.rs new file mode 100644 index 00000000..1b51d55b --- /dev/null +++ b/pure/src/lib.rs @@ -0,0 +1,152 @@ +pub mod overlay; +pub mod widget; + +pub(crate) mod flex; + +pub use widget::*; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub struct Pure<'a, Message, Renderer> { +    state: &'a mut State, +    element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Pure<'a, Message, Renderer> +where +    Message: 'static, +    Renderer: iced_native::Renderer + 'static, +{ +    pub fn new( +        state: &'a mut State, +        content: impl Into<Element<'a, Message, Renderer>>, +    ) -> Self { +        let element = content.into(); +        let _ = state.diff(&element); + +        Self { state, element } +    } +} + +pub struct State { +    state_tree: widget::Tree, +} + +impl State { +    pub fn new() -> Self { +        Self { +            state_tree: widget::Tree::empty(), +        } +    } + +    fn diff<Message, Renderer>( +        &mut self, +        new_element: &Element<Message, Renderer>, +    ) { +        self.state_tree.diff(new_element); +    } +} + +impl<'a, Message, Renderer> iced_native::Widget<Message, Renderer> +    for Pure<'a, Message, Renderer> +where +    Message: 'a, +    Renderer: iced_native::Renderer + 'a, +{ +    fn width(&self) -> Length { +        self.element.as_widget().width() +    } + +    fn height(&self) -> Length { +        self.element.as_widget().height() +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        self.element.as_widget().layout(renderer, limits) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        self.element.as_widget_mut().on_event( +            &mut self.state.state_tree, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        self.element.as_widget().draw( +            &self.state.state_tree, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.element.as_widget().mouse_interaction( +            &self.state.state_tree, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.element.as_widget_mut().overlay( +            &mut self.state.state_tree, +            layout, +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> Into<iced_native::Element<'a, Message, Renderer>> +    for Pure<'a, Message, Renderer> +where +    Message: 'a, +    Renderer: iced_native::Renderer + 'a, +{ +    fn into(self) -> iced_native::Element<'a, Message, Renderer> { +        iced_native::Element::new(self) +    } +} diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs new file mode 100644 index 00000000..72415634 --- /dev/null +++ b/pure/src/overlay.rs @@ -0,0 +1,21 @@ +use crate::Tree; + +use iced_native::Layout; + +pub use iced_native::overlay::*; + +pub fn from_children<'a, Message, Renderer>( +    children: &'a [crate::Element<'_, Message, Renderer>], +    tree: &'a mut Tree, +    layout: Layout<'_>, +    renderer: &Renderer, +) -> Option<Element<'a, Message, Renderer>> { +    children +        .iter() +        .zip(&mut tree.children) +        .zip(layout.children()) +        .filter_map(|((child, state), layout)| { +            child.as_widget().overlay(state, layout, renderer) +        }) +        .next() +} diff --git a/pure/src/widget.rs b/pure/src/widget.rs new file mode 100644 index 00000000..c516c1f2 --- /dev/null +++ b/pure/src/widget.rs @@ -0,0 +1,236 @@ +pub mod image; +pub mod tree; + +mod button; +mod checkbox; +mod column; +mod container; +mod element; +mod pick_list; +mod radio; +mod row; +mod scrollable; +mod slider; +mod space; +mod text; +mod text_input; +mod toggler; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use column::Column; +pub use container::Container; +pub use element::Element; +pub use image::Image; +pub use pick_list::PickList; +pub use radio::Radio; +pub use row::Row; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use space::Space; +pub use text::Text; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tree::Tree; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +use std::borrow::Cow; + +pub trait Widget<Message, Renderer> { +    fn width(&self) -> Length; + +    fn height(&self) -> Length; + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node; + +    fn draw( +        &self, +        state: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ); + +    fn tag(&self) -> tree::Tag { +        tree::Tag::stateless() +    } + +    fn state(&self) -> tree::State { +        tree::State::None +    } + +    fn children(&self) -> Vec<Tree> { +        Vec::new() +    } + +    fn diff(&self, _tree: &mut Tree) {} + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        _layout: Layout<'_>, +        _cursor_position: Point, +        _viewport: &Rectangle, +        _renderer: &Renderer, +    ) -> mouse::Interaction { +        mouse::Interaction::Idle +    } + +    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>, +    ) -> event::Status { +        event::Status::Ignored +    } + +    fn overlay<'a>( +        &'a self, +        _state: &'a mut Tree, +        _layout: Layout<'_>, +        _renderer: &Renderer, +    ) -> Option<overlay::Element<'a, Message, Renderer>> { +        None +    } +} + +pub fn container<'a, Message, Renderer>( +    content: impl Into<Element<'a, Message, Renderer>>, +) -> Container<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    Container::new(content) +} + +pub fn column<'a, Message, Renderer>() -> Column<'a, Message, Renderer> { +    Column::new() +} + +pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> { +    Row::new() +} + +pub fn scrollable<'a, Message, Renderer>( +    content: impl Into<Element<'a, Message, Renderer>>, +) -> Scrollable<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    Scrollable::new(content) +} + +pub fn button<'a, Message, Renderer>( +    content: impl Into<Element<'a, Message, Renderer>>, +) -> Button<'a, Message, Renderer> { +    Button::new(content) +} + +pub fn text<Renderer>(text: impl Into<String>) -> Text<Renderer> +where +    Renderer: iced_native::text::Renderer, +{ +    Text::new(text) +} + +pub fn checkbox<'a, Message, Renderer>( +    label: impl Into<String>, +    is_checked: bool, +    f: impl Fn(bool) -> Message + 'a, +) -> Checkbox<'a, Message, Renderer> +where +    Renderer: iced_native::text::Renderer, +{ +    Checkbox::new(is_checked, label, f) +} + +pub fn radio<'a, Message, Renderer, V>( +    label: impl Into<String>, +    value: V, +    selected: Option<V>, +    on_click: impl FnOnce(V) -> Message, +) -> Radio<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: iced_native::text::Renderer, +    V: Copy + Eq, +{ +    Radio::new(value, label, selected, on_click) +} + +pub fn toggler<'a, Message, Renderer>( +    label: impl Into<Option<String>>, +    is_checked: bool, +    f: impl Fn(bool) -> Message + 'a, +) -> Toggler<'a, Message, Renderer> +where +    Renderer: iced_native::text::Renderer, +{ +    Toggler::new(is_checked, label, f) +} + +pub fn text_input<'a, Message, Renderer>( +    placeholder: &str, +    value: &str, +    on_change: impl Fn(String) -> Message + 'a, +) -> TextInput<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: iced_native::text::Renderer, +{ +    TextInput::new(placeholder, value, on_change) +} + +pub fn slider<'a, Message, T>( +    range: std::ops::RangeInclusive<T>, +    value: T, +    on_change: impl Fn(T) -> Message + 'a, +) -> Slider<'a, T, Message> +where +    Message: Clone, +    T: Copy + From<u8> + std::cmp::PartialOrd, +{ +    Slider::new(range, value, on_change) +} + +pub fn pick_list<'a, Message, Renderer, T>( +    options: impl Into<Cow<'a, [T]>>, +    selected: Option<T>, +    on_selected: impl Fn(T) -> Message + 'a, +) -> PickList<'a, T, Message, Renderer> +where +    T: ToString + Eq + 'static, +    [T]: ToOwned<Owned = Vec<T>>, +    Renderer: iced_native::text::Renderer, +{ +    PickList::new(options, selected, on_selected) +} + +pub fn image<Handle>(handle: impl Into<Handle>) -> Image<Handle> { +    Image::new(handle.into()) +} + +pub fn horizontal_space(width: Length) -> Space { +    Space::with_width(width) +} + +pub fn vertical_space(height: Length) -> Space { +    Space::with_height(height) +} diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs new file mode 100644 index 00000000..4380b608 --- /dev/null +++ b/pure/src/widget/button.rs @@ -0,0 +1,224 @@ +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::widget::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::button; +use iced_native::{ +    Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; +use iced_style::button::StyleSheet; + +pub use button::State; + +pub struct Button<'a, Message, Renderer> { +    content: Element<'a, Message, Renderer>, +    on_press: Option<Message>, +    style_sheet: Box<dyn StyleSheet + 'a>, +    width: Length, +    height: Length, +    padding: Padding, +} + +impl<'a, Message, Renderer> Button<'a, Message, Renderer> { +    pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { +        Button { +            content: content.into(), +            on_press: None, +            style_sheet: Default::default(), +            width: Length::Shrink, +            height: Length::Shrink, +            padding: Padding::new(5), +        } +    } + +    /// Sets the width of the [`Button`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`Button`]. +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the [`Padding`] of the [`Button`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    /// Sets the message that will be produced when the [`Button`] is pressed. +    /// +    /// Unless `on_press` is called, the [`Button`] will be disabled. +    pub fn on_press(mut self, msg: Message) -> Self { +        self.on_press = Some(msg); +        self +    } + +    /// Sets the style of the [`Button`]. +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Button<'a, Message, Renderer> +where +    Message: 'static + Clone, +    Renderer: 'static + iced_native::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<State>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(State::new()) +    } + +    fn children(&self) -> Vec<Tree> { +        vec![Tree::new(&self.content)] +    } + +    fn diff(&self, tree: &mut Tree) { +        tree.diff_children(std::slice::from_ref(&self.content)) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        button::layout( +            renderer, +            limits, +            self.width, +            self.height, +            self.padding, +            |renderer, limits| { +                self.content.as_widget().layout(renderer, &limits) +            }, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        if let event::Status::Captured = self.content.as_widget_mut().on_event( +            &mut tree.children[0], +            event.clone(), +            layout.children().next().unwrap(), +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) { +            return event::Status::Captured; +        } + +        button::update( +            event, +            layout, +            cursor_position, +            shell, +            &self.on_press, +            || tree.state.downcast_mut::<State>(), +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        _style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) { +        let bounds = layout.bounds(); +        let content_layout = layout.children().next().unwrap(); + +        let styling = button::draw( +            renderer, +            bounds, +            cursor_position, +            self.on_press.is_some(), +            self.style_sheet.as_ref(), +            || tree.state.downcast_ref::<State>(), +        ); + +        self.content.as_widget().draw( +            &tree.children[0], +            renderer, +            &renderer::Style { +                text_color: styling.text_color, +            }, +            content_layout, +            cursor_position, +            &bounds, +        ); +    } + +    fn mouse_interaction( +        &self, +        _tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +        _renderer: &Renderer, +    ) -> mouse::Interaction { +        button::mouse_interaction( +            layout, +            cursor_position, +            self.on_press.is_some(), +        ) +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        self.content.as_widget().overlay( +            &mut tree.children[0], +            layout.children().next().unwrap(), +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Button<'a, Message, Renderer> +where +    Message: Clone + 'static, +    Renderer: iced_native::Renderer + 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs new file mode 100644 index 00000000..3448e616 --- /dev/null +++ b/pure/src/widget/checkbox.rs @@ -0,0 +1,102 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::Checkbox; + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Checkbox<'a, Message, Renderer> +where +    Renderer: text::Renderer, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    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>, +    ) -> event::Status { +        <Self as iced_native::Widget<Message, Renderer>>::on_event( +            self, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( +            self, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Checkbox<'a, Message, Renderer> +where +    Message: 'a, +    Renderer: text::Renderer + 'a, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs new file mode 100644 index 00000000..37ff96c5 --- /dev/null +++ b/pure/src/widget/column.rs @@ -0,0 +1,224 @@ +use crate::flex; +use crate::overlay; +use crate::widget::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{ +    Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, +}; + +use std::u32; + +pub struct Column<'a, Message, Renderer> { +    spacing: u16, +    padding: Padding, +    width: Length, +    height: Length, +    max_width: u32, +    align_items: Alignment, +    children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Column<'a, Message, Renderer> { +    pub fn new() -> Self { +        Self::with_children(Vec::new()) +    } + +    pub fn with_children( +        children: Vec<Element<'a, Message, Renderer>>, +    ) -> Self { +        Column { +            spacing: 0, +            padding: Padding::ZERO, +            width: Length::Shrink, +            height: Length::Shrink, +            max_width: u32::MAX, +            align_items: Alignment::Start, +            children, +        } +    } + +    pub fn spacing(mut self, units: u16) -> Self { +        self.spacing = units; +        self +    } + +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the maximum width of the [`Column`]. +    pub fn max_width(mut self, max_width: u32) -> Self { +        self.max_width = max_width; +        self +    } + +    pub fn align_items(mut self, align: Alignment) -> Self { +        self.align_items = align; +        self +    } + +    pub fn push( +        mut self, +        child: impl Into<Element<'a, Message, Renderer>>, +    ) -> Self { +        self.children.push(child.into()); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Column<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    fn children(&self) -> Vec<Tree> { +        self.children.iter().map(Tree::new).collect() +    } + +    fn diff(&self, tree: &mut Tree) { +        tree.diff_children(&self.children); +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let limits = limits +            .max_width(self.max_width) +            .width(self.width) +            .height(self.height); + +        flex::resolve( +            flex::Axis::Vertical, +            renderer, +            &limits, +            self.padding, +            self.spacing as f32, +            self.align_items, +            &self.children, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        self.children +            .iter_mut() +            .zip(&mut tree.children) +            .zip(layout.children()) +            .map(|((child, state), layout)| { +                child.as_widget_mut().on_event( +                    state, +                    event.clone(), +                    layout, +                    cursor_position, +                    renderer, +                    clipboard, +                    shell, +                ) +            }) +            .fold(event::Status::Ignored, event::Status::merge) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.children +            .iter() +            .zip(&tree.children) +            .zip(layout.children()) +            .map(|((child, state), layout)| { +                child.as_widget().mouse_interaction( +                    state, +                    layout, +                    cursor_position, +                    viewport, +                    renderer, +                ) +            }) +            .max() +            .unwrap_or_default() +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        for ((child, state), layout) in self +            .children +            .iter() +            .zip(&tree.children) +            .zip(layout.children()) +        { +            child.as_widget().draw( +                state, +                renderer, +                style, +                layout, +                cursor_position, +                viewport, +            ); +        } +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        overlay::from_children(&self.children, tree, layout, renderer) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Column<'a, Message, Renderer> +where +    Message: 'static, +    Renderer: iced_native::Renderer + 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs new file mode 100644 index 00000000..ebf69cab --- /dev/null +++ b/pure/src/widget/container.rs @@ -0,0 +1,251 @@ +//! Decorate content and apply alignment. +use crate::{Element, Tree, Widget}; + +use iced_native::alignment; +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::{ +    Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::u32; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct Container<'a, Message, Renderer> { +    padding: Padding, +    width: Length, +    height: Length, +    max_width: u32, +    max_height: u32, +    horizontal_alignment: alignment::Horizontal, +    vertical_alignment: alignment::Vertical, +    style_sheet: Box<dyn StyleSheet + 'a>, +    content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Container<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    /// Creates an empty [`Container`]. +    pub fn new<T>(content: T) -> Self +    where +        T: Into<Element<'a, Message, Renderer>>, +    { +        Container { +            padding: Padding::ZERO, +            width: Length::Shrink, +            height: Length::Shrink, +            max_width: u32::MAX, +            max_height: u32::MAX, +            horizontal_alignment: alignment::Horizontal::Left, +            vertical_alignment: alignment::Vertical::Top, +            style_sheet: Default::default(), +            content: content.into(), +        } +    } + +    /// Sets the [`Padding`] of the [`Container`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    /// Sets the width of the [`Container`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`Container`]. +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the maximum width of the [`Container`]. +    pub fn max_width(mut self, max_width: u32) -> Self { +        self.max_width = max_width; +        self +    } + +    /// Sets the maximum height of the [`Container`] in pixels. +    pub fn max_height(mut self, max_height: u32) -> Self { +        self.max_height = max_height; +        self +    } + +    /// Sets the content alignment for the horizontal axis of the [`Container`]. +    pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { +        self.horizontal_alignment = alignment; +        self +    } + +    /// Sets the content alignment for the vertical axis of the [`Container`]. +    pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { +        self.vertical_alignment = alignment; +        self +    } + +    /// Centers the contents in the horizontal axis of the [`Container`]. +    pub fn center_x(mut self) -> Self { +        self.horizontal_alignment = alignment::Horizontal::Center; +        self +    } + +    /// Centers the contents in the vertical axis of the [`Container`]. +    pub fn center_y(mut self) -> Self { +        self.vertical_alignment = alignment::Vertical::Center; +        self +    } + +    /// Sets the style of the [`Container`]. +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Container<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    fn children(&self) -> Vec<Tree> { +        vec![Tree::new(&self.content)] +    } + +    fn diff(&self, tree: &mut Tree) { +        tree.diff_children(std::slice::from_ref(&self.content)) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        container::layout( +            renderer, +            limits, +            self.width, +            self.height, +            self.padding, +            self.horizontal_alignment, +            self.vertical_alignment, +            |renderer, limits| { +                self.content.as_widget().layout(renderer, limits) +            }, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        self.content.as_widget_mut().on_event( +            &mut tree.children[0], +            event, +            layout.children().next().unwrap(), +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.content.as_widget().mouse_interaction( +            &tree.children[0], +            layout.children().next().unwrap(), +            cursor_position, +            viewport, +            renderer, +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        renderer_style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        let style = self.style_sheet.style(); + +        container::draw_background(renderer, &style, layout.bounds()); + +        self.content.as_widget().draw( +            &tree.children[0], +            renderer, +            &renderer::Style { +                text_color: style +                    .text_color +                    .unwrap_or(renderer_style.text_color), +            }, +            layout.children().next().unwrap(), +            cursor_position, +            viewport, +        ); +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        self.content.as_widget().overlay( +            &mut tree.children[0], +            layout.children().next().unwrap(), +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Renderer: 'a + iced_native::Renderer, +    Message: 'a, +{ +    fn from( +        column: Container<'a, Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(column) +    } +} diff --git a/pure/src/widget/element.rs b/pure/src/widget/element.rs new file mode 100644 index 00000000..3d5697fe --- /dev/null +++ b/pure/src/widget/element.rs @@ -0,0 +1,163 @@ +use crate::widget::tree::{self, Tree}; +use crate::widget::Widget; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub struct Element<'a, Message, Renderer> { +    widget: Box<dyn Widget<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { +    pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self { +        Self { +            widget: Box::new(widget), +        } +    } + +    pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> { +        self.widget.as_ref() +    } + +    pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> { +        self.widget.as_mut() +    } + +    pub fn map<B>( +        self, +        f: impl Fn(Message) -> B + 'a, +    ) -> Element<'a, B, Renderer> +    where +        Message: 'a, +        Renderer: iced_native::Renderer + 'a, +        B: 'a, +    { +        Element::new(Map::new(self.widget, f)) +    } +} + +struct Map<'a, A, B, Renderer> { +    widget: Box<dyn Widget<A, Renderer> + 'a>, +    mapper: Box<dyn Fn(A) -> B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { +    pub fn new<F>( +        widget: Box<dyn Widget<A, Renderer> + 'a>, +        mapper: F, +    ) -> Map<'a, A, B, Renderer> +    where +        F: 'a + Fn(A) -> B, +    { +        Map { +            widget, +            mapper: Box::new(mapper), +        } +    } +} + +impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> +where +    Renderer: iced_native::Renderer + 'a, +    A: 'a, +    B: 'a, +{ +    fn tag(&self) -> tree::Tag { +        self.widget.tag() +    } + +    fn state(&self) -> tree::State { +        self.widget.state() +    } + +    fn children(&self) -> Vec<Tree> { +        self.widget.children() +    } + +    fn diff(&self, tree: &mut Tree) { +        self.widget.diff(tree) +    } + +    fn width(&self) -> Length { +        self.widget.width() +    } + +    fn height(&self) -> Length { +        self.widget.height() +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        self.widget.layout(renderer, limits) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, B>, +    ) -> event::Status { +        let mut local_messages = Vec::new(); +        let mut local_shell = Shell::new(&mut local_messages); + +        let status = self.widget.on_event( +            tree, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            &mut local_shell, +        ); + +        shell.merge(local_shell, &self.mapper); + +        status +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        self.widget.draw( +            tree, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.widget.mouse_interaction( +            tree, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } +} diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs new file mode 100644 index 00000000..a5bca5a0 --- /dev/null +++ b/pure/src/widget/image.rs @@ -0,0 +1,66 @@ +use crate::widget::{Tree, Widget}; +use crate::Element; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::widget::image; +use iced_native::{Length, Point, Rectangle}; + +use std::hash::Hash; + +pub use image::Image; + +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> +where +    Handle: Clone + Hash, +    Renderer: iced_native::image::Renderer<Handle = Handle>, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } +} + +impl<'a, Message, Renderer, Handle> Into<Element<'a, Message, Renderer>> +    for Image<Handle> +where +    Message: Clone + 'a, +    Renderer: iced_native::image::Renderer<Handle = Handle> + 'a, +    Handle: Clone + Hash + 'a, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs new file mode 100644 index 00000000..9573f27a --- /dev/null +++ b/pure/src/widget/pick_list.rs @@ -0,0 +1,234 @@ +//! Display a dropdown list of selectable values. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::pick_list; +use iced_native::{ +    Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::borrow::Cow; + +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: text::Renderer> +where +    [T]: ToOwned<Owned = Vec<T>>, +{ +    on_selected: Box<dyn Fn(T) -> Message + 'a>, +    options: Cow<'a, [T]>, +    placeholder: Option<String>, +    selected: Option<T>, +    width: Length, +    padding: Padding, +    text_size: Option<u16>, +    font: Renderer::Font, +    style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T: 'a, Message, Renderer: text::Renderer> +    PickList<'a, T, Message, Renderer> +where +    T: ToString + Eq, +    [T]: ToOwned<Owned = Vec<T>>, +{ +    /// The default padding of a [`PickList`]. +    pub const DEFAULT_PADDING: Padding = Padding::new(5); + +    /// Creates a new [`PickList`] with the given [`State`], a list of options, +    /// the current selected value, and the message to produce when an option is +    /// selected. +    pub fn new( +        options: impl Into<Cow<'a, [T]>>, +        selected: Option<T>, +        on_selected: impl Fn(T) -> Message + 'a, +    ) -> Self { +        Self { +            on_selected: Box::new(on_selected), +            options: options.into(), +            placeholder: None, +            selected, +            width: Length::Shrink, +            text_size: None, +            padding: Self::DEFAULT_PADDING, +            font: Default::default(), +            style_sheet: Default::default(), +        } +    } + +    /// Sets the placeholder of the [`PickList`]. +    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { +        self.placeholder = Some(placeholder.into()); +        self +    } + +    /// Sets the width of the [`PickList`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the [`Padding`] of the [`PickList`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    /// Sets the text size of the [`PickList`]. +    pub fn text_size(mut self, size: u16) -> Self { +        self.text_size = Some(size); +        self +    } + +    /// Sets the font of the [`PickList`]. +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } + +    /// Sets the style of the [`PickList`]. +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> +    for PickList<'a, T, Message, Renderer> +where +    T: Clone + ToString + Eq + 'static, +    [T]: ToOwned<Owned = Vec<T>>, +    Message: 'static, +    Renderer: text::Renderer + 'a, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<pick_list::State<T>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(pick_list::State::<T>::new()) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        pick_list::layout( +            renderer, +            limits, +            self.width, +            self.padding, +            self.text_size, +            &self.font, +            self.placeholder.as_ref().map(String::as_str), +            &self.options, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        _renderer: &Renderer, +        _clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        pick_list::update( +            event, +            layout, +            cursor_position, +            shell, +            self.on_selected.as_ref(), +            self.selected.as_ref(), +            &self.options, +            || tree.state.downcast_mut::<pick_list::State<T>>(), +        ) +    } + +    fn mouse_interaction( +        &self, +        _tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +        _renderer: &Renderer, +    ) -> mouse::Interaction { +        pick_list::mouse_interaction(layout, cursor_position) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        _style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) { +        pick_list::draw( +            renderer, +            layout, +            cursor_position, +            self.padding, +            self.text_size, +            &self.font, +            self.placeholder.as_ref().map(String::as_str), +            self.selected.as_ref(), +            self.style_sheet.as_ref(), +        ) +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        _renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        let state = tree.state.downcast_mut::<pick_list::State<T>>(); + +        pick_list::overlay( +            layout, +            state, +            self.padding, +            self.text_size, +            self.font.clone(), +            &self.options, +            self.style_sheet.as_ref(), +        ) +    } +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for PickList<'a, T, Message, Renderer> +where +    T: Clone + ToString + Eq + 'static, +    [T]: ToOwned<Owned = Vec<T>>, +    Renderer: text::Renderer + 'a, +    Message: 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs new file mode 100644 index 00000000..ce3ede84 --- /dev/null +++ b/pure/src/widget/radio.rs @@ -0,0 +1,103 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::Radio; + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Radio<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: text::Renderer, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    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>, +    ) -> event::Status { +        <Self as iced_native::Widget<Message, Renderer>>::on_event( +            self, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( +            self, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Radio<'a, Message, Renderer> +where +    Message: 'a + Clone, +    Renderer: text::Renderer + 'a, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs new file mode 100644 index 00000000..fa0efa68 --- /dev/null +++ b/pure/src/widget/row.rs @@ -0,0 +1,211 @@ +use crate::flex; +use crate::overlay; +use crate::widget::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{ +    Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, +}; + +pub struct Row<'a, Message, Renderer> { +    spacing: u16, +    padding: Padding, +    width: Length, +    height: Length, +    align_items: Alignment, +    children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Row<'a, Message, Renderer> { +    pub fn new() -> Self { +        Self::with_children(Vec::new()) +    } + +    pub fn with_children( +        children: Vec<Element<'a, Message, Renderer>>, +    ) -> Self { +        Row { +            spacing: 0, +            padding: Padding::ZERO, +            width: Length::Shrink, +            height: Length::Shrink, +            align_items: Alignment::Start, +            children, +        } +    } + +    pub fn spacing(mut self, units: u16) -> Self { +        self.spacing = units; +        self +    } + +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    pub fn align_items(mut self, align: Alignment) -> Self { +        self.align_items = align; +        self +    } + +    pub fn push( +        mut self, +        child: impl Into<Element<'a, Message, Renderer>>, +    ) -> Self { +        self.children.push(child.into()); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Row<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    fn children(&self) -> Vec<Tree> { +        self.children.iter().map(Tree::new).collect() +    } + +    fn diff(&self, tree: &mut Tree) { +        tree.diff_children(&self.children) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let limits = limits.width(self.width).height(self.height); + +        flex::resolve( +            flex::Axis::Horizontal, +            renderer, +            &limits, +            self.padding, +            self.spacing as f32, +            self.align_items, +            &self.children, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        self.children +            .iter_mut() +            .zip(&mut tree.children) +            .zip(layout.children()) +            .map(|((child, state), layout)| { +                child.as_widget_mut().on_event( +                    state, +                    event.clone(), +                    layout, +                    cursor_position, +                    renderer, +                    clipboard, +                    shell, +                ) +            }) +            .fold(event::Status::Ignored, event::Status::merge) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        self.children +            .iter() +            .zip(&tree.children) +            .zip(layout.children()) +            .map(|((child, state), layout)| { +                child.as_widget().mouse_interaction( +                    state, +                    layout, +                    cursor_position, +                    viewport, +                    renderer, +                ) +            }) +            .max() +            .unwrap_or_default() +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        for ((child, state), layout) in self +            .children +            .iter() +            .zip(&tree.children) +            .zip(layout.children()) +        { +            child.as_widget().draw( +                state, +                renderer, +                style, +                layout, +                cursor_position, +                viewport, +            ); +        } +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        overlay::from_children(&self.children, tree, layout, renderer) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Row<'a, Message, Renderer> +where +    Message: 'static, +    Renderer: iced_native::Renderer + 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs new file mode 100644 index 00000000..1548fa9d --- /dev/null +++ b/pure/src/widget/scrollable.rs @@ -0,0 +1,265 @@ +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::scrollable; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Vector}; + +pub use iced_style::scrollable::StyleSheet; + +/// A widget that can vertically display an infinite amount of content with a +/// scrollbar. +#[allow(missing_debug_implementations)] +pub struct Scrollable<'a, Message, Renderer> { +    height: Length, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16, +    on_scroll: Option<Box<dyn Fn(f32) -> Message>>, +    style_sheet: Box<dyn StyleSheet + 'a>, +    content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer: iced_native::Renderer> +    Scrollable<'a, Message, Renderer> +{ +    /// Creates a new [`Scrollable`]. +    pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { +        Scrollable { +            height: Length::Shrink, +            scrollbar_width: 10, +            scrollbar_margin: 0, +            scroller_width: 10, +            on_scroll: None, +            style_sheet: Default::default(), +            content: content.into(), +        } +    } + +    /// Sets the height of the [`Scrollable`]. +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the scrollbar width of the [`Scrollable`] . +    /// Silently enforces a minimum value of 1. +    pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { +        self.scrollbar_width = scrollbar_width.max(1); +        self +    } + +    /// Sets the scrollbar margin of the [`Scrollable`] . +    pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { +        self.scrollbar_margin = scrollbar_margin; +        self +    } + +    /// Sets the scroller width of the [`Scrollable`] . +    /// +    /// It silently enforces a minimum value of 1. +    pub fn scroller_width(mut self, scroller_width: u16) -> Self { +        self.scroller_width = scroller_width.max(1); +        self +    } + +    /// Sets a function to call when the [`Scrollable`] is scrolled. +    /// +    /// The function takes the new relative offset of the [`Scrollable`] +    /// (e.g. `0` means top, while `1` means bottom). +    pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { +        self.on_scroll = Some(Box::new(f)); +        self +    } + +    /// Sets the style of the [`Scrollable`] . +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Scrollable<'a, Message, Renderer> +where +    Renderer: iced_native::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<scrollable::State>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(scrollable::State::new()) +    } + +    fn children(&self) -> Vec<Tree> { +        vec![Tree::new(&self.content)] +    } + +    fn diff(&self, tree: &mut Tree) { +        tree.diff_children(std::slice::from_ref(&self.content)) +    } + +    fn width(&self) -> Length { +        self.content.as_widget().width() +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        scrollable::layout( +            renderer, +            limits, +            Widget::<Message, Renderer>::width(self), +            self.height, +            |renderer, limits| { +                self.content.as_widget().layout(renderer, limits) +            }, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        scrollable::update( +            tree.state.downcast_mut::<scrollable::State>(), +            event, +            layout, +            cursor_position, +            clipboard, +            shell, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            &self.on_scroll, +            |event, layout, cursor_position, clipboard, shell| { +                self.content.as_widget_mut().on_event( +                    &mut tree.children[0], +                    event, +                    layout, +                    cursor_position, +                    renderer, +                    clipboard, +                    shell, +                ) +            }, +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) { +        scrollable::draw( +            tree.state.downcast_ref::<scrollable::State>(), +            renderer, +            layout, +            cursor_position, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            self.style_sheet.as_ref(), +            |renderer, layout, cursor_position, viewport| { +                self.content.as_widget().draw( +                    &tree.children[0], +                    renderer, +                    style, +                    layout, +                    cursor_position, +                    viewport, +                ) +            }, +        ) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        scrollable::mouse_interaction( +            tree.state.downcast_ref::<scrollable::State>(), +            layout, +            cursor_position, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +            |layout, cursor_position, viewport| { +                self.content.as_widget().mouse_interaction( +                    &tree.children[0], +                    layout, +                    cursor_position, +                    viewport, +                    renderer, +                ) +            }, +        ) +    } + +    fn overlay<'b>( +        &'b self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        self.content +            .as_widget() +            .overlay( +                &mut tree.children[0], +                layout.children().next().unwrap(), +                renderer, +            ) +            .map(|overlay| { +                let bounds = layout.bounds(); +                let content_layout = layout.children().next().unwrap(); +                let content_bounds = content_layout.bounds(); +                let offset = tree +                    .state +                    .downcast_ref::<scrollable::State>() +                    .offset(bounds, content_bounds); + +                overlay.translate(Vector::new(0.0, -(offset as f32))) +            }) +    } +} + +impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Message: 'a + Clone, +    Renderer: 'a + iced_native::Renderer, +{ +    fn from( +        text_input: Scrollable<'a, Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(text_input) +    } +} diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs new file mode 100644 index 00000000..1107bdc1 --- /dev/null +++ b/pure/src/widget/slider.rs @@ -0,0 +1,243 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::slider; +use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell, Size}; + +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`Slider`] will try to fill the horizontal space of its container. +/// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ``` +/// # use iced_native::widget::slider::{self, Slider}; +/// # +/// #[derive(Clone)] +/// pub enum Message { +///     SliderChanged(f32), +/// } +/// +/// let state = &mut slider::State::new(); +/// let value = 50.0; +/// +/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct Slider<'a, T, Message> { +    range: RangeInclusive<T>, +    step: T, +    value: T, +    on_change: Box<dyn Fn(T) -> Message + 'a>, +    on_release: Option<Message>, +    width: Length, +    height: u16, +    style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T, Message> Slider<'a, T, Message> +where +    T: Copy + From<u8> + std::cmp::PartialOrd, +    Message: Clone, +{ +    /// The default height of a [`Slider`]. +    pub const DEFAULT_HEIGHT: u16 = 22; + +    /// Creates a new [`Slider`]. +    /// +    /// It expects: +    ///   * an inclusive range of possible values +    ///   * the current value of the [`Slider`] +    ///   * a function that will be called when the [`Slider`] is dragged. +    ///   It receives the new value of the [`Slider`] and must produce a +    ///   `Message`. +    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self +    where +        F: 'a + Fn(T) -> Message, +    { +        let value = if value >= *range.start() { +            value +        } else { +            *range.start() +        }; + +        let value = if value <= *range.end() { +            value +        } else { +            *range.end() +        }; + +        Slider { +            value, +            range, +            step: T::from(1), +            on_change: Box::new(on_change), +            on_release: None, +            width: Length::Fill, +            height: Self::DEFAULT_HEIGHT, +            style_sheet: Default::default(), +        } +    } + +    /// Sets the release message of the [`Slider`]. +    /// This is called when the mouse is released from the slider. +    /// +    /// Typically, the user's interaction with the slider is finished when this message is produced. +    /// This is useful if you need to spawn a long-running task from the slider's result, where +    /// the default on_change message could create too many events. +    pub fn on_release(mut self, on_release: Message) -> Self { +        self.on_release = Some(on_release); +        self +    } + +    /// Sets the width of the [`Slider`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`Slider`]. +    pub fn height(mut self, height: u16) -> Self { +        self.height = height; +        self +    } + +    /// Sets the style of the [`Slider`]. +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } + +    /// Sets the step size of the [`Slider`]. +    pub fn step(mut self, step: T) -> Self { +        self.step = step; +        self +    } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> +    for Slider<'a, T, Message> +where +    T: Copy + Into<f64> + num_traits::FromPrimitive, +    Message: Clone, +    Renderer: iced_native::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<slider::State>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(slider::State::new()) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        _renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let limits = +            limits.width(self.width).height(Length::Units(self.height)); + +        let size = limits.resolve(Size::ZERO); + +        layout::Node::new(size) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        _renderer: &Renderer, +        _clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        slider::update( +            event, +            layout, +            cursor_position, +            shell, +            tree.state.downcast_mut::<slider::State>(), +            &mut self.value, +            &self.range, +            self.step, +            self.on_change.as_ref(), +            &self.on_release, +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        _style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) { +        slider::draw( +            renderer, +            layout, +            cursor_position, +            tree.state.downcast_ref::<slider::State>(), +            self.value, +            &self.range, +            self.style_sheet.as_ref(), +        ) +    } + +    fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +        _renderer: &Renderer, +    ) -> mouse::Interaction { +        slider::mouse_interaction( +            layout, +            cursor_position, +            tree.state.downcast_ref::<slider::State>(), +        ) +    } +} + +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>> +    for Element<'a, Message, Renderer> +where +    T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, +    Message: 'a + Clone, +    Renderer: 'a + iced_native::Renderer, +{ +    fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message, Renderer> { +        Element::new(slider) +    } +} diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs new file mode 100644 index 00000000..c04d962a --- /dev/null +++ b/pure/src/widget/space.rs @@ -0,0 +1,98 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::Space; + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +where +    Renderer: iced_native::Renderer, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    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>, +    ) -> event::Status { +        <Self as iced_native::Widget<Message, Renderer>>::on_event( +            self, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( +            self, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Space +where +    Renderer: iced_native::Renderer + 'a, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs new file mode 100644 index 00000000..bfcbaa4b --- /dev/null +++ b/pure/src/widget/text.rs @@ -0,0 +1,70 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Length, Point, Rectangle}; + +pub use iced_native::widget::Text; + +impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> +where +    Renderer: text::Renderer, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    fn draw( +        &self, +        _tree: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Text<Renderer> +where +    Renderer: text::Renderer + 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for &'static str +where +    Renderer: text::Renderer + 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Text::new(self).into() +    } +} diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs new file mode 100644 index 00000000..dec11164 --- /dev/null +++ b/pure/src/widget/text_input.rs @@ -0,0 +1,241 @@ +use crate::widget::tree::{self, Tree}; +use crate::widget::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::text_input; +use iced_native::{Clipboard, Length, Padding, Point, Rectangle, Shell}; + +pub use iced_style::text_input::StyleSheet; + +/// A field that can be filled with text. +/// +/// # Example +/// ``` +/// # use iced_native::renderer::Null; +/// # use iced_native::widget::text_input; +/// # +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; +/// #[derive(Debug, Clone)] +/// enum Message { +///     TextInputChanged(String), +/// } +/// +/// let mut state = text_input::State::new(); +/// let value = "Some text"; +/// +/// let input = TextInput::new( +///     &mut state, +///     "This is the placeholder...", +///     value, +///     Message::TextInputChanged, +/// ) +/// .padding(10); +/// ``` +///  +#[allow(missing_debug_implementations)] +pub struct TextInput<'a, Message, Renderer: text::Renderer> { +    placeholder: String, +    value: text_input::Value, +    is_secure: bool, +    font: Renderer::Font, +    width: Length, +    padding: Padding, +    size: Option<u16>, +    on_change: Box<dyn Fn(String) -> Message + 'a>, +    on_submit: Option<Message>, +    style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: text::Renderer, +{ +    /// Creates a new [`TextInput`]. +    /// +    /// It expects: +    /// - some [`State`] +    /// - a placeholder +    /// - the current value +    /// - a function that produces a message when the [`TextInput`] changes +    pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self +    where +        F: 'a + Fn(String) -> Message, +    { +        TextInput { +            placeholder: String::from(placeholder), +            value: text_input::Value::new(value), +            is_secure: false, +            font: Default::default(), +            width: Length::Fill, +            padding: Padding::ZERO, +            size: None, +            on_change: Box::new(on_change), +            on_submit: None, +            style_sheet: Default::default(), +        } +    } + +    /// Converts the [`TextInput`] into a secure password input. +    pub fn password(mut self) -> Self { +        self.is_secure = true; +        self +    } + +    /// Sets the [`Font`] of the [`Text`]. +    /// +    /// [`Font`]: crate::widget::text::Renderer::Font +    /// [`Text`]: crate::widget::Text +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } +    /// Sets the width of the [`TextInput`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the [`Padding`] of the [`TextInput`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    /// Sets the text size of the [`TextInput`]. +    pub fn size(mut self, size: u16) -> Self { +        self.size = Some(size); +        self +    } + +    /// Sets the message that should be produced when the [`TextInput`] is +    /// focused and the enter key is pressed. +    pub fn on_submit(mut self, message: Message) -> Self { +        self.on_submit = Some(message); +        self +    } + +    /// Sets the style of the [`TextInput`]. +    pub fn style( +        mut self, +        style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, +    ) -> Self { +        self.style_sheet = style_sheet.into(); +        self +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for TextInput<'a, Message, Renderer> +where +    Message: Clone, +    Renderer: iced_native::text::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<text_input::State>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(text_input::State::new()) +    } + +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        text_input::layout( +            renderer, +            limits, +            self.width, +            self.padding, +            self.size, +        ) +    } + +    fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        text_input::update( +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +            &mut self.value, +            self.size, +            &self.font, +            self.is_secure, +            self.on_change.as_ref(), +            &self.on_submit, +            || tree.state.downcast_mut::<text_input::State>(), +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        _style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) { +        text_input::draw( +            renderer, +            layout, +            cursor_position, +            tree.state.downcast_ref::<text_input::State>(), +            &self.value, +            &self.placeholder, +            self.size, +            &self.font, +            self.is_secure, +            self.style_sheet.as_ref(), +        ) +    } + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +        _renderer: &Renderer, +    ) -> mouse::Interaction { +        text_input::mouse_interaction(layout, cursor_position) +    } +} + +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Message: 'a + Clone, +    Renderer: 'a + text::Renderer, +{ +    fn from( +        text_input: TextInput<'a, Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(text_input) +    } +} diff --git a/pure/src/widget/toggler.rs b/pure/src/widget/toggler.rs new file mode 100644 index 00000000..1b3367a4 --- /dev/null +++ b/pure/src/widget/toggler.rs @@ -0,0 +1,103 @@ +use crate::widget::{Tree, Widget}; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::toggler::{Style, StyleSheet, Toggler}; + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Toggler<'a, Message, Renderer> +where +    Renderer: text::Renderer, +{ +    fn width(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::width(self) +    } + +    fn height(&self) -> Length { +        <Self as iced_native::Widget<Message, Renderer>>::height(self) +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        <Self as iced_native::Widget<Message, Renderer>>::layout( +            self, renderer, limits, +        ) +    } + +    fn draw( +        &self, +        _state: &Tree, +        renderer: &mut Renderer, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        <Self as iced_native::Widget<Message, Renderer>>::draw( +            self, +            renderer, +            style, +            layout, +            cursor_position, +            viewport, +        ) +    } + +    fn mouse_interaction( +        &self, +        _state: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( +            self, +            layout, +            cursor_position, +            viewport, +            renderer, +        ) +    } + +    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>, +    ) -> event::Status { +        <Self as iced_native::Widget<Message, Renderer>>::on_event( +            self, +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            shell, +        ) +    } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for Toggler<'a, Message, Renderer> +where +    Message: 'a, +    Renderer: text::Renderer + 'a, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs new file mode 100644 index 00000000..33f5693a --- /dev/null +++ b/pure/src/widget/tree.rs @@ -0,0 +1,115 @@ +use crate::widget::Element; + +use std::any::{self, Any}; + +pub struct Tree { +    pub tag: Tag, +    pub state: State, +    pub children: Vec<Tree>, +} + +impl Tree { +    pub fn empty() -> Self { +        Self { +            tag: Tag::stateless(), +            state: State::None, +            children: Vec::new(), +        } +    } + +    pub fn new<Message, Renderer>( +        element: &Element<'_, Message, Renderer>, +    ) -> Self { +        Self { +            tag: element.as_widget().tag(), +            state: element.as_widget().state(), +            children: element.as_widget().children(), +        } +    } + +    pub fn diff<Message, Renderer>( +        &mut self, +        new: &Element<'_, Message, Renderer>, +    ) { +        if self.tag == new.as_widget().tag() { +            new.as_widget().diff(self) +        } else { +            *self = Self::new(new); +        } +    } + +    pub fn diff_children<Message, Renderer>( +        &mut self, +        new_children: &[Element<'_, Message, Renderer>], +    ) { +        if self.children.len() > new_children.len() { +            self.children.truncate(new_children.len()); +        } + +        for (child_state, new) in +            self.children.iter_mut().zip(new_children.iter()) +        { +            child_state.diff(new); +        } + +        if self.children.len() < new_children.len() { +            self.children.extend( +                new_children[self.children.len()..].iter().map(Self::new), +            ); +        } +    } +} + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { +    pub fn of<T>() -> Self +    where +        T: 'static, +    { +        Self(any::TypeId::of::<T>()) +    } + +    pub fn stateless() -> Self { +        Self::of::<()>() +    } +} + +pub enum State { +    None, +    Some(Box<dyn Any>), +} + +impl State { +    pub fn new<T>(state: T) -> Self +    where +        T: 'static, +    { +        State::Some(Box::new(state)) +    } + +    pub fn downcast_ref<T>(&self) -> &T +    where +        T: 'static, +    { +        match self { +            State::None => panic!("Downcast on stateless state"), +            State::Some(state) => { +                state.downcast_ref().expect("Downcast widget state") +            } +        } +    } + +    pub fn downcast_mut<T>(&mut self) -> &mut T +    where +        T: 'static, +    { +        match self { +            State::None => panic!("Downcast on stateless state"), +            State::Some(state) => { +                state.downcast_mut().expect("Downcast widget state") +            } +        } +    } +} @@ -195,6 +195,9 @@ pub mod time;  pub mod widget;  pub mod window; +#[cfg(feature = "pure")] +pub mod pure; +  #[cfg(all(not(feature = "glow"), feature = "wgpu"))]  use iced_winit as runtime; @@ -214,6 +217,7 @@ pub use application::Application;  pub use element::Element;  pub use error::Error;  pub use executor::Executor; +pub use renderer::Renderer;  pub use result::Result;  pub use sandbox::Sandbox;  pub use settings::Settings; diff --git a/src/pure.rs b/src/pure.rs new file mode 100644 index 00000000..5cab5fd9 --- /dev/null +++ b/src/pure.rs @@ -0,0 +1,51 @@ +//! Leverage pure, virtual widgets in your application. +//! +//! The widgets found in this module are completely stateless versions of +//! [the original widgets]. +//! +//! Effectively, this means that, as a user of the library, you do not need to +//! keep track of the local state of each widget (e.g. [`button::State`]). +//! Instead, the runtime will keep track of everything for you! +//! +//! You can embed pure widgets anywhere in your [impure `Application`] using the +//! [`Pure`] widget and some [`State`]. +//! +//! In case you want to only use pure widgets in your application, this module +//! offers an alternate [`Application`] trait with a completely pure `view` +//! method. +//! +//! [the original widgets]: crate::widget +//! [`button::State`]: crate::widget::button::State +//! [impure `Application`]: crate::Application +pub use iced_pure::{ +    Button as _, Column as _, Element as _, Image as _, Row as _, Text as _, *, +}; + +/// A generic, pure [`Widget`]. +pub type Element<'a, Message> = +    iced_pure::Element<'a, Message, crate::Renderer>; + +/// A pure container widget. +pub type Container<'a, Message> = +    iced_pure::Container<'a, Message, crate::Renderer>; + +/// A pure column widget. +pub type Column<'a, Message> = iced_pure::Column<'a, Message, crate::Renderer>; + +/// A pure row widget. +pub type Row<'a, Message> = iced_pure::Row<'a, Message, crate::Renderer>; + +/// A pure button widget. +pub type Button<'a, Message> = iced_pure::Button<'a, Message, crate::Renderer>; + +/// A pure text widget. +pub type Text = iced_pure::Text<crate::Renderer>; + +/// A pure image widget. +pub type Image = iced_pure::Image<crate::widget::image::Handle>; + +mod application; +mod sandbox; + +pub use application::Application; +pub use sandbox::Sandbox; diff --git a/src/pure/application.rs b/src/pure/application.rs new file mode 100644 index 00000000..5f400bea --- /dev/null +++ b/src/pure/application.rs @@ -0,0 +1,186 @@ +use crate::pure::{self, Pure}; +use crate::window; +use crate::{Color, Command, Executor, Settings, Subscription}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Application`]: crate::Application +/// [`Element`]: pure::Element +pub trait Application: Sized { +    /// The [`Executor`] that will run commands and subscriptions. +    /// +    /// The [default executor] can be a good starting point! +    /// +    /// [`Executor`]: Self::Executor +    /// [default executor]: crate::executor::Default +    type Executor: Executor; + +    /// The type of __messages__ your [`Application`] will produce. +    type Message: std::fmt::Debug + Send; + +    /// The data needed to initialize your [`Application`]. +    type Flags; + +    /// Initializes the [`Application`] with the flags provided to +    /// [`run`] as part of the [`Settings`]. +    /// +    /// Here is where you should return the initial state of your app. +    /// +    /// Additionally, you can return a [`Command`] if you need to perform some +    /// async action in the background on startup. This is useful if you want to +    /// load state from a file, perform an initial HTTP request, etc. +    /// +    /// [`run`]: Self::run +    fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + +    /// Returns the current title of the [`Application`]. +    /// +    /// This title can be dynamic! The runtime will automatically update the +    /// title of your application when necessary. +    fn title(&self) -> String; + +    /// Handles a __message__ and updates the state of the [`Application`]. +    /// +    /// This is where you define your __update logic__. All the __messages__, +    /// produced by either user interactions or commands, will be handled by +    /// this method. +    /// +    /// Any [`Command`] returned will be executed immediately in the background. +    fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + +    /// Returns the event [`Subscription`] for the current state of the +    /// application. +    /// +    /// A [`Subscription`] will be kept alive as long as you keep returning it, +    /// and the __messages__ produced will be handled by +    /// [`update`](#tymethod.update). +    /// +    /// By default, this method returns an empty [`Subscription`]. +    fn subscription(&self) -> Subscription<Self::Message> { +        Subscription::none() +    } + +    /// Returns the widgets to display in the [`Application`]. +    /// +    /// These widgets can produce __messages__ based on user interaction. +    fn view(&self) -> pure::Element<'_, Self::Message>; + +    /// Returns the current [`Application`] mode. +    /// +    /// The runtime will automatically transition your application if a new mode +    /// is returned. +    /// +    /// Currently, the mode only has an effect in native platforms. +    /// +    /// By default, an application will run in windowed mode. +    fn mode(&self) -> window::Mode { +        window::Mode::Windowed +    } + +    /// Returns the background color of the [`Application`]. +    /// +    /// By default, it returns [`Color::WHITE`]. +    fn background_color(&self) -> Color { +        Color::WHITE +    } + +    /// Returns the scale factor of the [`Application`]. +    /// +    /// It can be used to dynamically control the size of the UI at runtime +    /// (i.e. zooming). +    /// +    /// For instance, a scale factor of `2.0` will make widgets twice as big, +    /// while a scale factor of `0.5` will shrink them to half their size. +    /// +    /// By default, it returns `1.0`. +    fn scale_factor(&self) -> f64 { +        1.0 +    } + +    /// Returns whether the [`Application`] should be terminated. +    /// +    /// By default, it returns `false`. +    fn should_exit(&self) -> bool { +        false +    } + +    /// Runs the [`Application`]. +    /// +    /// On native platforms, this method will take control of the current thread +    /// until the [`Application`] exits. +    /// +    /// On the web platform, this method __will NOT return__ unless there is an +    /// [`Error`] during startup. +    /// +    /// [`Error`]: crate::Error +    fn run(settings: Settings<Self::Flags>) -> crate::Result +    where +        Self: 'static, +    { +        <Instance<Self> as crate::Application>::run(settings) +    } +} + +struct Instance<A: Application> { +    application: A, +    state: pure::State, +} + +impl<A> crate::Application for Instance<A> +where +    A: Application, +    A::Message: 'static, +{ +    type Executor = A::Executor; +    type Message = A::Message; +    type Flags = A::Flags; + +    fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) { +        let (application, command) = A::new(flags); + +        ( +            Instance { +                application, +                state: pure::State::new(), +            }, +            command, +        ) +    } + +    fn title(&self) -> String { +        A::title(&self.application) +    } + +    fn update(&mut self, message: Self::Message) -> Command<Self::Message> { +        A::update(&mut self.application, message) +    } + +    fn subscription(&self) -> Subscription<Self::Message> { +        A::subscription(&self.application) +    } + +    fn view(&mut self) -> crate::Element<'_, Self::Message> { +        let content = A::view(&self.application); + +        Pure::new(&mut self.state, content).into() +    } + +    fn mode(&self) -> window::Mode { +        A::mode(&self.application) +    } + +    fn background_color(&self) -> Color { +        A::background_color(&self.application) +    } + +    fn scale_factor(&self) -> f64 { +        A::scale_factor(&self.application) +    } + +    fn should_exit(&self) -> bool { +        A::should_exit(&self.application) +    } +} diff --git a/src/pure/sandbox.rs b/src/pure/sandbox.rs new file mode 100644 index 00000000..fbd1d7a8 --- /dev/null +++ b/src/pure/sandbox.rs @@ -0,0 +1,119 @@ +use crate::pure; +use crate::{Color, Command, Error, Settings, Subscription}; + +/// A pure version of [`Sandbox`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Sandbox`]: crate::Sandbox +/// [`Element`]: pure::Element +pub trait Sandbox { +    /// The type of __messages__ your [`Sandbox`] will produce. +    type Message: std::fmt::Debug + Send; + +    /// Initializes the [`Sandbox`]. +    /// +    /// Here is where you should return the initial state of your app. +    fn new() -> Self; + +    /// Returns the current title of the [`Sandbox`]. +    /// +    /// This title can be dynamic! The runtime will automatically update the +    /// title of your application when necessary. +    fn title(&self) -> String; + +    /// Handles a __message__ and updates the state of the [`Sandbox`]. +    /// +    /// This is where you define your __update logic__. All the __messages__, +    /// produced by user interactions, will be handled by this method. +    fn update(&mut self, message: Self::Message); + +    /// Returns the widgets to display in the [`Sandbox`]. +    /// +    /// These widgets can produce __messages__ based on user interaction. +    fn view(&self) -> pure::Element<'_, Self::Message>; + +    /// Returns the background color of the [`Sandbox`]. +    /// +    /// By default, it returns [`Color::WHITE`]. +    fn background_color(&self) -> Color { +        Color::WHITE +    } + +    /// Returns the scale factor of the [`Sandbox`]. +    /// +    /// It can be used to dynamically control the size of the UI at runtime +    /// (i.e. zooming). +    /// +    /// For instance, a scale factor of `2.0` will make widgets twice as big, +    /// while a scale factor of `0.5` will shrink them to half their size. +    /// +    /// By default, it returns `1.0`. +    fn scale_factor(&self) -> f64 { +        1.0 +    } + +    /// Returns whether the [`Sandbox`] should be terminated. +    /// +    /// By default, it returns `false`. +    fn should_exit(&self) -> bool { +        false +    } + +    /// Runs the [`Sandbox`]. +    /// +    /// On native platforms, this method will take control of the current thread +    /// and __will NOT return__. +    /// +    /// It should probably be that last thing you call in your `main` function. +    fn run(settings: Settings<()>) -> Result<(), Error> +    where +        Self: 'static + Sized, +    { +        <Self as pure::Application>::run(settings) +    } +} + +impl<T> pure::Application for T +where +    T: Sandbox, +{ +    type Executor = iced_futures::backend::null::Executor; +    type Flags = (); +    type Message = T::Message; + +    fn new(_flags: ()) -> (Self, Command<T::Message>) { +        (T::new(), Command::none()) +    } + +    fn title(&self) -> String { +        T::title(self) +    } + +    fn update(&mut self, message: T::Message) -> Command<T::Message> { +        T::update(self, message); + +        Command::none() +    } + +    fn subscription(&self) -> Subscription<T::Message> { +        Subscription::none() +    } + +    fn view(&self) -> pure::Element<'_, T::Message> { +        T::view(self) +    } + +    fn background_color(&self) -> Color { +        T::background_color(self) +    } + +    fn scale_factor(&self) -> f64 { +        T::scale_factor(self) +    } + +    fn should_exit(&self) -> bool { +        T::should_exit(self) +    } +}  | 
