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/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/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 00000000..abfc4ebe Binary files /dev/null and b/examples/sidebar/fonts/icons.ttf differ diff --git a/examples/sidebar/images/ferris.png b/examples/sidebar/images/ferris.png new file mode 100644 index 00000000..ebce1a14 Binary files /dev/null and b/examples/sidebar/images/ferris.png differ 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..f71b6dad --- /dev/null +++ b/examples/sidebar/src/main.rs @@ -0,0 +1,163 @@ +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..7a298cea --- /dev/null +++ b/src/widgets/sidebar/sidebar.rs @@ -0,0 +1,1514 @@ +//! 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) }