From 58157231101b4e3a019387522f2c02658066bab8 Mon Sep 17 00:00:00 2001 From: Rizzen Yazston Date: Thu, 18 Jul 2024 20:59:18 +0200 Subject: [PATCH 1/2] Adding new feature 'sidebar'. --- Cargo.lock | 418 ++++---- Cargo.toml | 3 + examples/sidebar/Cargo.toml | 12 + examples/sidebar/fonts/LICENSE.txt | 12 + examples/sidebar/fonts/config.json | 40 + examples/sidebar/fonts/icons.ttf | Bin 0 -> 6672 bytes examples/sidebar/images/ferris.png | Bin 0 -> 33061 bytes examples/sidebar/src/counter.rs | 66 ++ examples/sidebar/src/ferris.rs | 79 ++ examples/sidebar/src/login.rs | 98 ++ examples/sidebar/src/main.rs | 159 +++ examples/sidebar/src/settings.rs | 154 +++ src/lib.rs | 4 + src/style.rs | 3 + src/style/sidebar.rs | 205 ++++ src/widgets.rs | 9 + src/widgets/sidebar.rs | 12 + src/widgets/sidebar/column.rs | 428 ++++++++ src/widgets/sidebar/row.rs | 433 ++++++++ src/widgets/sidebar/sidebar.rs | 1528 ++++++++++++++++++++++++++++ src/widgets/tab_bar.rs | 2 +- 21 files changed, 3469 insertions(+), 196 deletions(-) create mode 100644 examples/sidebar/Cargo.toml create mode 100644 examples/sidebar/fonts/LICENSE.txt create mode 100644 examples/sidebar/fonts/config.json create mode 100644 examples/sidebar/fonts/icons.ttf create mode 100644 examples/sidebar/images/ferris.png create mode 100644 examples/sidebar/src/counter.rs create mode 100644 examples/sidebar/src/ferris.rs create mode 100644 examples/sidebar/src/login.rs create mode 100644 examples/sidebar/src/main.rs create mode 100644 examples/sidebar/src/settings.rs create mode 100644 src/style/sidebar.rs create mode 100644 src/widgets/sidebar.rs create mode 100644 src/widgets/sidebar/column.rs create mode 100644 src/widgets/sidebar/row.rs create mode 100644 src/widgets/sidebar/sidebar.rs diff --git a/Cargo.lock b/Cargo.lock index c45cf294..ad09c23d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" dependencies = [ "async-io", "async-lock", @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" @@ -430,13 +430,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + [[package]] name = "calloop-wayland-source" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ - "calloop", + "calloop 0.12.4", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", "rustix", "wayland-backend", "wayland-client", @@ -661,19 +687,21 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71" +checksum = "70b7eecd441fdfc092d6afcb4d00a521ee6d3dc3ad882575ce13bf38be53fb71" dependencies = [ - "fontdb", - "libm", + "bitflags 2.6.0", + "fontdb 0.16.2", "log", "rangemap", - "rustc-hash", - "rustybuzz 0.11.0", + "rayon", + "rustc-hash 1.1.0", + "rustybuzz", "self_cell", "swash", "sys-locale", + "ttf-parser 0.21.1", "unicode-bidi", "unicode-linebreak", "unicode-script", @@ -1105,16 +1133,30 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.15.0" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.8.0", + "memmap2", "slotmap", "tinyvec", - "ttf-parser 0.19.2", + "ttf-parser 0.21.1", ] [[package]] @@ -1278,16 +1320,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gif" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gif" version = "0.13.1" @@ -1339,12 +1371,12 @@ dependencies = [ [[package]] name = "glyphon" version = "0.5.0" -source = "git+https://github.com/hecrj/glyphon.git?rev=f07e7bab705e69d39a5e6e52c73039a93c4552f8#f07e7bab705e69d39a5e6e52c73039a93c4552f8" +source = "git+https://github.com/hecrj/glyphon.git?rev=feef9f5630c2adb3528937e55f7bfad2da561a65#feef9f5630c2adb3528937e55f7bfad2da561a65" dependencies = [ "cosmic-text", "etagere", "lru", - "rustc-hash", + "rustc-hash 2.0.0", "wgpu", ] @@ -1512,7 +1544,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "iced_core", "iced_futures", @@ -1540,7 +1572,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "bitflags 2.6.0", "bytes", @@ -1550,7 +1582,7 @@ dependencies = [ "num-traits", "once_cell", "palette", - "rustc-hash", + "rustc-hash 2.0.0", "smol_str", "thiserror", "web-time", @@ -1559,12 +1591,12 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "futures", "iced_core", "log", - "rustc-hash", + "rustc-hash 2.0.0", "wasm-bindgen-futures", "wasm-timer", ] @@ -1572,7 +1604,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -1586,7 +1618,7 @@ dependencies = [ "lyon_path", "once_cell", "raw-window-handle", - "rustc-hash", + "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", ] @@ -1594,7 +1626,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -1606,7 +1638,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "bytes", "iced_core", @@ -1618,7 +1650,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "bytemuck", "cosmic-text", @@ -1626,7 +1658,7 @@ dependencies = [ "kurbo 0.10.4", "log", "resvg", - "rustc-hash", + "rustc-hash 2.0.0", "softbuffer", "tiny-skia", ] @@ -1634,7 +1666,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -1647,7 +1679,7 @@ dependencies = [ "lyon", "once_cell", "resvg", - "rustc-hash", + "rustc-hash 2.0.0", "thiserror", "wgpu", ] @@ -1655,13 +1687,13 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "iced_renderer", "iced_runtime", "num-traits", "once_cell", - "rustc-hash", + "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", ] @@ -1669,13 +1701,13 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.0-dev" -source = "git+https://github.com/iced-rs/iced.git#24f74768236dac874a602f7aa7de4ca8a0cc793f" +source = "git+https://github.com/iced-rs/iced.git#616689ca54942a13aac3615e571ae995ad4571b6" dependencies = [ "iced_futures", "iced_graphics", "iced_runtime", "log", - "rustc-hash", + "rustc-hash 2.0.0", "thiserror", "tracing", "wasm-bindgen-futures", @@ -1695,7 +1727,7 @@ dependencies = [ "byteorder", "color_quant", "exr", - "gif 0.13.1", + "gif", "jpeg-decoder", "num-traits", "png", @@ -1820,18 +1852,19 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" -version = "0.9.5" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" dependencies = [ "arrayvec", + "smallvec", ] [[package]] name = "kurbo" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" dependencies = [ "arrayvec", "smallvec", @@ -1997,15 +2030,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.9.4" @@ -2076,7 +2100,7 @@ dependencies = [ "indexmap", "log", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", @@ -2624,7 +2648,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] @@ -2847,12 +2871,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rctree" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" - [[package]] name = "read-fonts" version = "0.19.3" @@ -2909,15 +2927,14 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "resvg" -version = "0.36.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7980f653f9a7db31acff916a262c3b78c562919263edea29bf41a056e20497" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" dependencies = [ - "gif 0.12.0", + "gif", "jpeg-decoder", "log", "pico-args", - "png", "rgb", "svgtypes", "tiny-skia", @@ -2935,18 +2952,15 @@ dependencies = [ [[package]] name = "roxmltree" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" -dependencies = [ - "xmlparser", -] +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "roxmltree" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-ini" @@ -2964,6 +2978,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.38.34" @@ -2979,31 +2999,15 @@ dependencies = [ [[package]] name = "rustybuzz" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cd15fef9112a1f94ac64b58d1e4628192631ad6af4dc69997f995459c874e7" -dependencies = [ - "bitflags 1.3.2", - "bytemuck", - "smallvec", - "ttf-parser 0.19.2", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "rustybuzz" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "bytemuck", "libm", "smallvec", - "ttf-parser 0.20.0", + "ttf-parser 0.21.1", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -3039,8 +3043,8 @@ checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" dependencies = [ "ab_glyph", "log", - "memmap2 0.9.4", - "smithay-client-toolkit", + "memmap2", + "smithay-client-toolkit 0.18.1", "tiny-skia", ] @@ -3100,6 +3104,14 @@ dependencies = [ "digest", ] +[[package]] +name = "sidebar" +version = "0.1.0" +dependencies = [ + "iced", + "iced_aw", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3130,6 +3142,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skrifa" version = "0.19.3" @@ -3179,32 +3197,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ "bitflags 2.6.0", - "calloop", - "calloop-wayland-source", + "calloop 0.12.4", + "calloop-wayland-source 0.2.0", "cursor-icon", "libc", "log", - "memmap2 0.9.4", + "memmap2", "rustix", "thiserror", "wayland-backend", "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.3", + "wayland-protocols-wlr 0.3.3", "wayland-scanner", "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "wayland-backend", ] @@ -3232,7 +3275,7 @@ dependencies = [ "foreign-types", "js-sys", "log", - "memmap2 0.9.4", + "memmap2", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3299,12 +3342,12 @@ checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" [[package]] name = "svgtypes" -version = "0.12.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71499ff2d42f59d26edb21369a308ede691421f79ebc0f001e2b1fd3a7c9e52" +checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" dependencies = [ - "kurbo 0.9.5", - "siphasher", + "kurbo 0.11.0", + "siphasher 1.0.1", ] [[package]] @@ -3388,18 +3431,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -3550,15 +3593,15 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "ttf-parser" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "ttf-parser" @@ -3591,15 +3634,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-bidi-mirroring" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" [[package]] name = "unicode-ccc" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" @@ -3651,63 +3694,29 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "usvg" -version = "0.36.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51daa774fe9ee5efcf7b4fec13019b8119cda764d9a8b5b06df02bb1445c656" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" dependencies = [ "base64", - "log", - "pico-args", - "usvg-parser", - "usvg-text-layout", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c88a5ffaa338f0e978ecf3d4e00d8f9f493e29bed0752e1a808a1db16afc40" -dependencies = [ "data-url", "flate2", + "fontdb 0.18.0", "imagesize", - "kurbo 0.9.5", + "kurbo 0.11.0", "log", - "roxmltree 0.18.1", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", "simplecss", - "siphasher", + "siphasher 1.0.1", + "strict-num", "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-text-layout" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2374378cb7a3fb8f33894e0fdb8625e1bbc4f25312db8d91f862130b541593" -dependencies = [ - "fontdb", - "kurbo 0.9.5", - "log", - "rustybuzz 0.10.0", + "tiny-skia-path", "unicode-bidi", "unicode-script", "unicode-vo", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cacb0c5edeaf3e80e5afcf5b0d4004cc1d36318befc9a7c6606507e5d0f4062" -dependencies = [ - "rctree", - "strict-num", - "svgtypes", - "tiny-skia-path", + "xmlwriter", ] [[package]] @@ -3815,9 +3824,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269c04f203640d0da2092d1b8d89a2d081714ae3ac2f1b53e99f205740517198" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -3829,9 +3838,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bd0f46c069d3382a36c8666c1b9ccef32b8b04f41667ca1fef06a1adcc2982" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ "bitflags 2.6.0", "rustix", @@ -3852,9 +3861,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09414bcf0fd8d9577d73e9ac4659ebc45bcc9cff1980a350543ad8e50ee263b2" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" dependencies = [ "rustix", "wayland-client", @@ -3873,6 +3882,18 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-plasma" version = "0.2.0" @@ -3882,7 +3903,7 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-scanner", ] @@ -3895,15 +3916,28 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.3", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.31.3" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf466fc49a4feb65a511ca403fec3601494d0dee85dbf37fff6fa0dd4eec3b6" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2", "quick-xml", @@ -3912,9 +3946,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.3" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6754825230fa5b27bafaa28c30b3c9e72c55530581220cef401fa422c0fae7" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -3991,7 +4025,7 @@ dependencies = [ "parking_lot 0.12.3", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "web-sys", @@ -4001,9 +4035,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3" +checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" dependencies = [ "android_system_properties", "arrayvec", @@ -4035,7 +4069,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "wasm-bindgen", @@ -4341,7 +4375,7 @@ dependencies = [ "bitflags 2.6.0", "block2", "bytemuck", - "calloop", + "calloop 0.12.4", "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation", @@ -4350,7 +4384,7 @@ dependencies = [ "dpi", "js-sys", "libc", - "memmap2 0.9.4", + "memmap2", "ndk", "objc2", "objc2-app-kit", @@ -4363,7 +4397,7 @@ dependencies = [ "redox_syscall 0.4.1", "rustix", "sctk-adwaita", - "smithay-client-toolkit", + "smithay-client-toolkit 0.18.1", "smol_str", "tracing", "unicode-segmentation", @@ -4371,7 +4405,7 @@ dependencies = [ "wasm-bindgen-futures", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-protocols-plasma", "web-sys", "web-time", @@ -4481,12 +4515,6 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "xmlwriter" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7b7ff67b..a663ca94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ spinner = [] context_menu = [] slide_bar = [] drop_down = [] +sidebar = [] default = [ "badge", @@ -54,6 +55,7 @@ default = [ "spinner", "drop_down", "menu", + "sidebar", ] [dependencies] @@ -94,6 +96,7 @@ members = [ "examples/WidgetIDReturn", "examples/drop_down", "examples/menu", + "examples/sidebar", ] [workspace.dependencies.iced] diff --git a/examples/sidebar/Cargo.toml b/examples/sidebar/Cargo.toml new file mode 100644 index 00000000..bcfb7216 --- /dev/null +++ b/examples/sidebar/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sidebar" +version = "0.1.0" +authors = ["Kaiden42 ", "Rizzen Yazston"] +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced_aw = { workspace = true, features = ["sidebar", "icons"] } +iced = { workspace = true, features = [ "image"] } diff --git a/examples/sidebar/fonts/LICENSE.txt b/examples/sidebar/fonts/LICENSE.txt new file mode 100644 index 00000000..8fa3da36 --- /dev/null +++ b/examples/sidebar/fonts/LICENSE.txt @@ -0,0 +1,12 @@ +Font license info + + +## Font Awesome + + Copyright (C) 2016 by Dave Gandy + + Author: Dave Gandy + License: SIL () + Homepage: http://fortawesome.github.com/Font-Awesome/ + + diff --git a/examples/sidebar/fonts/config.json b/examples/sidebar/fonts/config.json new file mode 100644 index 00000000..dfc22c08 --- /dev/null +++ b/examples/sidebar/fonts/config.json @@ -0,0 +1,40 @@ +{ + "name": "icons", + "css_prefix_text": "icon-", + "css_use_suffix": false, + "hinting": true, + "units_per_em": 1000, + "ascent": 850, + "glyphs": [ + { + "uid": "8b80d36d4ef43889db10bc1f0dc9a862", + "css": "user", + "code": 59392, + "src": "fontawesome" + }, + { + "uid": "d73eceadda1f594cec0536087539afbf", + "css": "heart", + "code": 59393, + "src": "fontawesome" + }, + { + "uid": "1ee2aeb352153a403df4b441a8bc9bda", + "css": "calc", + "code": 61932, + "src": "fontawesome" + }, + { + "uid": "98687378abd1faf8f6af97c254eb6cd6", + "css": "cog-alt", + "code": 59394, + "src": "fontawesome" + }, + { + "uid": "5211af474d3a9848f67f945e2ccaf143", + "css": "cancel", + "code": 59395, + "src": "fontawesome" + } + ] +} \ No newline at end of file diff --git a/examples/sidebar/fonts/icons.ttf b/examples/sidebar/fonts/icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..abfc4ebec4eb001d864367d3864266653658a8fe GIT binary patch literal 6672 zcmd^CeQaCTbwBrg@k=Bni=t#xlArYS74ngiX-QUWMV3i@*s+($lI)O9lai*s>`J00 z$w8c@ft#j9pk`@@VFii~Lx$Qz*QMB{-40n$48}05gMlIaBkLLz7zPyCJRsl1%?>SQy-0xGB$BymO{(L%~je%n340a{%&3@pIa=F z7gE*;=vXTt5oByj)z`$gNXPikDHw@fsZ#N;v`;9IlCDDffM;r9e2%W)r1#%br*EI9 zGwL*b8!#TJ`oJbuiO9x_amAdgdm2(k*`R($u2C9o11LetP4)fvZr>(i%S`dGw>1t` z&_`o$1^fU1{o$Apop8f|sN4k0FUR+$`(g>{ru*`G+;m@FllR}NzKAFgw=2pGwT2p~ zl{!b;Nf&xq6+#nb(iE)>gi?m-h5E+2y2iAj?@4q<46CiZYoLEbZH%T5q+3-rVzj5k zV5?AWOzgYWv2ViP6dy?*yYXl|)79+#=JP*#PJ8k7m&S*4x#7Oq)5AUD=+Ul`+0){B z?u8fRCFOa?^V=2mCj4low$au)K|)L_0tC)rbU}>`!2mUjW=)SI^!9Giol*z-2Qj+R z2tU%%h({RW!%*PX<*+~AzUS31w;vrz)ko~n3G2(RUb=O~|F~aFh5dtrQ=PxJC7R;x z4UvwXJ>r#*F5Rm5xh`(6YrX0*RLL%i(R1`y^gjLZ=y!hoj-rLmKc(p2pZ#P_CH-$+ zf9=BbiE*pNC+OOrjH-$_m{bhqk1i!ZNe^ObIu%GUgfd%$iF#DwDUccnsI#Q1fgA*B9Z!CVJw5_?`k(9(e$9+-xU&#%&9DD73o1wL&OLR!xL>pTtcF~7d(Cm zfo1FN>h>7z-B`Z^BjP|;uSkhUME~GnF?gV>yNiYU(|ggt?~q7G_ljuafx&cRv?1bH z+$&mzXo$cZBp$L<0PI&aW8Wf)>%#_%^QV?u9e3Dg8UwkpQd7Qbg^#^Y~M z`)h?3_tw=lH)={&Y4$lM{!l9b9rX3IKy4S90iUx((v!w4J^;#hP-r+qCGO&-4n$g-2jMdjD z-dk@y=JSR_heL|e6YCTr&@Xxv1?{lc_n6OD8)QwZuOCU+(Sk3< zzoQ9-edy%f=@by6d#4kQ9MtOV|5V;qzD^y~IvQ)=UF*?E5tAxT6{U{4}2our27u8`^vH24dR^JFL}Rsf zQIcRnB03_v9f`vAeMb;N`3>HyaRrV>4D_c^>kSbIc{gx`2L}4Puwxr7A`;!-hTXw6 zTwmwzK(T&{?Zlv;`0>Z2l0b(uF8LQk#9$8|7S21 zjfRvP;V73Z+kez<{^Zw4_!f0nOWXs7#asqVKWdFTec$|4`-5${OSjv%)_&d+S5h(O zD}L%~-})vG%AYH)M7;RPpE+Zm!I$%t--o~VZ{Pc3c=#i1w$1_i%fC2}vyrGuZ=c7B zFpZ{{&q?P&ALrgfS}(WpWlMKI23t~Y00+t_wXVuLpfP$$=~32I9C=PpgFfK=#G_C@ zjnM|U9|qPO3E?FAmmT{9kX6SLIJXB}ULg-1ba|Xkbj0QPwlv}LItA#s%NvlNc6mP~ z=t-9kP(5y%xS?r2jB}p8_`n4{)Gd3AGsdAXb_ zl~>M{%BvgG^NSaji|Y>Wd`{1=Z=5Tyn)_0HPJ42Gb$-3LIX}l1H=bEcZ*DG_3+v?- zbHcS{uC15P&zCk+OPiZ(2YY+(9)q$}rZu`q>vWD5X^A$;q#j5g0!`x@&aX*lfiQur zqR!DO6|t3=_+CJdB}cP?_9)&3v{oUV2Vxm~ic0vdz&_JeEc!Hbi|DzG{%mnueup~^ z#5$1YoE|3agO$F!kdx4_I+&s}(j3OE`nW;Qz;+taO?blItUIHdNRWH|GBIz~orj$g z&?#s3n~)x)Ui?059>|#O`oPWpfe&suLdB(0$0f}}+J%kzbshglt#5it<;7%idDAEr zS4;EDTB*2Pf(6`;@LxdqK3h%X{I~}Bm1LWePUXh)rg{7g3Y|PIjhQn!*&mZV`ND#^ zbt)&7&f;HtQNpE?bvD-4CMho|YmHsS7}-K5DTOV~!a`CiwmE0Y>r+zeI&-y0__O1s z@#!aXZB|=sD`(27sa%^J&BshR#N1FmZ&s?H;+*V($ZeUjkM(_wygrpP;lx(al>Vt) z0TPpSe&z<58!W^M`FuVmMIxWKBu(Y!^ZBGyZFAg|T4xas>Dj5A)UAv(tPI@GOHoKl z&9>l|IakroW=z&p2j?&J<0Yxa+aS-HTjmyQSNil$OzUK>Fcm9K=W|vb+R>RDR56~E zJ91L$w)A8ZSFxNh7rAtLnAQ0msiiV8r;IR>FsM0GMFq3_lHynLnmjbf3IFyuWJJmRxl>0vqiVQ0O-jAiA zXs4MnC%G~a!ed)olh!15f*kg2?0G2H{rqny#vi~I5=Hjo&w)r^HZi}IvP^S$3$_OD zsLfQ>hSXpaC{q@=m`6|Mt|_K!#;z${>YjXtH#jf0N{ojQtqGJCR`K3CK~alDbthM{ zg*i*A+2S1b7$sYbfiL7yn4j)bgiF|Ut%>4L%)$s0m=}mM3hX^#5IDj+p@!;4>~$m( z_U%u30*gg-?sS~M7sm*1VRyztmK@+1m>6B}a${f(BQ!&fSb9;tra56v@?gAL4?7{_ z3AhoZ*<8vT#=*xPyFwVd6LO;ytz%H_9Ie%bx<9!Jmb)_WF)0ta9%Q$drodD5R zl!t7~OmWmEupzwxw#8}Nc;zxGG4tl%mJ(v+&4kWfU_ZU5F%fZAJa)^^< z3*~s+tKCZvQQN%6iL;`BiJmkLGNVh_Iy7-6(=8*7{5*Saol)c z8McOEZFjAATi*3C!@<}NYL@5U#*t?i-FfUxc??r-s1_%Va@1jcO724~j(>)H0(-59 z)XOw5leX*!Im%HRM_kPboLSp(I%ab^%A*MDqxMyzBVdk$5zL&huL?)@f$rxt7 zz!+wpVhl4+Gv+XuFEWOi0%MpdGKQJ6wmh&C<`OsLA&BQ3ZW!FWlTQ#og2sX^hj!45 z+;GrK4#((o4#&{*wtRR8`Xz2S&}D~X=!(NJbk&vzcc5i%IM6kRW9aWX97EUbYd%fc zz8Pf_(mOBJj;Uw)s7Vqo+5a$&3y63Xn{xG9y;dPTgV|G@nQMMUc&l899L~tRsa%KJ z2dO9XCXH5kmBuO^;^mXLHNKpysB>eLE^fW)y@Y>4M_(@C>I4Wb$N6hDil~XdA#Q(7 MYQK+zVvOj20kw&X-v9sr literal 0 HcmV?d00001 diff --git a/examples/sidebar/images/ferris.png b/examples/sidebar/images/ferris.png new file mode 100644 index 0000000000000000000000000000000000000000..ebce1a1456b94819d6bb751a332ebc0801f1bfd4 GIT binary patch literal 33061 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 literal 0 HcmV?d00001 diff --git a/examples/sidebar/src/counter.rs b/examples/sidebar/src/counter.rs new file mode 100644 index 00000000..c3ca5dfb --- /dev/null +++ b/examples/sidebar/src/counter.rs @@ -0,0 +1,66 @@ +use iced::{ + widget::{Button, Column, Container, Row, Text}, + Alignment, Element, +}; +use iced_aw::sidebar::TabLabel; + +use crate::{Icon, Message, Tab}; + +#[derive(Debug, Clone)] +pub enum CounterMessage { + Increase, + Decrease, +} + +#[derive(Default)] +pub struct CounterTab { + value: i32, +} + +impl CounterTab { + pub fn new() -> Self { + CounterTab { value: 0 } + } + + pub fn update(&mut self, message: CounterMessage) { + match message { + CounterMessage::Increase => self.value += 1, + CounterMessage::Decrease => self.value -= 1, + } + } +} + +impl Tab for CounterTab { + type Message = Message; + + fn title(&self) -> String { + String::from("Counter") + } + + fn tab_label(&self) -> TabLabel { + //TabLabel::Text(self.title()) + TabLabel::IconText(Icon::Calc.into(), self.title()) + } + + fn content(&self) -> Element<'_, Self::Message> { + let content: Element<'_, CounterMessage> = Container::new( + Column::new() + .align_x(Alignment::Center) + .max_width(600) + .padding(20) + .spacing(16) + .push(Text::new(format!("Count: {}", self.value)).size(32)) + .push( + Row::new() + .spacing(10) + .push(Button::new(Text::new("Decrease")).on_press(CounterMessage::Decrease)) + .push( + Button::new(Text::new("Increase")).on_press(CounterMessage::Increase), + ), + ), + ) + .into(); + + content.map(Message::Counter) + } +} diff --git a/examples/sidebar/src/ferris.rs b/examples/sidebar/src/ferris.rs new file mode 100644 index 00000000..5a0a20cb --- /dev/null +++ b/examples/sidebar/src/ferris.rs @@ -0,0 +1,79 @@ +use iced::{ + widget::{Column, Container, Image, Slider, Text}, + Alignment, Element, Length, +}; +use iced_aw::sidebar::TabLabel; + +use crate::{Icon, Message, Tab}; + +#[derive(Debug, Clone)] +pub enum FerrisMessage { + ImageWidthChanged(f32), +} + +#[derive(Default)] +pub struct FerrisTab { + ferris_width: f32, +} + +impl FerrisTab { + pub fn new() -> Self { + FerrisTab { + ferris_width: 100.0, + } + } + + pub fn update(&mut self, message: FerrisMessage) { + match message { + FerrisMessage::ImageWidthChanged(value) => self.ferris_width = value, + } + } +} + +impl Tab for FerrisTab { + type Message = Message; + + fn title(&self) -> String { + String::from("Ferris") + } + + fn tab_label(&self) -> TabLabel { + TabLabel::IconText(Icon::Heart.into(), self.title()) + } + + fn content(&self) -> Element<'_, Self::Message> { + let content: Element<'_, FerrisMessage> = Container::new( + Column::new() + .align_x(Alignment::Center) + .max_width(600) + .padding(20) + .spacing(16) + .push(Text::new(if self.ferris_width == 500.0 { + "Hugs!!!" + } else { + "Pull me closer!" + })) + .push(ferris(self.ferris_width)) + .push(Slider::new( + 100.0..=500.0, + self.ferris_width, + FerrisMessage::ImageWidthChanged, + )), + ) + .align_x(iced::alignment::Horizontal::Center) + .into(); + + content.map(Message::Ferris) + } +} + +fn ferris<'a>(width: f32) -> Container<'a, FerrisMessage> { + Container::new(if cfg!(target_arch = "wasm32") { + Image::new("images/ferris.png") + } else { + Image::new(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) + .width(Length::Fixed(width)) + }) + .width(Length::Fill) + .center_x(Length::Fill) +} diff --git a/examples/sidebar/src/login.rs b/examples/sidebar/src/login.rs new file mode 100644 index 00000000..d0cf6386 --- /dev/null +++ b/examples/sidebar/src/login.rs @@ -0,0 +1,98 @@ +use iced::{ + alignment::{Horizontal, Vertical}, + widget::{Button, Column, Container, Row, Text, TextInput}, + Alignment, Element, Length, +}; +use iced_aw::sidebar::TabLabel; + +use crate::{Icon, Message, Tab}; + +#[derive(Debug, Clone)] +pub enum LoginMessage { + UsernameChanged(String), + PasswordChanged(String), + ClearPressed, + LoginPressed, +} + +#[derive(Default)] +pub struct LoginTab { + username: String, + password: String, +} + +impl LoginTab { + pub fn new() -> Self { + LoginTab { + username: String::new(), + password: String::new(), + } + } + + pub fn update(&mut self, message: LoginMessage) { + match message { + LoginMessage::UsernameChanged(value) => self.username = value, + LoginMessage::PasswordChanged(value) => self.password = value, + LoginMessage::ClearPressed => { + self.username = String::new(); + self.password = String::new(); + } + LoginMessage::LoginPressed => {} + } + } +} + +impl Tab for LoginTab { + type Message = Message; + + fn title(&self) -> String { + String::from("Login") + } + + fn tab_label(&self) -> TabLabel { + //TabLabel::Text(self.title()) + TabLabel::IconText(Icon::User.into(), self.title()) + } + + fn content(&self) -> Element<'_, Self::Message> { + let content: Element<'_, LoginMessage> = Container::new( + Column::new() + .align_x(Alignment::Center) + .max_width(600) + .padding(20) + .spacing(16) + .push( + TextInput::new("Username", &self.username) + .on_input(LoginMessage::UsernameChanged) + .padding(10) + .size(32), + ) + .push( + TextInput::new("Password", &self.password) + .on_input(LoginMessage::PasswordChanged) + .padding(10) + .size(32) + .secure(true), + ) + .push( + Row::new() + .spacing(10) + .push( + Button::new(Text::new("Clear").align_x(Horizontal::Center)) + .width(Length::Fill) + .on_press(LoginMessage::ClearPressed), + ) + .push( + Button::new(Text::new("Login").align_x(Horizontal::Center)) + .width(Length::Fill) + .on_press(LoginMessage::LoginPressed), + ), + ), + ) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .into(); + + content.map(Message::Login) + } +} diff --git a/examples/sidebar/src/main.rs b/examples/sidebar/src/main.rs new file mode 100644 index 00000000..3e0a2771 --- /dev/null +++ b/examples/sidebar/src/main.rs @@ -0,0 +1,159 @@ +mod login; +use iced::{ + alignment::{Horizontal, Vertical}, + widget::{Column, Container, Text}, + Element, Font, Length, +}; +use iced_aw::sidebar::{SidebarWithContent, TabLabel}; +use login::{LoginMessage, LoginTab}; + +mod ferris; +use ferris::{FerrisMessage, FerrisTab}; + +mod counter; +use counter::{CounterMessage, CounterTab}; + +mod settings; +use settings::{style_from_index, SettingsMessage, SettingsTab, SidebarPosition}; + +const HEADER_SIZE: u16 = 32; +const TAB_PADDING: u16 = 16; +const ICON_BYTES: &[u8] = include_bytes!("../fonts/icons.ttf"); +const ICON: Font = Font::with_name("icons"); + +enum Icon { + User, + Heart, + Calc, + CogAlt, +} + +impl From for char { + fn from(icon: Icon) -> Self { + match icon { + Icon::User => '\u{E800}', + Icon::Heart => '\u{E801}', + Icon::Calc => '\u{F1EC}', + Icon::CogAlt => '\u{E802}', + } + } +} + +fn main() -> iced::Result { + iced::application("Sidebar example", TabBarExample::update, TabBarExample::view) + .font(iced_aw::BOOTSTRAP_FONT_BYTES) + .font(ICON_BYTES) + .run() +} + +#[derive(Default)] +struct TabBarExample { + active_tab: TabId, + login_tab: LoginTab, + ferris_tab: FerrisTab, + counter_tab: CounterTab, + settings_tab: SettingsTab, +} + +#[derive(Clone, PartialEq, Eq, Debug, Default)] +enum TabId { + #[default] + Login, + Ferris, + Counter, + Settings, +} + +#[derive(Clone, Debug)] +enum Message { + TabSelected(TabId), + Login(LoginMessage), + Ferris(FerrisMessage), + Counter(CounterMessage), + Settings(SettingsMessage), + TabClosed(TabId), +} + +impl TabBarExample { + fn update(&mut self, message: Message) { + match message { + Message::TabSelected(selected) => self.active_tab = selected, + Message::Login(message) => self.login_tab.update(message), + Message::Ferris(message) => self.ferris_tab.update(message), + Message::Counter(message) => self.counter_tab.update(message), + Message::Settings(message) => self.settings_tab.update(message), + Message::TabClosed(id) => println!("Tab {:?} event hit", id), + } + } + + fn view(&self) -> Element<'_, Message> { + let position = self + .settings_tab + .settings() + .sidebar_position + .unwrap_or_default(); + let theme = self + .settings_tab + .settings() + .sidebar_theme + .unwrap_or_default(); + + SidebarWithContent::new(Message::TabSelected) + .tab_icon_position(iced_aw::sidebar::Position::End) + .on_close(Message::TabClosed) + .push( + TabId::Login, + self.login_tab.tab_label(), + self.login_tab.view(), + ) + .push( + TabId::Ferris, + self.ferris_tab.tab_label(), + self.ferris_tab.view(), + ) + .push( + TabId::Counter, + self.counter_tab.tab_label(), + self.counter_tab.view(), + ) + .push( + TabId::Settings, + self.settings_tab.tab_label(), + self.settings_tab.view(), + ) + .set_active_tab(&self.active_tab) + .sidebar_style(style_from_index(theme)) + .icon_font(ICON) + .sidebar_position(match position { + SidebarPosition::Start => iced_aw::sidebar::SidebarPosition::Start, + SidebarPosition::End => iced_aw::sidebar::SidebarPosition::End, + }) + .into() + } +} + +trait Tab { + type Message; + + fn title(&self) -> String; + + fn tab_label(&self) -> TabLabel; + + fn view(&self) -> Element<'_, Self::Message> { + let column = Column::new() + .spacing(20) + .push(Text::new(self.title()).size(HEADER_SIZE)) + .push(self.content()) + .align_x(iced::Alignment::Center); + + Container::new(column) + .width(Length::Fill) + .height(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .padding(TAB_PADDING) + .into() + } + + fn content(&self) -> Element<'_, Self::Message>; +} diff --git a/examples/sidebar/src/settings.rs b/examples/sidebar/src/settings.rs new file mode 100644 index 00000000..28b587bb --- /dev/null +++ b/examples/sidebar/src/settings.rs @@ -0,0 +1,154 @@ +use crate::{Icon, Message, Tab}; +use iced::{ + widget::{Column, Container, Radio, Text}, + Element, +}; +use iced_aw::sidebar::TabLabel; +use iced_aw::style::{sidebar, StyleFn}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SidebarPosition { + #[default] + Start, + End, +} + +impl SidebarPosition { + pub const ALL: [SidebarPosition; 2] = [SidebarPosition::Start, SidebarPosition::End]; +} + +impl From for String { + fn from(position: SidebarPosition) -> Self { + String::from(match position { + SidebarPosition::Start => "Start", + SidebarPosition::End => "End", + }) + } +} + +//#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] +pub struct TabSettings { + pub sidebar_position: Option, + pub sidebar_theme: Option, + pub sidebar_theme_id: Option, +} + +impl TabSettings { + pub fn new() -> Self { + TabSettings { + sidebar_position: Some(SidebarPosition::Start), + sidebar_theme: Some(0), + sidebar_theme_id: Some(0), + } + } +} + +#[derive(Debug, Clone)] +pub enum SettingsMessage { + PositionSelected(SidebarPosition), + ThemeSelected(usize), +} + +#[derive(Default)] +pub struct SettingsTab { + settings: TabSettings, +} + +impl SettingsTab { + pub fn new() -> Self { + SettingsTab { + settings: TabSettings::new(), + } + } + + pub fn settings(&self) -> &TabSettings { + &self.settings + } + + pub fn update(&mut self, message: SettingsMessage) { + match message { + SettingsMessage::PositionSelected(position) => { + self.settings.sidebar_position = Some(position) + } + SettingsMessage::ThemeSelected(index) => { + self.settings.sidebar_theme_id = Some(index); + self.settings.sidebar_theme = Some(index) + } + } + } +} + +impl Tab for SettingsTab { + type Message = Message; + + fn title(&self) -> String { + String::from("Settings") + } + + fn tab_label(&self) -> TabLabel { + //TabLabel::Text(self.title()) + TabLabel::IconText(Icon::CogAlt.into(), self.title()) + } + + fn content(&self) -> Element<'_, Self::Message> { + let content: Element<'_, SettingsMessage> = Container::new( + Column::new() + .push(Text::new("TabBar position:").size(20)) + .push(SidebarPosition::ALL.iter().cloned().fold( + Column::new().padding(10).spacing(10), + |column, position| { + column.push( + Radio::new( + position, + position, + self.settings().sidebar_position, + SettingsMessage::PositionSelected, + ) + .size(16), + ) + }, + )) + .push(Text::new("TabBar color:").size(20)) + .push( + (0..6).fold(Column::new().padding(10).spacing(10), |column, id| { + column.push( + Radio::new( + predefined_style(id), + id, + self.settings().sidebar_theme_id, + SettingsMessage::ThemeSelected, + ) + .size(16), + ) + }), + ), + ) + .into(); + + content.map(Message::Settings) + } +} + +fn predefined_style(index: usize) -> String { + match index { + 0 => "Default".to_owned(), + 1 => "Dark".to_owned(), + 2 => "Red".to_owned(), + 3 => "Blue".to_owned(), + 4 => "Green".to_owned(), + 5 => "Purple".to_owned(), + _ => "Default".to_owned(), + } +} + +pub fn style_from_index(index: usize) -> StyleFn<'static, iced::Theme, sidebar::Style> { + match index { + 0 => Box::new(sidebar::primary), + 1 => Box::new(sidebar::dark), + 2 => Box::new(sidebar::red), + 3 => Box::new(sidebar::blue), + 4 => Box::new(sidebar::green), + 5 => Box::new(sidebar::purple), + _ => Box::new(sidebar::primary), + } +} diff --git a/src/lib.rs b/src/lib.rs index eb284467..b453a56f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,6 +146,10 @@ mod platform { #[doc(no_inline)] #[cfg(feature = "drop_down")] pub use {crate::widgets::drop_down, drop_down::DropDown}; + + #[doc(no_inline)] + #[cfg(feature = "sidebar")] + pub use crate::widgets::sidebar; } #[doc(no_inline)] diff --git a/src/style.rs b/src/style.rs index 9c5e61a3..294f4a02 100644 --- a/src/style.rs +++ b/src/style.rs @@ -35,3 +35,6 @@ pub mod menu_bar; #[cfg(feature = "context_menu")] pub mod context_menu; + +#[cfg(feature = "sidebar")] +pub mod sidebar; diff --git a/src/style/sidebar.rs b/src/style/sidebar.rs new file mode 100644 index 00000000..50af00dd --- /dev/null +++ b/src/style/sidebar.rs @@ -0,0 +1,205 @@ +//! This is the style for [`Sidebar`](crate::widgets::sidebar::Sidebar) and +//! [`SidebarWithContent`](crate::widgets::sidebar::SidebarWithContent). +//! +//! *This API requires the following crate features to be activated: `sidebar`* + +use super::{Status, StyleFn}; +use iced::{border::Radius, Background, Color, Theme}; + +/// The appearance of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[derive(Clone, Copy, Debug)] +pub struct Style { + /// The background of the sidebar. + pub background: Option, + + /// The border color of the sidebar. + pub border_color: Option, + + /// The border width of the sidebar. + pub border_width: f32, + + /// The background of the tab labels. + pub tab_label_background: Background, + + /// The border color of the tab labels. + pub tab_label_border_color: Color, + + /// The border with of the tab labels. + pub tab_label_border_width: f32, + + /// The icon color of the tab labels. + pub icon_color: Color, + + /// The color of the closing icon border + pub icon_background: Option, + + /// How soft/hard the corners of the icon border are + pub icon_border_radius: Radius, + + /// The text color of the tab labels. + pub text_color: Color, +} + +impl Default for Style { + fn default() -> Self { + Self { + background: None, + border_color: None, + border_width: 0.0, + tab_label_background: Background::Color([0.87, 0.87, 0.87].into()), + tab_label_border_color: [0.7, 0.7, 0.7].into(), + tab_label_border_width: 1.0, + icon_color: Color::BLACK, + icon_background: Some(Background::Color(Color::TRANSPARENT)), + icon_border_radius: 4.0.into(), + text_color: Color::BLACK, + } + } +} +/// The Catalog of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +pub trait Catalog { + ///Style for the trait to use. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self, Style>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The primary theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn primary(theme: &Theme, status: Status) -> Style { + let mut base = Style::default(); + let palette = theme.extended_palette(); + + base.text_color = palette.background.base.text; + + match status { + Status::Disabled => { + base.tab_label_background = Background::Color(palette.background.strong.color); + } + Status::Hovered => { + base.tab_label_background = Background::Color(palette.primary.strong.color); + } + _ => { + base.tab_label_background = Background::Color(palette.primary.base.color); + } + } + + base +} + +/// The dark theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn dark(_theme: &Theme, status: Status) -> Style { + let mut base = Style { + tab_label_background: Background::Color([0.1, 0.1, 0.1].into()), + tab_label_border_color: [0.3, 0.3, 0.3].into(), + icon_color: Color::WHITE, + text_color: Color::WHITE, + ..Default::default() + }; + + if status == Status::Disabled { + base.tab_label_background = Background::Color([0.13, 0.13, 0.13].into()); + } + + base +} + +/// The red theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn red(_theme: &Theme, status: Status) -> Style { + let mut base = Style { + tab_label_background: Background::Color([1.0, 0.0, 0.0].into()), + tab_label_border_color: Color::TRANSPARENT, + tab_label_border_width: 0.0, + icon_color: Color::WHITE, + text_color: Color::WHITE, + ..Default::default() + }; + + if status == Status::Disabled { + base.tab_label_background = Background::Color([0.13, 0.13, 0.13].into()); + base.icon_color = Color::BLACK; + base.text_color = Color::BLACK; + } + + base +} + +/// The blue theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn blue(_theme: &Theme, status: Status) -> Style { + let mut base = Style { + tab_label_background: Background::Color([0.0, 0.0, 1.0].into()), + tab_label_border_color: [0.0, 0.0, 1.0].into(), + icon_color: Color::WHITE, + text_color: Color::WHITE, + ..Default::default() + }; + + if status == Status::Disabled { + base.tab_label_background = Background::Color([0.5, 0.5, 1.0].into()); + base.tab_label_border_color = [0.5, 0.5, 1.0].into(); + } + + base +} + +/// The blue theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn green(_theme: &Theme, status: Status) -> Style { + let mut base = Style { + tab_label_background: Color::WHITE.into(), + icon_color: [0.0, 0.5, 0.0].into(), + text_color: [0.0, 0.5, 0.0].into(), + ..Default::default() + }; + + match status { + Status::Disabled => { + base.icon_color = [0.7, 0.7, 0.7].into(); + base.text_color = [0.7, 0.7, 0.7].into(); + base.tab_label_border_color = [0.7, 0.7, 0.7].into(); + } + _ => { + base.tab_label_border_color = [0.0, 0.5, 0.0].into(); + } + } + + base +} + +/// The purple theme of a [`Sidebar`](crate::widgets::sidebar::Sidebar). +#[must_use] +pub fn purple(_theme: &Theme, status: Status) -> Style { + let mut base = Style { + tab_label_background: Color::WHITE.into(), + tab_label_border_color: Color::TRANSPARENT, + icon_color: [0.7, 0.0, 1.0].into(), + text_color: [0.7, 0.0, 1.0].into(), + ..Default::default() + }; + + if status == Status::Disabled { + base.icon_color = Color::BLACK; + base.text_color = Color::BLACK; + } + + base +} diff --git a/src/widgets.rs b/src/widgets.rs index d5281a03..bb0fe3ec 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -119,3 +119,12 @@ pub mod drop_down; /// A drop down menu pub type DropDown<'a, Overlay, Message, Renderer> = drop_down::DropDown<'a, Overlay, Message, Renderer>; + +#[cfg(feature = "sidebar")] +pub mod sidebar; +/// A sidebar to show tabs on the side. +pub type Sidebar<'a, Message, TabId, Theme, Renderer> = + sidebar::Sidebar<'a, Message, TabId, Theme, Renderer>; +/// A [`SidebarWithContent`] widget for showing a [`Sidebar`](super::sidebar::SideBar) +pub type SidebarWithContent<'a, Message, TabId, Theme, Renderer> = + sidebar::SidebarWithContent<'a, Message, TabId, Theme, Renderer>; diff --git a/src/widgets/sidebar.rs b/src/widgets/sidebar.rs new file mode 100644 index 00000000..de90e9a2 --- /dev/null +++ b/src/widgets/sidebar.rs @@ -0,0 +1,12 @@ +//! Contains the sidebar related widgets and data enums. + +#[allow(clippy::module_inception)] +pub mod sidebar; +pub use sidebar::*; +pub mod column; +pub use column::*; + +// Not used by `Sidebar` itself, but included for completeness. +// The horizontal version of the vertical `Column` for the `Row`. +pub mod row; +pub use row::*; diff --git a/src/widgets/sidebar/column.rs b/src/widgets/sidebar/column.rs new file mode 100644 index 00000000..f44b83d0 --- /dev/null +++ b/src/widgets/sidebar/column.rs @@ -0,0 +1,428 @@ +//! Distribute content rows vertically while setting the row width to widest row. +//! For alignment [`Alignment::Start`] the last element of the row is flushed to the end. +//! For alignment [`Alignment::End`] the first element of the row is flushed to the start. +//! +//! Future: Idea to implement leaders before/after the flushed element for `Start`/`End` +//! alignments. + +use iced::{ + advanced::{ + layout::{self, Node}, + mouse, overlay, renderer, + widget::{tree::Tree, Operation}, + Clipboard, Layout, Shell, Widget, + }, + alignment, + event::{self, Event}, + widget::Row, + Alignment, Element, Length, Padding, Pixels, Point, Rectangle, Size, Vector, +}; + +/// A container that distributes its contents vertically. +#[allow(missing_debug_implementations)] +pub struct FlushColumn<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, + flush: bool, +} + +impl<'a, Message: 'a, Theme: 'a, Renderer> FlushColumn<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::Renderer + 'a, +{ + /// Creates an empty [`Column`]. + #[must_use] + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Column`] with the given capacity. + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Column`] with the given elements. + #[must_use] + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Column`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Column::width`] or [`Column::height`] accordingly. + #[must_use] + pub fn from_vec(children: Vec>) -> Self { + let children = children + .into_iter() + .map(|x| Element::into(x.into())) + .collect(); + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align: Alignment::Start, + clip: false, + children, + flush: true, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + #[must_use] + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + #[must_use] + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + #[must_use] + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + #[must_use] + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + #[must_use] + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Column`] should be clipped on + /// overflow. + #[must_use] + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Sets whether the end row element is flushed to the end when the alignment is set to Start, + /// or the start row element is flushed to the start when the alignment is set to End. + /// No effect for alignment set to Center. + #[must_use] + pub fn flush(mut self, flush: bool) -> Self { + self.flush = flush; + self + } + + /// Adds an element to the [`Column`]. + #[must_use] + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.size_hint(); + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + self.children.push(child.into()); + self + } + + /// Adds an element to the [`Column`], if `Some`. + #[must_use] + pub fn push_maybe(self, child: Option>>) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Column`] with the given children. + #[must_use] + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +#[allow(clippy::mismatching_type_param_order)] +impl<'a, Message: 'a, Renderer> Default for FlushColumn<'a, Message, Renderer> +where + Renderer: iced::advanced::Renderer + 'a, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message: 'a, Theme: 'a, Renderer: iced::advanced::Renderer + 'a> + FromIterator> for FlushColumn<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl<'a, Message: 'a, Theme: 'a, Renderer> Widget + for FlushColumn<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.max_width(self.max_width); + let node = layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ); + let mut container_x = std::f32::MAX; + let mut container_width = 0.0f32; + for row in node.children() { + if row.size().width > container_width { + container_width = row.size().width; + } + if row.bounds().x < container_x { + container_x = row.bounds().x; + } + } + let mut children = Vec::::new(); + for row in node.children() { + let mut row_children = Vec::::new(); + let bounds = row.bounds(); + let width_diff = container_width - bounds.width; + if !row.children().is_empty() { + for element in row.children() { + let bounds = element.bounds(); + let x = bounds.x + + match self.align { + Alignment::Start => 0.0, + Alignment::Center => width_diff / 2.0, + Alignment::End => width_diff, + }; + let mut element_node = + Node::with_children(element.size(), element.children().to_owned()); + element_node.move_to_mut(Point::new(x, bounds.y)); + row_children.push(element_node); + } + if row_children.len() > 1 { + match self.align { + Alignment::Start => { + let element = row_children.first().expect("Always exists."); + let bounds = element.bounds(); + let mut position = bounds.position(); + let mut element_node = + Node::with_children(bounds.size(), element.children().to_owned()); + position.x += width_diff; + element_node.move_to_mut(position); + let node = row_children.last_mut().expect("Always exists."); + *node = element_node; + } + Alignment::Center => {} + Alignment::End => { + let element = row_children.first().expect("Always exists."); + let bounds = element.bounds(); + let mut position = bounds.position(); + let mut element_node = + Node::with_children(bounds.size(), element.children().to_owned()); + position.x -= width_diff; + element_node.move_to_mut(position); + let node = row_children.first_mut().expect("Always exists."); + *node = element_node; + } + } + } + } + let mut row_node = + Node::with_children(Size::new(container_width, row.size().height), row_children); + row_node.move_to_mut(Point::new(container_x, bounds.y)); + children.push(row_node); + } + Node::with_children(node.size(), children) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> 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, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + 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, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + if self.clip { + &clipped_viewport + } else { + viewport + }, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::advanced::Renderer + 'a, +{ + fn from(column: FlushColumn<'a, Message, Theme, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/src/widgets/sidebar/row.rs b/src/widgets/sidebar/row.rs new file mode 100644 index 00000000..dc03400e --- /dev/null +++ b/src/widgets/sidebar/row.rs @@ -0,0 +1,433 @@ +//! Distribute content columns horizontally while setting the column height to highest column. +//! For alignment [`Alignment::Start`] the last element of the column is flushed to the end. +//! For alignment [`Alignment::End`] the first element of the column is flushed to the start. +//! +//! Future: Idea to implement leaders before/after the flushed element for `Start`/`End` +//! alignments. + +use iced::{ + advanced::{ + layout::{self, Node}, + mouse, overlay, renderer, + widget::{tree::Tree, Operation}, + Clipboard, Layout, Shell, Widget, + }, + alignment, + event::{self, Event}, + widget::Column, + Alignment, Element, Length, Padding, Pixels, Point, Rectangle, Size, Vector, +}; + +/// A container that distributes its contents horizontally. +#[allow(missing_debug_implementations)] +pub struct FlushRow<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_height: f32, + align: Alignment, + clip: bool, + children: Vec>, + flush: bool, +} + +impl<'a, Message: 'a, Theme: 'a, Renderer> FlushRow<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::Renderer + 'a, +{ + /// Creates an empty [`Row`]. + #[must_use] + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Row`] with the given capacity. + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Row`] with the given elements. + #[must_use] + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Row`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Row::width`] or [`Row::height`] accordingly. + #[must_use] + pub fn from_vec(children: Vec>) -> Self { + let children = children + .into_iter() + .map(|x| Element::into(x.into())) + .collect(); + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_height: f32::INFINITY, + align: Alignment::Start, + clip: false, + children, + flush: true, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + #[must_use] + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Row`]. + #[must_use] + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Row`]. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Row`]. + #[must_use] + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Row`]. + #[must_use] + pub fn max_height(mut self, max_height: impl Into) -> Self { + self.max_height = max_height.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Row`] . + #[must_use] + pub fn align_y(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Row`] should be clipped on + /// overflow. + #[must_use] + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Sets whether the end column element is flushed to the end when the alignment is set to + /// Start, or the start column element is flushed to the start when the alignment is set + /// to End. No effect for alignment set to Center. + #[must_use] + pub fn flush(mut self, flush: bool) -> Self { + self.flush = flush; + self + } + + /// Adds an element to the [`Row`]. + #[must_use] + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.size_hint(); + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + self.children.push(child.into()); + self + } + + /// Adds an element to the [`Row`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Row`] with the given children. + #[must_use] + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +#[allow(clippy::mismatching_type_param_order)] +impl<'a, Message: 'a, Renderer> Default for FlushRow<'a, Message, Renderer> +where + Renderer: iced::advanced::Renderer + 'a, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message: 'a, Theme: 'a, Renderer: iced::advanced::Renderer + 'a> + FromIterator> for FlushRow<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl<'a, Message, Theme, Renderer> Widget + for FlushRow<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.max_height(self.max_height); + let node = layout::flex::resolve( + layout::flex::Axis::Horizontal, + renderer, + &limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ); + let mut container_y = std::f32::MAX; + let mut container_height = 0.0f32; + for column in node.children() { + if column.size().height > container_height { + container_height = column.size().height; + } + if column.bounds().y < container_y { + container_y = column.bounds().y; + } + } + let mut children = Vec::::new(); + for column in node.children() { + let mut column_children = Vec::::new(); + let bounds = column.bounds(); + let height_diff = container_height - bounds.height; + if !column.children().is_empty() { + for element in column.children() { + let bounds = element.bounds(); + let y = bounds.y + + match self.align { + Alignment::Start => 0.0, + Alignment::Center => height_diff / 2.0, + Alignment::End => height_diff, + }; + let mut element_node = + Node::with_children(element.size(), element.children().to_owned()); + element_node.move_to_mut(Point::new(bounds.x, y)); + column_children.push(element_node); + } + if column_children.len() > 1 { + match self.align { + Alignment::Start => { + let element = column_children.first().expect("Always exists."); + let bounds = element.bounds(); + let mut position = bounds.position(); + let mut element_node = + Node::with_children(bounds.size(), element.children().to_owned()); + position.y += height_diff; + element_node.move_to_mut(position); + let node = column_children.last_mut().expect("Always exists."); + *node = element_node; + } + Alignment::Center => {} + Alignment::End => { + let element = column_children.first().expect("Always exists."); + let bounds = element.bounds(); + let mut position = bounds.position(); + let mut element_node = + Node::with_children(bounds.size(), element.children().to_owned()); + position.y -= height_diff; + element_node.move_to_mut(position); + let node = column_children.first_mut().expect("Always exists."); + *node = element_node; + } + } + } + } + let mut column_node = Node::with_children( + Size::new(column.size().width, container_height), + column_children, + ); + column_node.move_to_mut(Point::new(bounds.x, container_y)); + children.push(column_node); + } + Node::with_children(node.size(), children) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> 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, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + 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, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + if self.clip { + &clipped_viewport + } else { + viewport + }, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::advanced::Renderer + 'a, +{ + fn from(row: FlushRow<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} diff --git a/src/widgets/sidebar/sidebar.rs b/src/widgets/sidebar/sidebar.rs new file mode 100644 index 00000000..e4b9a626 --- /dev/null +++ b/src/widgets/sidebar/sidebar.rs @@ -0,0 +1,1528 @@ +//! There are two options available: [`Sidebar`] and [`SidebarWithContent`]. +//! +//! [`Sidebar`] is used to displays a side bar for selecting content to be displayed, and the +//! sidebar is normally to a side of displayed content. You have to manage the logic to show +//! the content by yourself. Mainly used to customise the sidebar, the content, or both. +//! +//! [`SidebarWithContent`] is an single widget containing both the sidebar and content area, +//! and it manages the displaying of the content. + +use super::column::FlushColumn; +use crate::{ + core::icons::{bootstrap::icon_to_string, Bootstrap, BOOTSTRAP_FONT}, + style::{ + sidebar::{self, Catalog, Style}, + Status, StyleFn, + }, +}; +use iced::{ + advanced::{ + layout::{Limits, Node}, + overlay, renderer, + widget::{ + tree::{State, Tag}, + Operation, Tree, + }, + Clipboard, Layout, Shell, Widget, + }, + alignment::{self, Horizontal, Vertical}, + event, + mouse::{self, Cursor}, + touch, + widget::{ + text::{self, LineHeight}, + Row, Text, + }, + Alignment, Background, Border, Color, Element, Event, Font, Length, Pixels, Point, Rectangle, + Shadow, Size, Vector, +}; +use std::marker::PhantomData; + +/// The default icon size. +const DEFAULT_ICON_SIZE: f32 = 16.0; +/// The default text size. +const DEFAULT_TEXT_SIZE: f32 = 16.0; +/// The default size of the close icon. +const DEFAULT_CLOSE_SIZE: f32 = 16.0; +/// The default padding between the tabs. +const DEFAULT_PADDING: f32 = 1.0; +/// The default spacing around the tabs. +const DEFAULT_SPACING: f32 = 0.0; + +/// A [`TabLabel`] showing an icon and/or a text on a tab +/// on a [`TabBar`](super::TabBar). +#[allow(missing_debug_implementations)] +#[derive(Clone, Hash)] +pub enum TabLabel { + /// A [`TabLabel`] showing only an icon on the tab. + Icon(char), + + /// A [`TabLabel`] showing only a text on the tab. + Text(String), + + /// A [`TabLabel`] showing an icon and a text on the tab. + IconText(char, String), +} + +#[derive(Clone, Copy, Default)] +/// The [`Position`] of the icon relative to text, this enum is only relative if +/// [`TabLabel::IconText`] is used. +pub enum Position { + /// Icon is placed at the start position. + #[default] + Start, + /// Icon is placed at the end position. + End, +} + +// +// +// ------ Just the sidebar. +// +// + +/// A sidebar to show tabs. +/// +/// # Example +/// ```ignore +/// # use iced_aw::sidebar::{TabLabel, Sidebar}; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// TabSelected(TabId), +/// } +/// +/// #[derive(PartialEq, Hash)] +/// enum TabId { +/// One, +/// Two, +/// Three, +/// } +/// +/// let sidebar = Sidebar::new( +/// Message::TabSelected, +/// ) +/// .push(TabId::One, TabLabel::Text(String::from("One"))) +/// .push(TabId::Two, TabLabel::Text(String::from("Two"))) +/// .push(TabId::Three, TabLabel::Text(String::from("Three"))) +/// .set_active_tab(&TabId::One); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Sidebar<'a, Message, TabId, Theme = iced::Theme, Renderer = iced::Renderer> +where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog, + TabId: Eq + Clone, +{ + /// The index of the currently active tab. + active_tab: usize, + /// The vector containing the labels of the tabs. + tab_labels: Vec, + /// The vector containing the indices of the tabs. + tab_indices: Vec, + /// The alignment of the tabs. + align_tabs: Alignment, + /// The function that produces the message when a tab is selected. + on_select: Box Message>, + /// The function that produces the message when the close icon was pressed. + on_close: Option Message>>, + /// The width of the [`Sidebar`]. + width: Length, + /// The height of the [`Sidebar`]. + height: Length, + /// The height of the tabs of the [`Sidebar`]. + tab_height: Length, + /// The icon size. + icon_size: f32, + /// The text size. + text_size: f32, + // The size of the close icon. + close_size: f32, + /// The padding of the tabs of the [`Sidebar`]. + padding: f32, + /// The spacing of the tabs of the [`Sidebar`]. + spacing: f32, + /// The optional icon font of the [`Sidebar`]. + font: Option, + /// The optional text font of the [`Sidebar`]. + text_font: Option, + /// The style of the [`Sidebar`]. + class: ::Class<'a>, + /// Where the icon is placed relative to text + position: Position, + /// Where to place the close icon on the tab + close_position: Position, + #[allow(clippy::missing_docs_in_private_items)] + _renderer: PhantomData, +} + +impl<'a, Message, TabId, Theme, Renderer> Sidebar<'a, Message, TabId, Theme, Renderer> +where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog, + TabId: Eq + Clone, +{ + /// Creates a new [`Sidebar`] with the index of the selected tab and a specified + /// message which will be send when a tab is selected by the user. + /// + /// It expects: + /// * the index of the currently active tab. + /// * the function that will be called if a tab is selected by the user. + /// It takes the index of the selected tab. + pub fn new(on_select: F) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + Self::with_tab_labels(Vec::new(), on_select) + } + + /// Similar to `new` but with a given Vector of the [`TabLabel`](crate::sidebar::TabLabel)s. + /// + /// It expects: + /// * the index of the currently active tab. + /// * a vector containing the [`TabLabel`]s of the [`Sidebar`]. + /// * the function that will be called if a tab is selected by the user. + /// It takes the index of the selected tab. + pub fn with_tab_labels(tab_labels: Vec<(TabId, TabLabel)>, on_select: F) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + Self { + active_tab: 0, + tab_indices: tab_labels.iter().map(|(id, _)| id.clone()).collect(), + tab_labels: tab_labels.into_iter().map(|(_, label)| label).collect(), + align_tabs: Alignment::Start, + on_select: Box::new(on_select), + on_close: None, + width: Length::Shrink, + height: Length::Fill, + tab_height: Length::Shrink, + icon_size: DEFAULT_ICON_SIZE, + text_size: DEFAULT_TEXT_SIZE, + close_size: DEFAULT_CLOSE_SIZE, + padding: DEFAULT_PADDING, + spacing: DEFAULT_SPACING, + font: None, + text_font: None, + class: ::default(), + position: Position::Start, + close_position: Position::End, + _renderer: PhantomData, + } + } + + /// Sets the alignment of the tabs for the [`Sidebar`]. + #[must_use] + pub fn align_tabs(mut self, align: Alignment) -> Self { + self.align_tabs = align; + self + } + + /// Sets the size of the close icon of the + /// [`TabLabel`](crate::sidebar::TabLabel)s of the [`Sidebar`]. + #[must_use] + pub fn close_size(mut self, close_size: f32) -> Self { + self.close_size = close_size; + self + } + + /// Gets the id of the currently active tab on the [`Sidebar`]. + #[must_use] + pub fn get_active_tab_id(&self) -> Option<&TabId> { + self.tab_indices.get(self.active_tab) + } + + /// Gets the index of the currently active tab on the [`Sidebar`]. + #[must_use] + pub fn get_active_tab_idx(&self) -> usize { + self.active_tab + } + + /// Gets the width of the [`Sidebar`]. + #[must_use] + pub fn get_height(&self) -> Length { + self.height + } + + /// Gets the width of the [`Sidebar`]. + #[must_use] + pub fn get_width(&self) -> Length { + self.width + } + + /// Sets the height of the [`Sidebar`]. + #[must_use] + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the font of the icons of the + /// [`TabLabel`](crate::sidebar::TabLabel)s of the [`Sidebar`]. + #[must_use] + pub fn icon_font(mut self, font: Font) -> Self { + self.font = Some(font); + self + } + + /// Sets the icon size of the [`TabLabel`](crate::sidebar::TabLabel)s of the [`Sidebar`]. + #[must_use] + pub fn icon_size(mut self, icon_size: f32) -> Self { + self.icon_size = icon_size; + self + } + + /// Sets the message that will be produced when the close icon of a tab + /// on the [`Sidebar`] is pressed. + /// + /// Setting this enables the drawing of a close icon on the tabs. + #[must_use] + pub fn on_close(mut self, on_close: F) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + self.on_close = Some(Box::new(on_close)); + self + } + + /// Sets the padding of the tabs of the [`Sidebar`]. + #[must_use] + pub fn padding(mut self, padding: f32) -> Self { + self.padding = padding; + self + } + + /// Pushes a [`TabLabel`](crate::sidebar::TabLabel) to the [`Sidebar`]. + #[must_use] + pub fn push(mut self, id: TabId, tab_label: TabLabel) -> Self { + self.tab_labels.push(tab_label); + self.tab_indices.push(id); + self + } + + /// Gets the amount of tabs on the [`Sidebar`]. + #[must_use] + pub fn size(&self) -> usize { + self.tab_indices.len() + } + + /// Sets the spacing between the tabs of the [`Sidebar`]. + #[must_use] + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + /// Sets the font of the text of the + /// [`TabLabel`](crate::sidebar::TabLabel)s of the [`Sidebar`]. + #[must_use] + pub fn text_font(mut self, text_font: Font) -> Self { + self.text_font = Some(text_font); + self + } + + /// Sets the text size of the [`TabLabel`](crate::sidebar::TabLabel)s of the [`Sidebar`]. + #[must_use] + pub fn text_size(mut self, text_size: f32) -> Self { + self.text_size = text_size; + self + } + + /// Sets the height of a tab on the [`Sidebar`]. + #[must_use] + pub fn tab_height(mut self, height: Length) -> Self { + self.tab_height = height; + self + } + + /// Sets up the active tab on the [`Sidebar`]. + #[must_use] + pub fn set_active_tab(mut self, active_tab: &TabId) -> Self { + self.active_tab = self + .tab_indices + .iter() + .position(|id| id == active_tab) + .map_or(0, |a| a); + self + } + + #[must_use] + /// Sets the [`Position`] of the close icon on the tab. + /// Only used when [`Sidebar::on_close()`] is used. + pub fn set_close_position(mut self, position: Position) -> Self { + self.close_position = position; + self + } + + #[must_use] + /// Sets the [`Position`] of the Icon next to Text. Only used in [`TabLabel::IconText`]. + pub fn set_position(mut self, position: Position) -> Self { + self.position = position; + self + } + + /// Sets the style of the [`Sidebar`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + ::Class<'a>: From>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into(); + self + } + + /// Sets the class of the input of the [`Sidebar`]. + #[must_use] + pub fn class(mut self, class: impl Into<::Class<'a>>) -> Self { + self.class = class.into(); + self + } + + /// Sets the width of the [`Sidebar`]. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } +} + +impl<'a, Message, TabId, Theme, Renderer> Widget + for Sidebar<'a, Message, TabId, Theme, Renderer> +where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog + text::Catalog, + TabId: Eq + Clone, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + fn layout_icon( + icon: &char, + size: f32, + font: Option, + ) -> Text<'_, Theme, Renderer> + where + Renderer: iced::advanced::text::Renderer, + Renderer::Font: From, + Theme: iced::widget::text::Catalog, + { + Text::::new(icon.to_string()) + .size(size) + .font(font.unwrap_or_default()) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) + .shaping(iced::advanced::text::Shaping::Advanced) + .width(Length::Shrink) + } + + fn layout_text( + text: &str, + size: f32, + font: Option, + ) -> Text<'_, Theme, Renderer> + where + Renderer: iced::advanced::text::Renderer, + Renderer::Font: From, + Theme: iced::widget::text::Catalog, + { + Text::::new(text) + .size(size) + .font(font.unwrap_or_default()) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) + .shaping(text::Shaping::Advanced) + .width(Length::Shrink) + } + + let column = self + .tab_labels + .iter() + .fold( + FlushColumn::::new(), + |column, tab_label| { + let label = match tab_label { + TabLabel::Icon(icon) => Row::new() + .align_y(Alignment::Center) + .push(layout_icon(icon, self.icon_size + 1.0, self.font)), + TabLabel::Text(text) => Row::new() + .padding(5.0) + .align_y(Alignment::Center) + .push(layout_text(text, self.text_size + 1.0, self.text_font)), + TabLabel::IconText(icon, text) => { + let mut row = Row::new().align_y(Alignment::Center); + match self.position { + Position::Start => { + row = row + .push(layout_icon(icon, self.icon_size + 1.0, self.font)) + .push(layout_text( + text, + self.text_size + 1.0, + self.text_font, + )); + } + Position::End => { + row = row + .push(layout_text( + text, + self.text_size + 1.0, + self.text_font, + )) + .push(layout_icon(icon, self.icon_size + 1.0, self.font)); + } + } + row + } + }; + let mut tab = Row::new(); + if self.on_close.is_some() { + let close = Row::new() + .width(Length::Fixed(self.close_size * 1.3 + 1.0)) + .height(Length::Fixed(self.close_size * 1.3 + 1.0)) + .align_y(Alignment::Center); + match self.close_position { + Position::Start => tab = tab.push(close).push(label), + Position::End => tab = tab.push(label).push(close), + } + } else { + tab = tab.push(label); + } + tab = tab + .align_y(Alignment::Center) + .padding(self.padding) + .height(self.tab_height) + .width(self.width); + column.push(tab) + }, + ) + .width(self.width) + .height(self.height) + .spacing(self.spacing) + .align_x(self.align_tabs); + let element: Element = Element::new(column); + let tab_tree = if let Some(child_tree) = tree.children.get_mut(0) { + child_tree.diff(element.as_widget()); + child_tree + } else { + let child_tree = Tree::new(element.as_widget()); + tree.children.insert(0, child_tree); + &mut tree.children[0] + }; + element + .as_widget() + .layout(tab_tree, renderer, &limits.loose()) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if cursor + .position() + .map_or(false, |pos| layout.bounds().contains(pos)) + { + let tabs_map: Vec = layout + .children() + .map(|layout| { + cursor + .position() + .map_or(false, |pos| layout.bounds().contains(pos)) + }) + .collect(); + + if let Some(new_selected) = tabs_map.iter().position(|b| *b) { + shell.publish( + self.on_close + .as_ref() + .filter(|_on_close| { + let tab_layout = layout.children().nth(new_selected).expect("widgets: Layout should have a tab layout at the selected index"); + let cross_layout = tab_layout.children().nth(1).expect("widgets: Layout should have a close layout"); + + cursor.position().map_or(false, |pos| cross_layout.bounds().contains(pos) ) + }) + .map_or_else( + || (self.on_select)(self.tab_indices[new_selected].clone()), + |on_close| (on_close)(self.tab_indices[new_selected].clone()), + ), + ); + return event::Status::Captured; + } + } + event::Status::Ignored + } + _ => event::Status::Ignored, + } + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let children = layout.children(); + let mut mouse_interaction = mouse::Interaction::default(); + for layout in children { + let is_mouse_over = cursor + .position() + .map_or(false, |pos| layout.bounds().contains(pos)); + let new_mouse_interaction = if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }; + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + } + mouse_interaction + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let children = layout.children(); + let is_mouse_over = cursor.position().map_or(false, |pos| bounds.contains(pos)); + let style_sheet = if is_mouse_over { + sidebar::Catalog::style(theme, &self.class, Status::Hovered) + } else { + sidebar::Catalog::style(theme, &self.class, Status::Disabled) + }; + if bounds.intersects(viewport) { + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: (0.0).into(), + width: style_sheet.border_width, + color: style_sheet.border_color.unwrap_or(Color::TRANSPARENT), + }, + shadow: Shadow::default(), + }, + style_sheet + .background + .unwrap_or_else(|| Color::TRANSPARENT.into()), + ); + } + for ((i, tab), layout) in self.tab_labels.iter().enumerate().zip(children) { + draw_tab( + renderer, + tab, + layout, + self.position, + theme, + &self.class, + i == self.get_active_tab_idx(), + cursor, + (self.font.unwrap_or(BOOTSTRAP_FONT), self.icon_size), + (self.text_font.unwrap_or_default(), self.text_size), + self.close_size, + viewport, + self.on_close.is_some(), + &self.close_position, + ); + } + } +} + +/// Draws a tab. +#[allow( + clippy::borrowed_box, + clippy::too_many_lines, + clippy::too_many_arguments +)] +fn draw_tab( + renderer: &mut Renderer, + tab: &TabLabel, + layout: Layout<'_>, + position: Position, + theme: &Theme, + class: &::Class<'_>, + is_selected: bool, + cursor: Cursor, + icon_data: (Font, f32), + text_data: (Font, f32), + close_size: f32, + viewport: &Rectangle, + on_close: bool, + close_position: &Position, +) where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog + text::Catalog, +{ + fn icon_bound_rectangle(item: Option>) -> Rectangle { + item.expect("Graphics: Layout should have an icons layout for an IconText") + .bounds() + } + + fn text_bound_rectangle(item: Option>) -> Rectangle { + item.expect("Graphics: Layout should have an texts layout for an IconText") + .bounds() + } + + fn render_icon_text( + renderer: &mut Renderer, + tab: &TabLabel, + label_layout: Layout, + icon_data: (Font, f32), + text_data: (Font, f32), + style: &Style, + position: Position, + ) where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + { + let mut label_layout_children = label_layout.children(); + match tab { + TabLabel::Icon(icon) => { + let icon_bounds = icon_bound_rectangle(label_layout_children.next()); + renderer.fill_text( + iced::advanced::text::Text { + content: icon.to_string(), + bounds: Size::new(icon_bounds.width, icon_bounds.height), + size: Pixels(icon_data.1), + font: icon_data.0, + horizontal_alignment: Horizontal::Center, + vertical_alignment: Vertical::Center, + line_height: LineHeight::Relative(1.3), + shaping: iced::advanced::text::Shaping::Advanced, + }, + Point::new(icon_bounds.center_x(), icon_bounds.center_y()), + style.icon_color, + icon_bounds, + ); + } + TabLabel::Text(text) => { + let text_bounds = text_bound_rectangle(label_layout_children.next()); + renderer.fill_text( + iced::advanced::text::Text { + content: text.to_string(), + bounds: Size::new(text_bounds.width, text_bounds.height), + size: Pixels(text_data.1), + font: text_data.0, + horizontal_alignment: Horizontal::Center, + vertical_alignment: Vertical::Center, + line_height: LineHeight::Relative(1.3), + shaping: iced::advanced::text::Shaping::Advanced, + }, + Point::new(text_bounds.center_x(), text_bounds.center_y()), + style.text_color, + text_bounds, + ); + } + TabLabel::IconText(icon, text) => { + let icon_bounds: Rectangle; + let text_bounds: Rectangle; + match position { + Position::Start => { + icon_bounds = icon_bound_rectangle(label_layout_children.next()); + text_bounds = text_bound_rectangle(label_layout_children.next()); + } + Position::End => { + text_bounds = text_bound_rectangle(label_layout_children.next()); + icon_bounds = icon_bound_rectangle(label_layout_children.next()); + } + } + renderer.fill_text( + iced::advanced::text::Text { + content: icon.to_string(), + bounds: Size::new(icon_bounds.width, icon_bounds.height), + size: Pixels(icon_data.1), + font: icon_data.0, + horizontal_alignment: Horizontal::Center, + vertical_alignment: Vertical::Center, + line_height: LineHeight::Relative(1.3), + shaping: iced::advanced::text::Shaping::Advanced, + }, + Point::new(icon_bounds.center_x(), icon_bounds.center_y()), + style.icon_color, + icon_bounds, + ); + renderer.fill_text( + iced::advanced::text::Text { + content: text.to_string(), + bounds: Size::new(text_bounds.width, text_bounds.height), + size: Pixels(text_data.1), + font: text_data.0, + horizontal_alignment: Horizontal::Center, + vertical_alignment: Vertical::Center, + line_height: LineHeight::Relative(1.3), + shaping: iced::advanced::text::Shaping::Advanced, + }, + Point::new(text_bounds.center_x(), text_bounds.center_y()), + style.text_color, + text_bounds, + ); + } + }; + } + + fn render_close( + renderer: &mut Renderer, + style: &Style, + cross_layout: Layout, + cursor: Cursor, + close_size: f32, + viewport: &Rectangle, + ) where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + { + let cross_bounds = cross_layout.bounds(); + let is_mouse_over_cross = cursor.is_over(cross_bounds); + renderer.fill_text( + iced::advanced::text::Text { + content: icon_to_string(Bootstrap::X), + bounds: Size::new(cross_bounds.width, cross_bounds.height), + size: Pixels(close_size + if is_mouse_over_cross { 1.0 } else { 0.0 }), + font: BOOTSTRAP_FONT, + horizontal_alignment: Horizontal::Center, + vertical_alignment: Vertical::Center, + line_height: LineHeight::Relative(1.3), + shaping: iced::advanced::text::Shaping::Basic, + }, + Point::new(cross_bounds.center_x(), cross_bounds.center_y()), + style.text_color, + cross_bounds, + ); + if is_mouse_over_cross && cross_bounds.intersects(viewport) { + renderer.fill_quad( + renderer::Quad { + bounds: cross_bounds, + border: Border { + radius: style.icon_border_radius, + width: style.border_width, + color: style.border_color.unwrap_or(Color::TRANSPARENT), + }, + shadow: Shadow::default(), + }, + style + .icon_background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + } + + let bounds = layout.bounds(); + let is_mouse_over = cursor.position().map_or(false, |pos| bounds.contains(pos)); + let style = if is_mouse_over { + sidebar::Catalog::style(theme, class, Status::Hovered) + } else if is_selected { + sidebar::Catalog::style(theme, class, Status::Active) + } else { + sidebar::Catalog::style(theme, class, Status::Disabled) + }; + if bounds.intersects(viewport) { + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: (0.0).into(), + width: style.tab_label_border_width, + color: style.tab_label_border_color, + }, + shadow: Shadow::default(), + }, + style.tab_label_background, + ); + } + let mut children = layout.children(); + if on_close { + match close_position { + Position::Start => { + let cross_layout = children + .next() + .expect("Graphics: Expected close icon layout."); + render_close( + renderer, + &style, + cross_layout, + cursor, + close_size, + viewport, + ); + let label_layout = children + .next() + .expect("Graphics: Layout should have a label layout"); + render_icon_text( + renderer, + tab, + label_layout, + icon_data, + text_data, + &style, + position, + ); + } + Position::End => { + let label_layout = children + .next() + .expect("Graphics: Layout should have a label layout"); + render_icon_text( + renderer, + tab, + label_layout, + icon_data, + text_data, + &style, + position, + ); + let cross_layout = children + .next() + .expect("Graphics: Expected close icon layout."); + render_close( + renderer, + &style, + cross_layout, + cursor, + close_size, + viewport, + ); + } + } + } else { + let label_layout = children + .next() + .expect("Graphics: Layout should have a label layout"); + render_icon_text( + renderer, + tab, + label_layout, + icon_data, + text_data, + &style, + position, + ); + } +} + +impl<'a, Message, TabId, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer, + Theme: 'a + Catalog + text::Catalog, + Message: 'a, + TabId: 'a + Eq + Clone, +{ + fn from(sidebar: Sidebar<'a, Message, TabId, Theme, Renderer>) -> Self { + Element::new(sidebar) + } +} + +// +// +// ------ Sidebar with content. +// +// + +/// A [`SidebarPosition`] for defining the position of a [`Sidebar`] to the content. +#[derive(Clone, Hash)] +#[allow(missing_debug_implementations)] +pub enum SidebarPosition { + /// A [`SidebarPosition`] for placing the [`Sidebar`] to the start of its content. + Start, + + /// A [`SidebarPosition`] for placing the [`Sidebar`] to the end of its content. + End, +} + +/// A [`SidebarWithContent`] widget for showing a [`Sidebar`] +/// along with the tab's content. +/// +/// # Example +/// ```ignore +/// # use iced_aw::{TabLabel, tabs::SidebarWithContent}; +/// # use iced::widget::Text; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// TabSelected(TabId), +/// } +/// +/// #[derive(Debug, Clone)] +/// enum TabId { +/// One, +/// Two, +/// Three, +/// } +/// +/// let tabs = SidebarWithContent::new(Message::TabSelected) +/// .push(TabId::One, TabLabel::Text(String::from("One")), Text::new(String::from("One"))) +/// .push(TabId::Two, TabLabel::Text(String::from("Two")), Text::new(String::from("Two"))) +/// .push(TabId::Three, TabLabel::Text(String::from("Three")), Text::new(String::from("Three"))) +/// .set_active_tab(&TabId::Two); +/// ``` +/// +#[allow(missing_debug_implementations)] +pub struct SidebarWithContent<'a, Message, TabId, Theme = iced::Theme, Renderer = iced::Renderer> +where + Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog, + TabId: Eq + Clone, +{ + /// The [`Sidebar`] of the [`SidebarWithContent`]. + sidebar: Sidebar<'a, Message, TabId, Theme, Renderer>, + /// The vector containing the content of the tabs. + tabs: Vec>, + /// The vector containing the indices of the tabs. + indices: Vec, + /// The position of the [`Sidebar`]. + sidebar_position: SidebarPosition, + /// the width of the [`SidebarWithContent`]. + width: Length, + /// The height of the [`SidebarWithContent`]. + height: Length, +} + +impl<'a, Message, TabId, Theme, Renderer> SidebarWithContent<'a, Message, TabId, Theme, Renderer> +where + Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog + text::Catalog, + TabId: Eq + Clone, +{ + /// Creates a new [`SidebarWithContent`] widget with the index of the selected tab and a + /// specified message which will be send when a tab is selected by the user. + /// + /// It expects: + /// * the index of the currently active tab. + /// * the function that will be called if a tab is selected by the user. + /// It takes the index of the selected tab. + pub fn new(on_select: F) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + Self::new_with_tabs(Vec::new(), on_select) + } + + /// Similar to `new` but with a given Vector of the + /// [`TabLabel`](super::sidebar::TabLabel) along with the tab's content. + /// + /// It expects: + /// * the index of the currently active tab. + /// * a vector containing the [`TabLabel`]s along with the content + /// [`Element`]s of the [`SidebarWithContent`]. + /// * the function that will be called if a tab is selected by the user. + /// It takes the index of the selected tab. + pub fn new_with_tabs( + tabs: Vec<(TabId, TabLabel, Element<'a, Message, Theme, Renderer>)>, + on_select: F, + ) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + let mut tab_labels = Vec::with_capacity(tabs.len()); + let mut elements = Vec::with_capacity(tabs.len()); + let mut indices = Vec::with_capacity(tabs.len()); + for (id, tab_label, element) in tabs { + tab_labels.push((id.clone(), tab_label)); + indices.push(id); + elements.push(element); + } + SidebarWithContent { + sidebar: Sidebar::with_tab_labels(tab_labels, on_select), + tabs: elements, + indices, + sidebar_position: SidebarPosition::Start, + width: Length::Fill, + height: Length::Shrink, + } + } + + /// Sets the size of the close icon of the + /// [`TabLabel`](super::sidebar::TabLabel) of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn close_size(mut self, close_size: f32) -> Self { + self.sidebar = self.sidebar.close_size(close_size); + self + } + + /// Sets the alignment of the tabs for the [`Sidebar`]. + #[must_use] + pub fn align_tabs(mut self, align: Alignment) -> Self { + self.sidebar = self.sidebar.align_tabs(align); + self + } + + /// Sets the Icon render Position for the + /// [`TabLabel`](super::sidebar::TabLabel) of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn tab_icon_position(mut self, position: Position) -> Self { + self.sidebar = self.sidebar.set_position(position); + self + } + + /// Sets the close icon render Position for the tab of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn close_icon_position(mut self, position: Position) -> Self { + self.sidebar = self.sidebar.set_close_position(position); + self + } + + /// Sets the height of the [`SidebarWithContent`]. + #[must_use] + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the font of the icons of the + /// [`TabLabel`](super::sidebar::TabLabel)s of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn icon_font(mut self, font: Font) -> Self { + self.sidebar = self.sidebar.icon_font(font); + self + } + + /// Sets the icon size of the [`TabLabel`](super::sidebar::TabLabel) of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn icon_size(mut self, icon_size: f32) -> Self { + self.sidebar = self.sidebar.icon_size(icon_size); + self + } + + /// Sets the message that will be produced when the close icon of a tab + /// on the [`Sidebar`] is pressed. + /// + /// Setting this enables the drawing of a close icon on the tabs. + #[must_use] + pub fn on_close(mut self, on_close: F) -> Self + where + F: 'static + Fn(TabId) -> Message, + { + self.sidebar = self.sidebar.on_close(on_close); + self + } + + /// Pushes a [`TabLabel`](super::sidebar::TabLabel) along with the tabs + /// content to the [`SidebarWithContent`]. + #[must_use] + pub fn push(mut self, id: TabId, tab_label: TabLabel, element: E) -> Self + where + E: Into>, + { + self.sidebar = self.sidebar.push(id.clone(), tab_label); + self.tabs.push(element.into()); + self.indices.push(id); + self + } + + /// Sets the active tab of the [`SidebarWithContent`] using the ``TabId``. + #[must_use] + pub fn set_active_tab(mut self, id: &TabId) -> Self { + self.sidebar = self.sidebar.set_active_tab(id); + self + } + + /// Sets the height of the [`Sidebar`](super::sidebar::Sidebar) of the [`SidebarWithContent`]. + #[must_use] + pub fn sidebar_height(mut self, height: Length) -> Self { + self.sidebar = self.sidebar.height(height); + self + } + + /// Sets the width of the [`Sidebar`](super::sidebar::Sidebar) of the [`SidebarWithContent`]. + #[must_use] + pub fn sidebar_width(mut self, width: Length) -> Self { + self.sidebar = self.sidebar.width(width); + self + } + + /// Sets the [`SidebarPosition`] of the [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn sidebar_position(mut self, position: SidebarPosition) -> Self { + self.sidebar_position = position; + self + } + + /// Sets the style of the [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn sidebar_style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + ::Class<'a>: From>, + { + self.sidebar = self.sidebar.style(style); + self + } + + /// Sets the padding of the tabs of the [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn tab_label_padding(mut self, padding: f32) -> Self { + self.sidebar = self.sidebar.padding(padding); + self + } + + /// Sets the spacing between the tabs of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn tab_label_spacing(mut self, spacing: f32) -> Self { + self.sidebar = self.sidebar.spacing(spacing); + self + } + + /// Sets the font of the text of the + /// [`TabLabel`](super::sidebar::TabLabel)s of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn text_font(mut self, text_font: Font) -> Self { + self.sidebar = self.sidebar.text_font(text_font); + self + } + + /// Sets the text size of the [`TabLabel`](super::sidebar::TabLabel) of the + /// [`Sidebar`](super::sidebar::Sidebar). + #[must_use] + pub fn text_size(mut self, text_size: f32) -> Self { + self.sidebar = self.sidebar.text_size(text_size); + self + } + + /// Sets the width of the [`SidebarWithContent`]. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } +} + +impl<'a, Message, TabId, Theme, Renderer> Widget + for SidebarWithContent<'a, Message, TabId, Theme, Renderer> +where + Renderer: renderer::Renderer + iced::advanced::text::Renderer, + Theme: Catalog + text::Catalog, + TabId: Eq + Clone, +{ + fn children(&self) -> Vec { + let tabs = Tree { + tag: Tag::stateless(), + state: State::None, + children: self.tabs.iter().map(Tree::new).collect(), + }; + let bar = Tree { + tag: self.sidebar.tag(), + state: self.sidebar.state(), + children: self.sidebar.children(), + }; + vec![bar, tabs] + } + + fn diff(&self, tree: &mut Tree) { + if tree.children.is_empty() { + tree.children = self.children(); + } + + if let Some(tabs) = tree.children.get_mut(1) { + tabs.diff_children(&self.tabs); + } + } + + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + let sidebar_limits = limits.width(Length::Shrink).height(self.height); + let mut sidebar_node = + self.sidebar + .layout(&mut tree.children[0], renderer, &sidebar_limits); + let tab_content_limits = limits + .width(self.width) + .height(self.height) + .shrink([sidebar_node.size().width, 0.0]); + let mut tab_content_node = + if let Some(element) = self.tabs.get(self.sidebar.get_active_tab_idx()) { + element.as_widget().layout( + &mut tree.children[1].children[self.sidebar.get_active_tab_idx()], + renderer, + &tab_content_limits, + ) + } else { + Row::::new() + .width(Length::Fill) + .height(Length::Shrink) + .layout(tree, renderer, &tab_content_limits) + }; + let sidebar_bounds = sidebar_node.bounds(); + sidebar_node = sidebar_node.move_to(Point::new( + sidebar_bounds.x + + match self.sidebar_position { + SidebarPosition::Start => 0.0, + SidebarPosition::End => tab_content_node.bounds().width, + }, + sidebar_bounds.y, + )); + let tab_content_bounds = tab_content_node.bounds(); + tab_content_node = tab_content_node.move_to(Point::new( + tab_content_bounds.x + + match self.sidebar_position { + SidebarPosition::Start => sidebar_node.bounds().width, + SidebarPosition::End => 0.0, + }, + tab_content_bounds.y, + )); + Node::with_children( + Size::new( + sidebar_node.size().width + tab_content_node.size().width, + tab_content_node.size().height, + ), + match self.sidebar_position { + SidebarPosition::Start => vec![sidebar_node, tab_content_node], + SidebarPosition::End => vec![tab_content_node, sidebar_node], + }, + ) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let mut children = layout.children(); + let (sidebar_layout, tab_content_layout) = match self.sidebar_position { + SidebarPosition::Start => { + let sidebar_layout = children + .next() + .expect("Native: Layout should have a Sidebar layout at line start position"); + let tab_content_layout = children.next().expect( + "Native: Layout should have a tab content layout at line start position", + ); + (sidebar_layout, tab_content_layout) + } + SidebarPosition::End => { + let tab_content_layout = children + .next() + .expect("Native: Layout should have a tab content layout at line end position"); + let sidebar_layout = children + .next() + .expect("Native: Layout should have a Sidebar layout at line end position"); + (sidebar_layout, tab_content_layout) + } + }; + let status_sidebar = self.sidebar.on_event( + &mut Tree::empty(), + event.clone(), + sidebar_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + let idx = self.sidebar.get_active_tab_idx(); + let status_element = self + .tabs + .get_mut(idx) + .map_or(event::Status::Ignored, |element| { + element.as_widget_mut().on_event( + &mut state.children[1].children[idx], + event, + tab_content_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }); + status_sidebar.merge(status_element) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + // Sidebar + let mut children = layout.children(); + let sidebar_layout = match self.sidebar_position { + SidebarPosition::Start => children + .next() + .expect("Native: There should be a Sidebar at the line start position"), + SidebarPosition::End => children + .last() + .expect("Native: There should be a Sidebar at the line end position"), + }; + let mut mouse_interaction = mouse::Interaction::default(); + let new_mouse_interaction = self.sidebar.mouse_interaction( + &Tree::empty(), + sidebar_layout, + cursor, + viewport, + renderer, + ); + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + // Tab content + let mut children = layout.children(); + let tab_content_layout = match self.sidebar_position { + SidebarPosition::Start => children + .last() + .expect("Graphics: There should be a Sidebar at the line start position"), + SidebarPosition::End => children + .next() + .expect("Graphics: There should be a Sidebar at the line end position"), + }; + let idx = self.sidebar.get_active_tab_idx(); + if let Some(element) = self.tabs.get(idx) { + let new_mouse_interaction = element.as_widget().mouse_interaction( + &state.children[1].children[idx], + tab_content_layout, + cursor, + viewport, + renderer, + ); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + } + mouse_interaction + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let mut children = layout.children(); + let sidebar_layout = match self.sidebar_position { + SidebarPosition::Start => children + .next() + .expect("Native: There should be a Sidebar at the line start position"), + SidebarPosition::End => children + .last() + .expect("Native: There should be a Sidebar at the line end position"), + }; + self.sidebar.draw( + &Tree::empty(), + renderer, + theme, + style, + sidebar_layout, + cursor, + viewport, + ); + let mut children = layout.children(); + let tab_content_layout = match self.sidebar_position { + SidebarPosition::Start => children + .last() + .expect("Graphics: There should be a Sidebar at the line start position"), + SidebarPosition::End => children + .next() + .expect("Graphics: There should be a Sidebar at the line end position"), + }; + let idx = self.sidebar.get_active_tab_idx(); + if let Some(element) = self.tabs.get(idx) { + element.as_widget().draw( + &state.children[1].children[idx], + renderer, + theme, + style, + tab_content_layout, + cursor, + viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + let layout = match self.sidebar_position { + SidebarPosition::Start => layout.children().nth(1), + SidebarPosition::End => layout.children().next(), + }; + layout.and_then(|layout| { + let idx = self.sidebar.get_active_tab_idx(); + self.tabs + .get_mut(idx) + .map(Element::as_widget_mut) + .and_then(|w| { + w.overlay( + &mut state.children[1].children[idx], + layout, + renderer, + translation, + ) + }) + }) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { + let active_tab = self.sidebar.get_active_tab_idx(); + operation.container(None, layout.bounds(), &mut |operation| { + self.tabs[active_tab].as_widget().operate( + &mut tree.children[1].children[active_tab], + layout + .children() + .nth(1) + .expect("Sidebar is 0th child, contents are 1st node"), + renderer, + operation, + ); + }); + } +} + +impl<'a, Message, TabId, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer, + Theme: 'a + Catalog + text::Catalog, + Message: 'a, + TabId: 'a + Eq + Clone, +{ + fn from(content: SidebarWithContent<'a, Message, TabId, Theme, Renderer>) -> Self { + Element::new(content) + } +} diff --git a/src/widgets/tab_bar.rs b/src/widgets/tab_bar.rs index 4881d673..b71d295b 100644 --- a/src/widgets/tab_bar.rs +++ b/src/widgets/tab_bar.rs @@ -377,7 +377,7 @@ where .size(size) .font(font.unwrap_or_default()) .align_x(alignment::Horizontal::Center) - // .align_y(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) .shaping(iced::advanced::text::Shaping::Advanced) .width(Length::Shrink) } From 0872e232dbe56373458462f03eaa71f02a66dd35 Mon Sep 17 00:00:00 2001 From: Rizzen Yazston Date: Thu, 18 Jul 2024 21:02:25 +0200 Subject: [PATCH 2/2] Ran 'cargo fmt' and 'cargo clippy'. --- examples/menu/src/main.rs | 5 +---- examples/sidebar/src/main.rs | 12 ++++++++---- src/widgets/sidebar/sidebar.rs | 18 ++---------------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index ca49f15d..9032256f 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -477,10 +477,7 @@ fn labeled_button( label: &str, msg: Message, ) -> button::Button { - base_button( - text(label).align_y(alignment::Vertical::Center), - msg, - ) + base_button(text(label).align_y(alignment::Vertical::Center), msg) } fn debug_button(label: &str) -> button::Button { diff --git a/examples/sidebar/src/main.rs b/examples/sidebar/src/main.rs index 3e0a2771..f71b6dad 100644 --- a/examples/sidebar/src/main.rs +++ b/examples/sidebar/src/main.rs @@ -40,10 +40,14 @@ impl From for char { } fn main() -> iced::Result { - iced::application("Sidebar example", TabBarExample::update, TabBarExample::view) - .font(iced_aw::BOOTSTRAP_FONT_BYTES) - .font(ICON_BYTES) - .run() + iced::application( + "Sidebar example", + TabBarExample::update, + TabBarExample::view, + ) + .font(iced_aw::BOOTSTRAP_FONT_BYTES) + .font(ICON_BYTES) + .run() } #[derive(Default)] diff --git a/src/widgets/sidebar/sidebar.rs b/src/widgets/sidebar/sidebar.rs index e4b9a626..7a298cea 100644 --- a/src/widgets/sidebar/sidebar.rs +++ b/src/widgets/sidebar/sidebar.rs @@ -852,14 +852,7 @@ fn draw_tab( let cross_layout = children .next() .expect("Graphics: Expected close icon layout."); - render_close( - renderer, - &style, - cross_layout, - cursor, - close_size, - viewport, - ); + render_close(renderer, &style, cross_layout, cursor, close_size, viewport); let label_layout = children .next() .expect("Graphics: Layout should have a label layout"); @@ -889,14 +882,7 @@ fn draw_tab( let cross_layout = children .next() .expect("Graphics: Expected close icon layout."); - render_close( - renderer, - &style, - cross_layout, - cursor, - close_size, - viewport, - ); + render_close(renderer, &style, cross_layout, cursor, close_size, viewport); } } } else {