aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpml68 <contact@pml68.dev>2025-03-30 16:24:46 +0200
committerpml68 <contact@pml68.dev>2025-03-30 16:24:46 +0200
commitb86e2076d759c3726b4406abbd61290fd41581ce (patch)
treec1d184ffc89151017a18f72104d891489196c1ef
downloadiced_dialog-b86e2076d759c3726b4406abbd61290fd41581ce.tar.gz
feat: initial commit
-rw-r--r--.cargo/config.toml3
-rw-r--r--.gitattributes6
-rw-r--r--.github/workflows/ci.yml21
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml56
-rw-r--r--LICENSE21
-rw-r--r--README.md11
-rw-r--r--example/Cargo.toml8
-rw-r--r--example/src/main.rs62
-rw-r--r--rustfmt.toml3
-rw-r--r--src/dialog.rs282
-rw-r--r--src/lib.rs46
12 files changed, 521 insertions, 0 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..feb6aad
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,3 @@
+[alias]
+lint = "clippy --no-deps -- -D warnings"
+lint-all = "clippy --no-deps -- -D clippy::pedantic"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..2742e4d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+* text=auto eol=lf
+
+# Older git versions try to fix line endings on images, this prevents it.
+*.png binary
+*.jpg binary
+*.ico binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..daff1be
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,21 @@
+name: CI
+on: [push, pull_request]
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: hecrj/setup-rust-action@v2
+ with:
+ components: clippy
+ - uses: actions/checkout@master
+ - name: Install dependencies
+ run: |
+ export DEBIAN_FRONTED=noninteractive
+ sudo apt-get -qq update
+ sudo apt-get install -y libxkbcommon-dev
+ - name: Check lints
+ run: cargo lint
+ - name: Run tests
+ run: cargo test --verbose
+ - name: Build example
+ run: cargo build -p example
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2c96eb1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+target/
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..16c55e1
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,56 @@
+[package]
+name = "iced_dialog"
+description = "A custom dialog widget for `iced`"
+authors = ["pml68 <contact@pml68.dev>"]
+version = "0.14.0-dev"
+edition = "2024"
+license = "MIT"
+readme = "README.md"
+repository = "https://github.com/pml68/iced_dialog"
+categories = ["gui"]
+keywords = ["gui", "ui", "graphics", "interface", "widgets"]
+rust-version = "1.85"
+
+[dependencies]
+iced_widget.workspace = true
+iced_core.workspace = true
+
+[workspace.dependencies.iced_widget]
+git = "https://github.com/iced-rs/iced"
+branch = "master"
+features = ["advanced"]
+
+[workspace.dependencies.iced_core]
+git = "https://github.com/iced-rs/iced"
+branch = "master"
+features = ["advanced"]
+
+[workspace]
+members = ["example"]
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
+
+[lints.rust]
+missing_docs = "deny"
+unsafe_code = "deny"
+unused_results = "deny"
+
+[lints.clippy]
+type-complexity = "allow"
+semicolon_if_nothing_returned = "deny"
+trivially-copy-pass-by-ref = "deny"
+default_trait_access = "deny"
+match-wildcard-for-single-variants = "deny"
+redundant-closure-for-method-calls = "deny"
+filter_map_next = "deny"
+manual_let_else = "deny"
+unused_async = "deny"
+from_over_into = "deny"
+needless_borrow = "deny"
+new_without_default = "deny"
+useless_conversion = "deny"
+
+[lints.rustdoc]
+broken_intra_doc_links = "forbid"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fb9185f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Polesznyák Márk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18d0e0a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# iced_dialog
+
+Custom dialog for [`iced`](https://iced.rs)
+
+## Example
+See the [/example](/example) directory.
+
+You can run it like this:
+```bash
+cargo run -p example
+```
diff --git a/example/Cargo.toml b/example/Cargo.toml
new file mode 100644
index 0000000..7bc22dc
--- /dev/null
+++ b/example/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "example"
+version = "0.0.1"
+edition = "2024"
+
+[dependencies]
+iced = { git = "https://github.com/iced-rs/iced", branch = "master" }
+iced_dialog = { path = ".." }
diff --git a/example/src/main.rs b/example/src/main.rs
new file mode 100644
index 0000000..0234319
--- /dev/null
+++ b/example/src/main.rs
@@ -0,0 +1,62 @@
+use iced::{
+ Element, Length, Task,
+ widget::{center, column, text},
+};
+use iced_dialog::{button, dialog};
+
+#[derive(Default)]
+struct State {
+ is_open: bool,
+ action_text: String,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ OpenDialog,
+ Saved,
+ Cancelled,
+}
+
+fn main() -> iced::Result {
+ iced::run("Dialog Example", State::update, State::view)
+}
+
+impl State {
+ fn update(&mut self, message: Message) -> Task<Message> {
+ match message {
+ Message::OpenDialog => self.is_open = true,
+ Message::Saved => {
+ self.action_text = "User saved their work".to_owned();
+ self.is_open = false;
+ }
+ Message::Cancelled => {
+ self.action_text = "User cancelled the dialog".to_owned();
+ self.is_open = false;
+ }
+ }
+ Task::none()
+ }
+
+ fn view(&self) -> Element<'_, Message> {
+ let base = center(
+ column![
+ text(&self.action_text),
+ iced::widget::button("Open Dialog")
+ .on_press(Message::OpenDialog)
+ ]
+ .spacing(14.0),
+ )
+ .width(Length::Fill)
+ .height(Length::Fill);
+
+ let dialog_content = text("Do you want to save?");
+
+ dialog(self.is_open, base, dialog_content)
+ .title("Save")
+ .push_button(button("Save").on_press(Message::Saved))
+ .push_button(button("Cancel").on_press(Message::Cancelled))
+ .width(350)
+ .height(234)
+ .into()
+ }
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..e029395
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,3 @@
+edition = "2024"
+group_imports = "StdExternalCrate"
+max_width = 80
diff --git a/src/dialog.rs b/src/dialog.rs
new file mode 100644
index 0000000..d1e7b9d
--- /dev/null
+++ b/src/dialog.rs
@@ -0,0 +1,282 @@
+//! Dialogs can be used to provide users with
+//! important information and make them act on it.
+use iced_core::{
+ self as core, Alignment, Element, Length, Padding, Pixels,
+ alignment::Vertical, color,
+};
+use iced_widget::{
+ Column, Container, Row, Theme, center, container, mouse_area, opaque,
+ stack, text, vertical_space,
+};
+
+/// A message dialog.
+///
+/// Only the content is required, [`buttons`] and the [`title`] are optional.
+///
+/// [`buttons`]: Dialog::with_buttons
+/// [`title`]: Dialog::title
+pub struct Dialog<'a, Message, Theme, Renderer>
+where
+ Renderer: 'a + core::text::Renderer,
+ Theme: 'a + Catalog,
+{
+ is_open: bool,
+ base: Element<'a, Message, Theme, Renderer>,
+ title: Option<String>,
+ content: Element<'a, Message, Theme, Renderer>,
+ buttons: Vec<Element<'a, Message, Theme, Renderer>>,
+ font: Option<Renderer::Font>,
+ width: Length,
+ height: Length,
+ spacing: f32,
+ padding: Padding,
+ button_alignment: Alignment,
+ title_class: <Theme as text::Catalog>::Class<'a>,
+ container_class: <Theme as container::Catalog>::Class<'a>,
+}
+
+impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
+where
+ Renderer: 'a + core::Renderer + core::text::Renderer,
+ Theme: 'a + Catalog,
+ Message: 'a + Clone,
+ <Theme as container::Catalog>::Class<'a>:
+ From<container::StyleFn<'a, Theme>>,
+{
+ /// Creates a new [`Dialog`] with the given base and dialog content.
+ pub fn new(
+ is_open: bool,
+ base: impl Into<Element<'a, Message, Theme, Renderer>>,
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self::with_buttons(is_open, base, content, Vec::new())
+ }
+
+ /// Creates a new [`Dialog`] with the given base, dialog content and buttons.
+ pub fn with_buttons(
+ is_open: bool,
+ base: impl Into<Element<'a, Message, Theme, Renderer>>,
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ buttons: Vec<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ is_open,
+ base: base.into(),
+ title: None,
+ content: content.into(),
+ buttons,
+ font: None,
+ width: 400.into(),
+ height: 260.into(),
+ spacing: 8.0,
+ padding: 24.into(),
+ button_alignment: Alignment::Start,
+ title_class: <Theme as Catalog>::default_title(),
+ container_class: <Theme as Catalog>::default_container(),
+ }
+ }
+
+ /// Sets the [`Dialog`]'s title.
+ pub fn title(mut self, title: impl Into<String>) -> Self {
+ self.title = Some(title.into());
+ self
+ }
+
+ /// Sets the [`Dialog`]'s width.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the [`Dialog`]'s height.
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Sets the [`Dialog`]'s padding.
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self
+ }
+
+ /// Sets the [`Dialog`]'s spacing.
+ pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
+ self.spacing = spacing.into().0;
+ self
+ }
+
+ /// Sets the vertical alignment of the [`Dialog`]'s buttons.
+ pub fn align_buttons(mut self, align: impl Into<Vertical>) -> Self {
+ self.button_alignment = Alignment::from(align.into());
+ self
+ }
+
+ /// Sets the [`Font`] of the [`Dialog`]'s title.
+ ///
+ /// [`Font`]: https://docs.iced.rs/iced_core/text/trait.Renderer.html#associatedtype.Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Adds a button to the [`Dialog`].
+ pub fn push_button(
+ mut self,
+ button: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ self.buttons.push(button.into());
+ self
+ }
+
+ /// Adds a button to the [`Dialog`], if `Some`.
+ pub fn push_button_maybe(
+ self,
+ button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
+ ) -> Self {
+ if let Some(button) = button {
+ self.push_button(button)
+ } else {
+ self
+ }
+ }
+
+ /// Extends the [`Dialog`] with the given buttons.
+ pub fn extend(
+ self,
+ buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ buttons.into_iter().fold(self, Self::push_button)
+ }
+
+ /// Sets the style of the [`Dialog`]'s title.
+ #[must_use]
+ pub fn title_style(
+ mut self,
+ style: impl Fn(&Theme) -> text::Style + 'a,
+ ) -> Self
+ where
+ <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
+ {
+ self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
+ self
+ }
+
+ /// Sets the style of the [`Dialog`]'s container.
+ #[must_use]
+ pub fn container_style(
+ mut self,
+ style: impl Fn(&Theme) -> container::Style + 'a,
+ ) -> Self
+ where
+ <Theme as container::Catalog>::Class<'a>:
+ From<container::StyleFn<'a, Theme>>,
+ {
+ self.container_class =
+ (Box::new(style) as container::StyleFn<'a, Theme>).into();
+ self
+ }
+
+ fn view(self) -> Element<'a, Message, Theme, Renderer> {
+ if self.is_open {
+ let contents = Container::new(
+ Column::new()
+ .push_maybe(self.title.map(|title| {
+ let text = text(title)
+ .size(20)
+ .line_height(text::LineHeight::Absolute(Pixels(
+ 26.0,
+ )))
+ .class(self.title_class);
+
+ if let Some(font) = self.font {
+ text.font(font)
+ } else {
+ text
+ }
+ }))
+ .push(vertical_space().height(12))
+ .push(self.content),
+ )
+ .width(Length::Fill)
+ .padding(self.padding);
+
+ let buttons = Container::new(
+ Row::with_children(self.buttons).spacing(self.spacing),
+ )
+ .height(80)
+ .padding(self.padding);
+
+ let dialog = Container::new(
+ Column::new()
+ .push(contents)
+ .push(vertical_space())
+ .push(buttons),
+ )
+ .width(self.width)
+ .height(self.height)
+ .class(self.container_class)
+ .clip(true);
+
+ modal(self.base, dialog)
+ } else {
+ self.base
+ }
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
+where
+ Renderer: 'a + core::Renderer + core::text::Renderer,
+ Theme: 'a + Catalog,
+ Message: 'a + Clone,
+ <Theme as container::Catalog>::Class<'a>:
+ From<container::StyleFn<'a, Theme>>,
+{
+ fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
+ dialog.view()
+ }
+}
+
+fn modal<'a, Message, Theme, Renderer>(
+ base: impl Into<Element<'a, Message, Theme, Renderer>>,
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a + Clone,
+ Renderer: 'a + core::Renderer,
+ Theme: 'a + container::Catalog,
+ Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
+{
+ let area =
+ mouse_area(center(opaque(content)).style(|_theme| container::Style {
+ background: Some(color!(0x000000, 0.3).into()),
+ ..Default::default()
+ }));
+
+ stack![base.into(), opaque(area)].into()
+}
+
+/// The theme catalog of a [`Dialog`].
+pub trait Catalog: text::Catalog + container::Catalog {
+ /// The default class for the [`Dialog`]'s title.
+ fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
+ <Self as text::Catalog>::default()
+ }
+
+ /// The default class for the [`Dialog`]'s container.
+ fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
+ <Self as container::Catalog>::default()
+ }
+}
+
+impl Catalog for Theme {
+ fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
+ Box::new(|theme| {
+ container::background(
+ theme.extended_palette().background.base.color,
+ )
+ })
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..856a45c
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,46 @@
+//! Custom dialog for `iced`
+//!
+//! # Example
+//! See [here](https://github.com/pml68/iced_dialog/tree/master/example)
+pub mod dialog;
+use dialog::Dialog;
+use iced_core as core;
+use iced_core::alignment::Horizontal;
+use iced_widget::Button;
+use iced_widget::{container, text};
+
+/// Creates a new [`Dialog`] with the given base and dialog content.
+pub fn dialog<'a, Message, Theme, Renderer>(
+ is_open: bool,
+ base: impl Into<core::Element<'a, Message, Theme, Renderer>>,
+ content: impl Into<core::Element<'a, Message, Theme, Renderer>>,
+) -> Dialog<'a, Message, Theme, Renderer>
+where
+ Renderer: 'a + core::Renderer + core::text::Renderer,
+ Theme: 'a + dialog::Catalog,
+ Message: 'a + Clone,
+ <Theme as container::Catalog>::Class<'a>:
+ From<container::StyleFn<'a, Theme>>,
+{
+ Dialog::new(is_open, base, content)
+}
+
+/// Pre-styled [`Button`] for [`Dialog`]s.
+///
+/// [`Button`]: https://docs.iced.rs/iced/widget/struct.Button.html
+pub fn button<'a, Message, Theme, Renderer>(
+ content: &'a str,
+) -> Button<'a, Message, Theme, Renderer>
+where
+ Theme: 'a + iced_widget::button::Catalog + text::Catalog,
+ Renderer: 'a + core::Renderer + core::text::Renderer,
+{
+ iced_widget::button(
+ text(content)
+ .size(14)
+ .line_height(text::LineHeight::Absolute(core::Pixels(20.0)))
+ .align_x(Horizontal::Center),
+ )
+ .height(32)
+ .width(core::Length::Fill)
+}