CSS improvements, light and dark mode
This commit is contained in:
parent
1324cae7e3
commit
afda2d9d4f
|
|
@ -114,6 +114,17 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
|
|
@ -172,6 +183,56 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
|
|
@ -502,6 +563,15 @@ version = "0.22.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64-simd"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
|
||||
dependencies = [
|
||||
"simd-abstraction",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.6.0"
|
||||
|
|
@ -544,6 +614,18 @@ version = "2.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
|
|
@ -593,6 +675,28 @@ version = "3.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
|
||||
dependencies = [
|
||||
"bytecheck_derive",
|
||||
"ptr_meta",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck_derive"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
|
|
@ -756,6 +860,46 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
|
|
@ -787,6 +931,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "com"
|
||||
version = "0.6.0"
|
||||
|
|
@ -837,6 +987,35 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
|
||||
dependencies = [
|
||||
"const-str-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str-proc-macro"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
|
@ -955,6 +1134,38 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
|
||||
dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser-color"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser-macros"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursor-icon"
|
||||
version = "1.2.0"
|
||||
|
|
@ -972,6 +1183,34 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
|
||||
dependencies = [
|
||||
"matches",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
|
|
@ -1045,6 +1284,21 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa-short"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.29.1"
|
||||
|
|
@ -1061,7 +1315,7 @@ version = "0.29.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
"egui",
|
||||
|
|
@ -1099,7 +1353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"emath",
|
||||
"epaint",
|
||||
"log",
|
||||
|
|
@ -1112,7 +1366,7 @@ version = "0.29.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
"egui",
|
||||
|
|
@ -1132,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6"
|
||||
dependencies = [
|
||||
"accesskit_winit",
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"arboard",
|
||||
"egui",
|
||||
"log",
|
||||
|
|
@ -1149,7 +1403,7 @@ version = "0.29.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"egui",
|
||||
"enum-map",
|
||||
"image",
|
||||
|
|
@ -1164,7 +1418,7 @@ version = "0.29.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"bytemuck",
|
||||
"egui",
|
||||
"glow 0.14.2",
|
||||
|
|
@ -1245,7 +1499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"bytemuck",
|
||||
"ecolor",
|
||||
"emath",
|
||||
|
|
@ -1489,6 +1743,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
|
|
@ -2012,6 +2272,21 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
|
|
@ -2237,6 +2512,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2250,6 +2527,21 @@ dependencies = [
|
|||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
|
|
@ -2373,6 +2665,12 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
|
|
@ -2450,12 +2748,14 @@ dependencies = [
|
|||
name = "lightningbeam-editor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"eframe",
|
||||
"egui-wgpu",
|
||||
"egui_extras",
|
||||
"image",
|
||||
"kurbo 0.11.3",
|
||||
"lightningbeam-core",
|
||||
"lightningcss",
|
||||
"muda",
|
||||
"peniko 0.5.0",
|
||||
"pollster",
|
||||
|
|
@ -2467,6 +2767,46 @@ dependencies = [
|
|||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightningcss"
|
||||
version = "1.0.0-alpha.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b407ca668368d1d5a86cea58ac82d9f9f9ca4bac1e9dce6f16f875f0f081a911"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"bitflags 2.10.0",
|
||||
"const-str",
|
||||
"cssparser",
|
||||
"cssparser-color",
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"getrandom 0.3.4",
|
||||
"indexmap",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"lightningcss-derive",
|
||||
"parcel_selectors",
|
||||
"parcel_sourcemap",
|
||||
"pastey",
|
||||
"pathdiff",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde-content",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightningcss-derive"
|
||||
version = "1.0.0-alpha.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linebender_resource_handle"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2530,6 +2870,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
|
|
@ -3095,6 +3441,12 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.49"
|
||||
|
|
@ -3114,6 +3466,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
|
|
@ -3148,6 +3506,36 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parcel_selectors"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cssparser",
|
||||
"log",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"precomputed-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parcel_sourcemap"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"data-url 0.1.1",
|
||||
"rkyv",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"vlq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -3183,6 +3571,18 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "peniko"
|
||||
version = "0.2.0"
|
||||
|
|
@ -3221,6 +3621,16 @@ dependencies = [
|
|||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
|
|
@ -3374,6 +3784,12 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
|
|
@ -3461,6 +3877,26 @@ dependencies = [
|
|||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
|
||||
dependencies = [
|
||||
"ptr_meta_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta_derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.25"
|
||||
|
|
@ -3519,6 +3955,12 @@ version = "5.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
|
@ -3569,7 +4011,7 @@ dependencies = [
|
|||
"built",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools",
|
||||
"itertools 0.12.1",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
|
|
@ -3665,6 +4107,15 @@ dependencies = [
|
|||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
|
|
@ -3710,6 +4161,35 @@ dependencies = [
|
|||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"bytecheck",
|
||||
"bytes",
|
||||
"hashbrown 0.12.3",
|
||||
"ptr_meta",
|
||||
"rend",
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv_derive"
|
||||
version = "0.7.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.19.0"
|
||||
|
|
@ -3831,6 +4311,12 @@ dependencies = [
|
|||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
|
|
@ -3847,6 +4333,15 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-content"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
|
|
@ -3926,6 +4421,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-abstraction"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
|
||||
dependencies = [
|
||||
"outref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
|
|
@ -3941,6 +4445,12 @@ dependencies = [
|
|||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.2"
|
||||
|
|
@ -4098,6 +4608,12 @@ dependencies = [
|
|||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "svg_fmt"
|
||||
version = "0.4.5"
|
||||
|
|
@ -4170,6 +4686,12 @@ dependencies = [
|
|||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
|
|
@ -4547,7 +5069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"data-url",
|
||||
"data-url 0.3.2",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
"imagesize",
|
||||
|
|
@ -4573,7 +5095,7 @@ version = "0.37.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"data-url 0.3.2",
|
||||
"flate2",
|
||||
"imagesize",
|
||||
"kurbo 0.9.5",
|
||||
|
|
@ -4603,6 +5125,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
|
|
@ -4683,6 +5211,12 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vlq"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
|
@ -5412,7 +5946,7 @@ version = "0.30.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.10.0",
|
||||
|
|
@ -5488,6 +6022,15 @@ version = "0.6.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11"
|
||||
version = "2.21.0"
|
||||
|
|
|
|||
|
|
@ -33,3 +33,5 @@ resvg = { workspace = true }
|
|||
|
||||
# Utilities
|
||||
pollster = { workspace = true }
|
||||
lightningcss = "1.0.0-alpha.68"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
/* Lightningbeam Editor Styles
|
||||
* CSS with variables and selector-based theming
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
LIGHT MODE VARIABLES
|
||||
============================================ */
|
||||
:root {
|
||||
/* Base colors */
|
||||
--bg-primary: #f6f6f6;
|
||||
--bg-secondary: #ccc;
|
||||
--bg-panel: #aaa;
|
||||
--bg-header: #ddd;
|
||||
--background-color: #ccc;
|
||||
--foreground-color: #ddd;
|
||||
--highlight: #ddd;
|
||||
--shadow: #999;
|
||||
--shade: #aaa;
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #0f0f0f;
|
||||
--text-secondary: #666;
|
||||
--text-tertiary: #999;
|
||||
|
||||
/* Border colors */
|
||||
--border-light: #bbb;
|
||||
--border-medium: #999;
|
||||
--border-dark: #555;
|
||||
|
||||
/* UI backgrounds */
|
||||
--grid-bg: #555;
|
||||
--grid-hover: #666;
|
||||
|
||||
/* Dimensions */
|
||||
--header-height: 40px;
|
||||
--pane-border-width: 1px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
DARK MODE VARIABLES
|
||||
============================================ */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Base colors */
|
||||
--bg-primary: #2f2f2f;
|
||||
--bg-secondary: #3f3f3f;
|
||||
--bg-panel: #222222;
|
||||
--bg-header: #444;
|
||||
--background-color: #333;
|
||||
--foreground-color: #888;
|
||||
--highlight: #4f4f4f;
|
||||
--shadow: #111;
|
||||
--shade: #222;
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #f6f6f6;
|
||||
--text-secondary: #aaa;
|
||||
--text-tertiary: #777;
|
||||
|
||||
/* Border colors */
|
||||
--border-light: #555;
|
||||
--border-medium: #444;
|
||||
--border-dark: #333;
|
||||
|
||||
/* UI backgrounds */
|
||||
--grid-bg: #0f0f0f;
|
||||
--grid-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
COMPONENT STYLES (applies to both modes)
|
||||
============================================ */
|
||||
|
||||
/* Pane headers */
|
||||
.pane-header {
|
||||
background-color: var(--bg-header);
|
||||
color: var(--text-primary);
|
||||
height: var(--header-height);
|
||||
border-color: var(--border-medium);
|
||||
}
|
||||
|
||||
/* Pane content areas */
|
||||
.pane-content {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
/* General panel */
|
||||
.panel {
|
||||
background-color: var(--bg-panel);
|
||||
border-color: var(--border-medium);
|
||||
}
|
||||
|
||||
/* Grid backgrounds */
|
||||
.grid {
|
||||
background-color: var(--grid-bg);
|
||||
}
|
||||
|
||||
.grid-hover {
|
||||
background-color: var(--grid-hover);
|
||||
}
|
||||
|
||||
/* Specific pane IDs */
|
||||
#stage {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
#timeline {
|
||||
background-color: var(--shade);
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
#infopanel {
|
||||
background-color: var(--bg-panel);
|
||||
}
|
||||
|
||||
#node-editor {
|
||||
background-color: #2d2d2d;
|
||||
border-color: #4d4d4d;
|
||||
}
|
||||
|
||||
/* Timeline specific elements */
|
||||
.timeline-background {
|
||||
background-color: var(--shade);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
background-color: var(--shadow);
|
||||
}
|
||||
|
||||
.timeline-spacer {
|
||||
background-color: var(--shadow);
|
||||
}
|
||||
|
||||
.timeline-scrubber {
|
||||
background-color: #cc2222;
|
||||
border-color: #cc2222;
|
||||
}
|
||||
|
||||
.timeline-layer-active {
|
||||
background-color: var(--highlight);
|
||||
}
|
||||
|
||||
.timeline-layer-inactive {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.timeline-row-active {
|
||||
background-color: var(--grid-bg);
|
||||
}
|
||||
|
||||
.timeline-row-inactive {
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-medium);
|
||||
}
|
||||
|
||||
.button-hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
|
||||
/* Text */
|
||||
.text-primary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use lightningbeam_core::layout::{LayoutDefinition, LayoutNode};
|
|||
use lightningbeam_core::pane::PaneType;
|
||||
use lightningbeam_core::tool::Tool;
|
||||
use std::collections::HashMap;
|
||||
use clap::Parser;
|
||||
|
||||
mod panes;
|
||||
use panes::{PaneInstance, PaneRenderer, SharedPaneState};
|
||||
|
|
@ -10,9 +11,46 @@ use panes::{PaneInstance, PaneRenderer, SharedPaneState};
|
|||
mod menu;
|
||||
use menu::{MenuAction, MenuSystem};
|
||||
|
||||
mod theme;
|
||||
use theme::{Theme, ThemeMode};
|
||||
|
||||
/// Lightningbeam Editor - Animation and video editing software
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "Lightningbeam Editor")]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Use light theme
|
||||
#[arg(long, conflicts_with = "dark")]
|
||||
light: bool,
|
||||
|
||||
/// Use dark theme
|
||||
#[arg(long, conflicts_with = "light")]
|
||||
dark: bool,
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
println!("🚀 Starting Lightningbeam Editor...");
|
||||
|
||||
// Parse command line arguments
|
||||
let args = Args::parse();
|
||||
|
||||
// Determine theme mode from arguments
|
||||
let theme_mode = if args.light {
|
||||
ThemeMode::Light
|
||||
} else if args.dark {
|
||||
ThemeMode::Dark
|
||||
} else {
|
||||
ThemeMode::System
|
||||
};
|
||||
|
||||
// Load theme
|
||||
let mut theme = Theme::load_default().expect("Failed to load theme");
|
||||
theme.set_mode(theme_mode);
|
||||
println!("✅ Loaded theme with {} selectors (mode: {:?})", theme.len(), theme_mode);
|
||||
|
||||
// Debug: print theme info
|
||||
theme.debug_print();
|
||||
|
||||
// Load layouts from JSON
|
||||
let layouts = load_layouts();
|
||||
println!("✅ Loaded {} layouts", layouts.len());
|
||||
|
|
@ -39,7 +77,7 @@ fn main() -> eframe::Result {
|
|||
eframe::run_native(
|
||||
"Lightningbeam Editor",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(EditorApp::new(cc, layouts)))),
|
||||
Box::new(move |cc| Ok(Box::new(EditorApp::new(cc, layouts, theme)))),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -191,15 +229,38 @@ struct EditorApp {
|
|||
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
||||
menu_system: Option<MenuSystem>, // Native menu system for event checking
|
||||
pending_view_action: Option<MenuAction>, // Pending view action (zoom, recenter) to be handled by hovered pane
|
||||
theme: Theme, // Theme system for colors and dimensions
|
||||
document: lightningbeam_core::document::Document, // Active document being edited
|
||||
}
|
||||
|
||||
impl EditorApp {
|
||||
fn new(cc: &eframe::CreationContext, layouts: Vec<LayoutDefinition>) -> Self {
|
||||
fn new(cc: &eframe::CreationContext, layouts: Vec<LayoutDefinition>, theme: Theme) -> Self {
|
||||
let current_layout = layouts[0].layout.clone();
|
||||
|
||||
// Initialize native menu system
|
||||
let menu_system = MenuSystem::new().ok();
|
||||
|
||||
// Create default document with a simple test scene
|
||||
let mut document = lightningbeam_core::document::Document::with_size("Untitled Animation", 1920.0, 1080.0)
|
||||
.with_duration(10.0)
|
||||
.with_framerate(60.0);
|
||||
|
||||
// Add a test layer with a simple shape to visualize
|
||||
use lightningbeam_core::layer::{AnyLayer, VectorLayer};
|
||||
use lightningbeam_core::object::Object;
|
||||
use lightningbeam_core::shape::{Shape, ShapeColor};
|
||||
use vello::kurbo::{Circle, Shape as KurboShape};
|
||||
|
||||
let circle = Circle::new((200.0, 150.0), 50.0);
|
||||
let path = circle.to_path(0.1);
|
||||
let shape = Shape::new(path).with_fill(ShapeColor::rgb(100, 150, 250));
|
||||
let object = Object::new(shape.id);
|
||||
|
||||
let mut vector_layer = VectorLayer::new("Layer 1");
|
||||
vector_layer.add_shape(shape);
|
||||
vector_layer.add_object(object);
|
||||
document.root.add_child(AnyLayer::Vector(vector_layer));
|
||||
|
||||
Self {
|
||||
layouts,
|
||||
current_layout_index: 0,
|
||||
|
|
@ -216,6 +277,8 @@ impl EditorApp {
|
|||
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
||||
menu_system,
|
||||
pending_view_action: None,
|
||||
theme,
|
||||
document,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -527,6 +590,8 @@ impl eframe::App for EditorApp {
|
|||
&mut self.pending_view_action,
|
||||
&mut fallback_pane_priority,
|
||||
&mut pending_handlers,
|
||||
&self.theme,
|
||||
&mut self.document,
|
||||
);
|
||||
|
||||
// Execute action on the best handler (two-phase dispatch)
|
||||
|
|
@ -603,10 +668,12 @@ fn render_layout_node(
|
|||
pending_view_action: &mut Option<MenuAction>,
|
||||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
theme: &Theme,
|
||||
document: &mut lightningbeam_core::document::Document,
|
||||
) {
|
||||
match node {
|
||||
LayoutNode::Pane { name } => {
|
||||
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers);
|
||||
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers, theme, document);
|
||||
}
|
||||
LayoutNode::HorizontalGrid { percent, children } => {
|
||||
// Handle dragging
|
||||
|
|
@ -649,6 +716,8 @@ fn render_layout_node(
|
|||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
);
|
||||
|
||||
let mut right_path = path.clone();
|
||||
|
|
@ -672,6 +741,8 @@ fn render_layout_node(
|
|||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -787,6 +858,8 @@ fn render_layout_node(
|
|||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
);
|
||||
|
||||
let mut bottom_path = path.clone();
|
||||
|
|
@ -810,6 +883,8 @@ fn render_layout_node(
|
|||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -905,6 +980,8 @@ fn render_pane(
|
|||
pending_view_action: &mut Option<MenuAction>,
|
||||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
theme: &Theme,
|
||||
document: &mut lightningbeam_core::document::Document,
|
||||
) {
|
||||
let pane_type = PaneType::from_name(pane_name);
|
||||
|
||||
|
|
@ -1048,14 +1125,93 @@ fn render_pane(
|
|||
egui::Color32::from_gray(220),
|
||||
);
|
||||
|
||||
// TODO: Add pane-specific header controls here
|
||||
// For example, Timeline pane would add playback controls
|
||||
// Create header controls area (positioned after title)
|
||||
let title_width = 150.0; // Approximate width for title
|
||||
let header_controls_rect = egui::Rect::from_min_size(
|
||||
header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0 + title_width, 0.0),
|
||||
egui::vec2(header_rect.width() - (icon_padding * 2.0 + icon_size + 8.0 + title_width), header_height),
|
||||
);
|
||||
|
||||
// Make pane content clickable
|
||||
// Render pane-specific header controls (if pane has them)
|
||||
if let Some(pane_type) = pane_type {
|
||||
// Get or create pane instance for header rendering
|
||||
let needs_new_instance = pane_instances
|
||||
.get(path)
|
||||
.map(|instance| instance.pane_type() != pane_type)
|
||||
.unwrap_or(true);
|
||||
|
||||
if needs_new_instance {
|
||||
pane_instances.insert(path.clone(), panes::PaneInstance::new(pane_type));
|
||||
}
|
||||
|
||||
if let Some(pane_instance) = pane_instances.get_mut(path) {
|
||||
let mut header_ui = ui.new_child(egui::UiBuilder::new().max_rect(header_controls_rect).layout(egui::Layout::left_to_right(egui::Align::Center)));
|
||||
let mut shared = panes::SharedPaneState {
|
||||
tool_icon_cache,
|
||||
icon_cache,
|
||||
selected_tool,
|
||||
fill_color,
|
||||
stroke_color,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
theme,
|
||||
pending_handlers,
|
||||
document,
|
||||
};
|
||||
pane_instance.render_header(&mut header_ui, &mut shared);
|
||||
}
|
||||
}
|
||||
|
||||
// Make pane content clickable (use full rect for split preview interaction)
|
||||
let pane_id = ui.id().with(("pane", path));
|
||||
let response = ui.interact(content_rect, pane_id, egui::Sense::click());
|
||||
let response = ui.interact(rect, pane_id, egui::Sense::click());
|
||||
|
||||
// Handle split preview mode
|
||||
// Render pane-specific content using trait-based system
|
||||
if let Some(pane_type) = pane_type {
|
||||
// Get or create pane instance for this path
|
||||
// Check if we need a new instance (either doesn't exist or type changed)
|
||||
let needs_new_instance = pane_instances
|
||||
.get(path)
|
||||
.map(|instance| instance.pane_type() != pane_type)
|
||||
.unwrap_or(true);
|
||||
|
||||
if needs_new_instance {
|
||||
pane_instances.insert(path.clone(), PaneInstance::new(pane_type));
|
||||
}
|
||||
|
||||
// Get the pane instance and render its content
|
||||
if let Some(pane_instance) = pane_instances.get_mut(path) {
|
||||
// Create shared state
|
||||
let mut shared = SharedPaneState {
|
||||
tool_icon_cache,
|
||||
icon_cache,
|
||||
selected_tool,
|
||||
fill_color,
|
||||
stroke_color,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
theme,
|
||||
pending_handlers,
|
||||
document,
|
||||
};
|
||||
|
||||
// Render pane content (header was already rendered above)
|
||||
pane_instance.render_content(ui, content_rect, path, &mut shared);
|
||||
}
|
||||
} else {
|
||||
// Unknown pane type - draw placeholder
|
||||
let content_text = "Unknown pane type";
|
||||
let text_pos = content_rect.center();
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
content_text,
|
||||
egui::FontId::proportional(16.0),
|
||||
egui::Color32::from_gray(150),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle split preview mode (rendered AFTER pane content for proper z-ordering)
|
||||
if let SplitPreviewMode::Active {
|
||||
is_horizontal,
|
||||
hovered_pane,
|
||||
|
|
@ -1126,8 +1282,12 @@ fn render_pane(
|
|||
);
|
||||
}
|
||||
|
||||
// Create a high-priority interaction for split preview (rendered last = highest priority)
|
||||
let split_preview_id = ui.id().with(("split_preview", path));
|
||||
let split_response = ui.interact(rect, split_preview_id, egui::Sense::click());
|
||||
|
||||
// If clicked, perform the split
|
||||
if response.clicked() {
|
||||
if split_response.clicked() {
|
||||
if *is_horizontal {
|
||||
*layout_action = Some(LayoutAction::SplitHorizontal(path.clone(), *split_percent));
|
||||
} else {
|
||||
|
|
@ -1141,60 +1301,6 @@ fn render_pane(
|
|||
} else if response.clicked() {
|
||||
*selected_pane = Some(path.clone());
|
||||
}
|
||||
|
||||
// Render pane-specific content using trait-based system
|
||||
if let Some(pane_type) = pane_type {
|
||||
// Get or create pane instance for this path
|
||||
// Check if we need a new instance (either doesn't exist or type changed)
|
||||
let needs_new_instance = pane_instances
|
||||
.get(path)
|
||||
.map(|instance| instance.pane_type() != pane_type)
|
||||
.unwrap_or(true);
|
||||
|
||||
if needs_new_instance {
|
||||
pane_instances.insert(path.clone(), PaneInstance::new(pane_type));
|
||||
}
|
||||
|
||||
// Get the pane instance and render it
|
||||
if let Some(pane_instance) = pane_instances.get_mut(path) {
|
||||
// Create shared state
|
||||
let mut shared = SharedPaneState {
|
||||
tool_icon_cache,
|
||||
icon_cache,
|
||||
selected_tool,
|
||||
fill_color,
|
||||
stroke_color,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
};
|
||||
|
||||
// Render pane header (if it has one)
|
||||
let has_header = pane_instance.render_header(ui, &mut shared);
|
||||
|
||||
// Adjust content rect if header was rendered
|
||||
let final_content_rect = if has_header {
|
||||
// Header was drawn by the pane, adjust content area
|
||||
content_rect
|
||||
} else {
|
||||
content_rect
|
||||
};
|
||||
|
||||
// Render pane content
|
||||
pane_instance.render_content(ui, final_content_rect, path, &mut shared);
|
||||
}
|
||||
} else {
|
||||
// Unknown pane type - draw placeholder
|
||||
let content_text = "Unknown pane type";
|
||||
let text_pos = content_rect.center();
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
content_text,
|
||||
egui::FontId::proportional(16.0),
|
||||
egui::Color32::from_gray(150),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render toolbar with tool buttons
|
||||
|
|
|
|||
|
|
@ -39,9 +39,12 @@ pub struct SharedPaneState<'a> {
|
|||
/// Lower number = higher priority. None = no fallback pane seen yet
|
||||
/// Priority order: Stage(0) > Timeline(1) > PianoRoll(2) > NodeEditor(3)
|
||||
pub fallback_pane_priority: &'a mut Option<u32>,
|
||||
pub theme: &'a crate::theme::Theme,
|
||||
/// Registry of handlers for the current pending action
|
||||
/// Panes register themselves here during render, execution happens after
|
||||
pub pending_handlers: &'a mut Vec<ViewActionHandler>,
|
||||
/// Active document being edited
|
||||
pub document: &'a mut lightningbeam_core::document::Document,
|
||||
}
|
||||
|
||||
/// Trait for pane rendering
|
||||
|
|
|
|||
|
|
@ -6,24 +6,28 @@ use eframe::egui;
|
|||
use super::{NodePath, PaneRenderer, SharedPaneState};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Resources for a single Vello instance
|
||||
struct VelloResources {
|
||||
/// Shared Vello resources (created once, reused by all Stage panes)
|
||||
struct SharedVelloResources {
|
||||
renderer: Arc<Mutex<vello::Renderer>>,
|
||||
texture: Option<wgpu::Texture>,
|
||||
texture_view: Option<wgpu::TextureView>,
|
||||
// Blit pipeline for rendering texture to screen
|
||||
blit_pipeline: wgpu::RenderPipeline,
|
||||
blit_bind_group_layout: wgpu::BindGroupLayout,
|
||||
sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
/// Per-instance Vello resources (created for each Stage pane)
|
||||
struct InstanceVelloResources {
|
||||
texture: Option<wgpu::Texture>,
|
||||
texture_view: Option<wgpu::TextureView>,
|
||||
blit_bind_group: Option<wgpu::BindGroup>,
|
||||
}
|
||||
|
||||
/// Container for all Vello instances, stored in egui's CallbackResources
|
||||
pub struct VelloResourcesMap {
|
||||
instances: std::collections::HashMap<u64, VelloResources>,
|
||||
shared: Option<Arc<SharedVelloResources>>,
|
||||
instances: std::collections::HashMap<u64, InstanceVelloResources>,
|
||||
}
|
||||
|
||||
impl VelloResources {
|
||||
impl SharedVelloResources {
|
||||
pub fn new(device: &wgpu::Device) -> Result<Self, String> {
|
||||
let renderer = vello::Renderer::new(
|
||||
device,
|
||||
|
|
@ -118,20 +122,27 @@ impl VelloResources {
|
|||
..Default::default()
|
||||
});
|
||||
|
||||
println!("✅ Vello renderer and blit pipeline initialized");
|
||||
println!("✅ Vello shared resources initialized (renderer and shaders)");
|
||||
|
||||
Ok(Self {
|
||||
renderer: Arc::new(Mutex::new(renderer)),
|
||||
texture: None,
|
||||
texture_view: None,
|
||||
blit_pipeline,
|
||||
blit_bind_group_layout,
|
||||
sampler,
|
||||
blit_bind_group: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_texture(&mut self, device: &wgpu::Device, width: u32, height: u32) {
|
||||
impl InstanceVelloResources {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
texture: None,
|
||||
texture_view: None,
|
||||
blit_bind_group: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_texture(&mut self, device: &wgpu::Device, shared: &SharedVelloResources, width: u32, height: u32) {
|
||||
// Clamp to GPU limits (most GPUs support up to 8192)
|
||||
let max_texture_size = 8192;
|
||||
let width = width.min(max_texture_size);
|
||||
|
|
@ -157,10 +168,10 @@ impl VelloResources {
|
|||
|
||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// Create bind group for blit pipeline
|
||||
// Create bind group for blit pipeline (using shared layout and sampler)
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("vello_blit_bind_group"),
|
||||
layout: &self.blit_bind_group_layout,
|
||||
layout: &shared.blit_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
|
|
@ -168,7 +179,7 @@ impl VelloResources {
|
|||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
resource: wgpu::BindingResource::Sampler(&shared.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -185,11 +196,12 @@ struct VelloCallback {
|
|||
pan_offset: egui::Vec2,
|
||||
zoom: f32,
|
||||
instance_id: u64,
|
||||
document: lightningbeam_core::document::Document,
|
||||
}
|
||||
|
||||
impl VelloCallback {
|
||||
fn new(rect: egui::Rect, pan_offset: egui::Vec2, zoom: f32, instance_id: u64) -> Self {
|
||||
Self { rect, pan_offset, zoom, instance_id }
|
||||
fn new(rect: egui::Rect, pan_offset: egui::Vec2, zoom: f32, instance_id: u64, document: lightningbeam_core::document::Document) -> Self {
|
||||
Self { rect, pan_offset, zoom, instance_id, document }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,15 +217,26 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
// Get or create the resources map
|
||||
if !resources.contains::<VelloResourcesMap>() {
|
||||
resources.insert(VelloResourcesMap {
|
||||
shared: None,
|
||||
instances: std::collections::HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let map: &mut VelloResourcesMap = resources.get_mut().unwrap();
|
||||
|
||||
// Get or create resources for this specific instance
|
||||
let vello_resources = map.instances.entry(self.instance_id).or_insert_with(|| {
|
||||
VelloResources::new(device).expect("Failed to initialize Vello renderer")
|
||||
// Initialize shared resources if not yet created (only happens once for first Stage pane)
|
||||
if map.shared.is_none() {
|
||||
map.shared = Some(Arc::new(
|
||||
SharedVelloResources::new(device).expect("Failed to initialize shared Vello resources")
|
||||
));
|
||||
}
|
||||
|
||||
let shared = map.shared.as_ref().unwrap().clone();
|
||||
|
||||
// Get or create per-instance resources
|
||||
let instance_resources = map.instances.entry(self.instance_id).or_insert_with(|| {
|
||||
println!("✅ Creating instance resources for Stage pane #{}", self.instance_id);
|
||||
InstanceVelloResources::new()
|
||||
});
|
||||
|
||||
// Ensure texture is the right size
|
||||
|
|
@ -224,46 +247,21 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
return Vec::new();
|
||||
}
|
||||
|
||||
vello_resources.ensure_texture(device, width, height);
|
||||
instance_resources.ensure_texture(device, &shared, width, height);
|
||||
|
||||
// Build Vello scene using the document renderer
|
||||
let mut scene = vello::Scene::new();
|
||||
|
||||
// Create a test document with a simple shape
|
||||
use lightningbeam_core::document::Document;
|
||||
use lightningbeam_core::layer::{AnyLayer, VectorLayer};
|
||||
use lightningbeam_core::object::Object;
|
||||
use lightningbeam_core::shape::{Shape, ShapeColor};
|
||||
use vello::kurbo::{Circle, Shape as KurboShape};
|
||||
|
||||
let mut doc = Document::new("Test Animation");
|
||||
|
||||
// Create a simple circle shape
|
||||
let circle = Circle::new((200.0, 150.0), 50.0);
|
||||
let path = circle.to_path(0.1);
|
||||
let shape = Shape::new(path).with_fill(ShapeColor::rgb(100, 150, 250));
|
||||
|
||||
// Create an object for the shape
|
||||
let object = Object::new(shape.id);
|
||||
|
||||
// Create a vector layer
|
||||
let mut vector_layer = VectorLayer::new("Layer 1");
|
||||
vector_layer.add_shape(shape);
|
||||
vector_layer.add_object(object);
|
||||
|
||||
// Add to document
|
||||
doc.root.add_child(AnyLayer::Vector(vector_layer));
|
||||
|
||||
// Build camera transform: translate for pan, scale for zoom
|
||||
use vello::kurbo::Affine;
|
||||
let camera_transform = Affine::translate((self.pan_offset.x as f64, self.pan_offset.y as f64))
|
||||
* Affine::scale(self.zoom as f64);
|
||||
|
||||
// Render the document to the scene with camera transform
|
||||
lightningbeam_core::renderer::render_document_with_transform(&doc, &mut scene, camera_transform);
|
||||
lightningbeam_core::renderer::render_document_with_transform(&self.document, &mut scene, camera_transform);
|
||||
|
||||
// Render scene to texture
|
||||
if let Some(texture_view) = &vello_resources.texture_view {
|
||||
// Render scene to texture using shared renderer
|
||||
if let Some(texture_view) = &instance_resources.texture_view {
|
||||
let render_params = vello::RenderParams {
|
||||
base_color: vello::peniko::Color::rgb8(45, 45, 48), // Dark background
|
||||
width,
|
||||
|
|
@ -271,7 +269,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
antialiasing_method: vello::AaConfig::Msaa16,
|
||||
};
|
||||
|
||||
if let Ok(mut renderer) = vello_resources.renderer.lock() {
|
||||
if let Ok(mut renderer) = shared.renderer.lock() {
|
||||
renderer
|
||||
.render_to_texture(device, queue, &scene, texture_view, &render_params)
|
||||
.ok();
|
||||
|
|
@ -293,20 +291,26 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
None => return, // Resources not initialized yet
|
||||
};
|
||||
|
||||
// Get resources for this specific instance
|
||||
let vello_resources = match map.instances.get(&self.instance_id) {
|
||||
// Get shared resources
|
||||
let shared = match &map.shared {
|
||||
Some(s) => s,
|
||||
None => return, // Shared resources not initialized yet
|
||||
};
|
||||
|
||||
// Get instance resources
|
||||
let instance_resources = match map.instances.get(&self.instance_id) {
|
||||
Some(r) => r,
|
||||
None => return, // Instance not initialized yet
|
||||
};
|
||||
|
||||
// Check if we have a bind group (texture ready)
|
||||
let bind_group = match &vello_resources.blit_bind_group {
|
||||
let bind_group = match &instance_resources.blit_bind_group {
|
||||
Some(bg) => bg,
|
||||
None => return, // Texture not ready yet
|
||||
};
|
||||
|
||||
// Render fullscreen quad with our texture
|
||||
render_pass.set_pipeline(&vello_resources.blit_pipeline);
|
||||
// Render fullscreen quad with our texture (using shared pipeline)
|
||||
render_pass.set_pipeline(&shared.blit_pipeline);
|
||||
render_pass.set_bind_group(0, bind_group, &[]);
|
||||
render_pass.draw(0..4, 0..1); // Triangle strip: 4 vertices
|
||||
}
|
||||
|
|
@ -527,7 +531,7 @@ impl PaneRenderer for StagePane {
|
|||
}
|
||||
|
||||
// Use egui's custom painting callback for Vello
|
||||
let callback = VelloCallback::new(rect, self.pan_offset, self.zoom, self.instance_id);
|
||||
let callback = VelloCallback::new(rect, self.pan_offset, self.zoom, self.instance_id, shared.document.clone());
|
||||
|
||||
let cb = egui_wgpu::Callback::new_paint_callback(
|
||||
rect,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,554 @@
|
|||
/// Timeline pane - frame-based animation timeline
|
||||
/// Timeline pane - Modern GarageBand-style timeline
|
||||
///
|
||||
/// This will eventually render keyframes, layers, and playback controls.
|
||||
/// For now, it's a placeholder.
|
||||
/// Phase 1 Implementation: Time Ruler & Playhead
|
||||
/// - Time-based ruler (seconds, not frames)
|
||||
/// - Playhead for current time
|
||||
/// - Zoom/pan controls
|
||||
/// - Basic layer visualization
|
||||
|
||||
use eframe::egui;
|
||||
use super::{NodePath, PaneRenderer, SharedPaneState};
|
||||
|
||||
const RULER_HEIGHT: f32 = 30.0;
|
||||
const LAYER_HEIGHT: f32 = 60.0;
|
||||
const LAYER_HEADER_WIDTH: f32 = 200.0;
|
||||
const MIN_PIXELS_PER_SECOND: f32 = 20.0;
|
||||
const MAX_PIXELS_PER_SECOND: f32 = 500.0;
|
||||
|
||||
pub struct TimelinePane {
|
||||
// TODO: Add state for zoom, scroll, playback, etc.
|
||||
/// Current playback time in seconds
|
||||
current_time: f64,
|
||||
|
||||
/// Horizontal zoom level (pixels per second)
|
||||
pixels_per_second: f32,
|
||||
|
||||
/// Horizontal scroll offset (in seconds)
|
||||
viewport_start_time: f64,
|
||||
|
||||
/// Vertical scroll offset (in pixels)
|
||||
viewport_scroll_y: f32,
|
||||
|
||||
/// Total duration of the animation
|
||||
duration: f64,
|
||||
|
||||
/// Is the user currently dragging the playhead?
|
||||
is_scrubbing: bool,
|
||||
|
||||
/// Is the user panning the timeline?
|
||||
is_panning: bool,
|
||||
last_pan_pos: Option<egui::Pos2>,
|
||||
|
||||
/// Is playback currently active?
|
||||
is_playing: bool,
|
||||
|
||||
/// Currently selected/active layer index
|
||||
active_layer: usize,
|
||||
}
|
||||
|
||||
impl TimelinePane {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
Self {
|
||||
current_time: 0.0,
|
||||
pixels_per_second: 100.0,
|
||||
viewport_start_time: 0.0,
|
||||
active_layer: 0,
|
||||
viewport_scroll_y: 0.0,
|
||||
duration: 10.0, // Default 10 seconds
|
||||
is_scrubbing: false,
|
||||
is_panning: false,
|
||||
last_pan_pos: None,
|
||||
is_playing: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a view action with the given parameters
|
||||
/// Called from main.rs after determining this is the best handler
|
||||
pub fn execute_view_action(&mut self, action: &crate::menu::MenuAction, zoom_center: egui::Vec2) {
|
||||
use crate::menu::MenuAction;
|
||||
match action {
|
||||
MenuAction::ZoomIn => self.zoom_in(zoom_center.x),
|
||||
MenuAction::ZoomOut => self.zoom_out(zoom_center.x),
|
||||
MenuAction::ActualSize => self.actual_size(),
|
||||
MenuAction::RecenterView => self.recenter(),
|
||||
_ => {} // Not a view action we handle
|
||||
}
|
||||
}
|
||||
|
||||
/// Zoom in by a fixed increment
|
||||
pub fn zoom_in(&mut self, center_x: f32) {
|
||||
self.apply_zoom_at_point(0.2, center_x);
|
||||
}
|
||||
|
||||
/// Zoom out by a fixed increment
|
||||
pub fn zoom_out(&mut self, center_x: f32) {
|
||||
self.apply_zoom_at_point(-0.2, center_x);
|
||||
}
|
||||
|
||||
/// Reset zoom to 100 pixels per second
|
||||
pub fn actual_size(&mut self) {
|
||||
self.pixels_per_second = 100.0;
|
||||
}
|
||||
|
||||
/// Reset pan to start and zoom to default
|
||||
pub fn recenter(&mut self) {
|
||||
self.viewport_start_time = 0.0;
|
||||
self.viewport_scroll_y = 0.0;
|
||||
self.pixels_per_second = 100.0;
|
||||
}
|
||||
|
||||
/// Apply zoom while keeping the time under the cursor stationary
|
||||
fn apply_zoom_at_point(&mut self, zoom_delta: f32, mouse_x: f32) {
|
||||
let old_zoom = self.pixels_per_second;
|
||||
|
||||
// Calculate time position under mouse before zoom
|
||||
let time_at_mouse = self.x_to_time(mouse_x);
|
||||
|
||||
// Apply zoom
|
||||
let new_zoom = (old_zoom * (1.0 + zoom_delta)).clamp(MIN_PIXELS_PER_SECOND, MAX_PIXELS_PER_SECOND);
|
||||
self.pixels_per_second = new_zoom;
|
||||
|
||||
// Adjust viewport so the same time stays under the mouse
|
||||
let new_mouse_x = self.time_to_x(time_at_mouse);
|
||||
let time_delta = (new_mouse_x - mouse_x) / new_zoom;
|
||||
self.viewport_start_time = (self.viewport_start_time + time_delta as f64).max(0.0);
|
||||
}
|
||||
|
||||
/// Convert time (seconds) to pixel x-coordinate
|
||||
fn time_to_x(&self, time: f64) -> f32 {
|
||||
((time - self.viewport_start_time) * self.pixels_per_second as f64) as f32
|
||||
}
|
||||
|
||||
/// Convert pixel x-coordinate to time (seconds)
|
||||
fn x_to_time(&self, x: f32) -> f64 {
|
||||
self.viewport_start_time + (x / self.pixels_per_second) as f64
|
||||
}
|
||||
|
||||
/// Calculate appropriate interval for time ruler based on zoom level
|
||||
fn calculate_ruler_interval(&self) -> f64 {
|
||||
// Target: 50-100px between major ticks
|
||||
let target_px = 75.0;
|
||||
let target_seconds = target_px / self.pixels_per_second;
|
||||
|
||||
// Standard intervals: 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100...
|
||||
let intervals = [0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0];
|
||||
|
||||
// Find the interval closest to our target
|
||||
intervals.iter()
|
||||
.min_by_key(|&&interval| ((interval - target_seconds as f64).abs() * 1000.0) as i32)
|
||||
.copied()
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// Render the time ruler at the top
|
||||
fn render_ruler(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) {
|
||||
let painter = ui.painter();
|
||||
|
||||
// Background
|
||||
let bg_style = theme.style(".timeline-background", ui.ctx());
|
||||
let bg_color = bg_style.background_color.unwrap_or(egui::Color32::from_rgb(34, 34, 34));
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
0.0,
|
||||
bg_color,
|
||||
);
|
||||
|
||||
// Get text color from theme
|
||||
let text_style = theme.style(".text-primary", ui.ctx());
|
||||
let text_color = text_style.text_color.unwrap_or(egui::Color32::from_gray(200));
|
||||
|
||||
// Calculate interval for tick marks
|
||||
let interval = self.calculate_ruler_interval();
|
||||
|
||||
// Draw tick marks and labels
|
||||
let start_time = (self.viewport_start_time / interval).floor() * interval;
|
||||
let end_time = self.x_to_time(rect.width());
|
||||
|
||||
let mut time = start_time;
|
||||
while time <= end_time {
|
||||
let x = self.time_to_x(time);
|
||||
|
||||
if x >= 0.0 && x <= rect.width() {
|
||||
// Major tick mark
|
||||
painter.line_segment(
|
||||
[
|
||||
rect.min + egui::vec2(x, rect.height() - 10.0),
|
||||
rect.min + egui::vec2(x, rect.height()),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(100)),
|
||||
);
|
||||
|
||||
// Time label
|
||||
let label = format!("{:.1}s", time);
|
||||
painter.text(
|
||||
rect.min + egui::vec2(x + 2.0, 5.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
label,
|
||||
egui::FontId::proportional(12.0),
|
||||
text_color,
|
||||
);
|
||||
}
|
||||
|
||||
// Minor tick marks (subdivisions)
|
||||
let minor_interval = interval / 5.0;
|
||||
for i in 1..5 {
|
||||
let minor_time = time + minor_interval * i as f64;
|
||||
let minor_x = self.time_to_x(minor_time);
|
||||
|
||||
if minor_x >= 0.0 && minor_x <= rect.width() {
|
||||
painter.line_segment(
|
||||
[
|
||||
rect.min + egui::vec2(minor_x, rect.height() - 5.0),
|
||||
rect.min + egui::vec2(minor_x, rect.height()),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(60)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the playhead (current time indicator)
|
||||
fn render_playhead(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) {
|
||||
let x = self.time_to_x(self.current_time);
|
||||
|
||||
if x >= 0.0 && x <= rect.width() {
|
||||
let painter = ui.painter();
|
||||
let scrubber_style = theme.style(".timeline-scrubber", ui.ctx());
|
||||
let scrubber_color = scrubber_style.background_color.unwrap_or(egui::Color32::from_rgb(204, 34, 34));
|
||||
|
||||
// Red vertical line
|
||||
painter.line_segment(
|
||||
[
|
||||
rect.min + egui::vec2(x, 0.0),
|
||||
egui::pos2(rect.min.x + x, rect.max.y),
|
||||
],
|
||||
egui::Stroke::new(2.0, scrubber_color),
|
||||
);
|
||||
|
||||
// Playhead handle (triangle at top)
|
||||
let handle_size = 8.0;
|
||||
let points = vec![
|
||||
rect.min + egui::vec2(x, 0.0),
|
||||
rect.min + egui::vec2(x - handle_size / 2.0, handle_size),
|
||||
rect.min + egui::vec2(x + handle_size / 2.0, handle_size),
|
||||
];
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
points,
|
||||
scrubber_color,
|
||||
egui::Stroke::NONE,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Render layer header column (left side with track names and controls)
|
||||
fn render_layer_headers(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) {
|
||||
let painter = ui.painter();
|
||||
|
||||
// Background for header column
|
||||
let header_style = theme.style(".timeline-header", ui.ctx());
|
||||
let header_bg = header_style.background_color.unwrap_or(egui::Color32::from_rgb(17, 17, 17));
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
0.0,
|
||||
header_bg,
|
||||
);
|
||||
|
||||
// Theme colors for active/inactive layers
|
||||
let active_style = theme.style(".timeline-layer-active", ui.ctx());
|
||||
let inactive_style = theme.style(".timeline-layer-inactive", ui.ctx());
|
||||
let active_color = active_style.background_color.unwrap_or(egui::Color32::from_rgb(79, 79, 79));
|
||||
let inactive_color = inactive_style.background_color.unwrap_or(egui::Color32::from_rgb(51, 51, 51));
|
||||
|
||||
// Get text color from theme
|
||||
let text_style = theme.style(".text-primary", ui.ctx());
|
||||
let text_color = text_style.text_color.unwrap_or(egui::Color32::from_gray(200));
|
||||
|
||||
// Test: Draw 3 layer headers
|
||||
for i in 0..3 {
|
||||
let y = rect.min.y + i as f32 * LAYER_HEIGHT - self.viewport_scroll_y;
|
||||
|
||||
// Skip if layer is outside visible area
|
||||
if y + LAYER_HEIGHT < rect.min.y || y > rect.max.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(rect.min.x, y),
|
||||
egui::vec2(LAYER_HEADER_WIDTH, LAYER_HEIGHT),
|
||||
);
|
||||
|
||||
// Active vs inactive background colors
|
||||
let bg_color = if i == self.active_layer {
|
||||
active_color
|
||||
} else {
|
||||
inactive_color
|
||||
};
|
||||
|
||||
painter.rect_filled(header_rect, 0.0, bg_color);
|
||||
|
||||
// Layer name
|
||||
painter.text(
|
||||
header_rect.min + egui::vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("Layer {}", i + 1),
|
||||
egui::FontId::proportional(14.0),
|
||||
text_color,
|
||||
);
|
||||
|
||||
// Separator line at bottom
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(header_rect.min.x, header_rect.max.y),
|
||||
egui::pos2(header_rect.max.x, header_rect.max.y),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(20)),
|
||||
);
|
||||
}
|
||||
|
||||
// Right border for header column
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(rect.max.x, rect.min.y),
|
||||
egui::pos2(rect.max.x, rect.max.y),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(20)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Render layer rows (timeline content area)
|
||||
fn render_layers(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) {
|
||||
let painter = ui.painter();
|
||||
|
||||
// Theme colors for active/inactive layers
|
||||
let active_style = theme.style(".timeline-row-active", ui.ctx());
|
||||
let inactive_style = theme.style(".timeline-row-inactive", ui.ctx());
|
||||
let active_color = active_style.background_color.unwrap_or(egui::Color32::from_rgb(85, 85, 85));
|
||||
let inactive_color = inactive_style.background_color.unwrap_or(egui::Color32::from_rgb(136, 136, 136));
|
||||
|
||||
// Test: Draw 3 layer rows
|
||||
for i in 0..3 {
|
||||
let y = rect.min.y + i as f32 * LAYER_HEIGHT - self.viewport_scroll_y;
|
||||
|
||||
// Skip if layer is outside visible area
|
||||
if y + LAYER_HEIGHT < rect.min.y || y > rect.max.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let layer_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(rect.min.x, y),
|
||||
egui::vec2(rect.width(), LAYER_HEIGHT),
|
||||
);
|
||||
|
||||
// Active vs inactive background colors
|
||||
let bg_color = if i == self.active_layer {
|
||||
active_color
|
||||
} else {
|
||||
inactive_color
|
||||
};
|
||||
|
||||
painter.rect_filled(layer_rect, 0.0, bg_color);
|
||||
|
||||
// Grid lines matching ruler
|
||||
let interval = self.calculate_ruler_interval();
|
||||
let start_time = (self.viewport_start_time / interval).floor() * interval;
|
||||
let end_time = self.x_to_time(rect.width());
|
||||
|
||||
let mut time = start_time;
|
||||
while time <= end_time {
|
||||
let x = self.time_to_x(time);
|
||||
|
||||
if x >= 0.0 && x <= rect.width() {
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(rect.min.x + x, y),
|
||||
egui::pos2(rect.min.x + x, y + LAYER_HEIGHT),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(30)),
|
||||
);
|
||||
}
|
||||
|
||||
time += interval;
|
||||
}
|
||||
|
||||
// Separator line at bottom
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(layer_rect.min.x, layer_rect.max.y),
|
||||
egui::pos2(layer_rect.max.x, layer_rect.max.y),
|
||||
],
|
||||
egui::Stroke::new(1.0, egui::Color32::from_gray(20)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle mouse input for scrubbing, panning, and zooming
|
||||
fn handle_input(&mut self, ui: &mut egui::Ui, full_timeline_rect: egui::Rect, ruler_rect: egui::Rect, content_rect: egui::Rect) {
|
||||
let response = ui.allocate_rect(full_timeline_rect, egui::Sense::click_and_drag());
|
||||
|
||||
// Only process input if mouse is over the timeline pane
|
||||
if !response.hovered() {
|
||||
self.is_panning = false;
|
||||
self.last_pan_pos = None;
|
||||
self.is_scrubbing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let alt_held = ui.input(|i| i.modifiers.alt);
|
||||
let ctrl_held = ui.input(|i| i.modifiers.ctrl || i.modifiers.command);
|
||||
|
||||
// Get mouse position relative to content area
|
||||
let mouse_pos = response.hover_pos().unwrap_or(content_rect.center());
|
||||
let mouse_x = (mouse_pos.x - content_rect.min.x).max(0.0);
|
||||
|
||||
// Calculate max vertical scroll based on number of layers
|
||||
// TODO: Get actual layer count from document - for now using test count of 3
|
||||
const TEST_LAYER_COUNT: usize = 3;
|
||||
let total_content_height = TEST_LAYER_COUNT as f32 * LAYER_HEIGHT;
|
||||
let visible_height = content_rect.height();
|
||||
let max_scroll_y = (total_content_height - visible_height).max(0.0);
|
||||
|
||||
// Scrubbing (clicking/dragging on ruler, but only when not panning)
|
||||
if ruler_rect.contains(ui.input(|i| i.pointer.hover_pos().unwrap_or_default())) && !alt_held {
|
||||
if response.clicked() || (response.dragged() && !self.is_panning) {
|
||||
if let Some(pos) = response.interact_pointer_pos() {
|
||||
let x = (pos.x - content_rect.min.x).max(0.0);
|
||||
self.current_time = self.x_to_time(x).max(0.0).min(self.duration);
|
||||
self.is_scrubbing = true;
|
||||
}
|
||||
} else if !response.dragged() {
|
||||
self.is_scrubbing = false;
|
||||
}
|
||||
} else {
|
||||
if !response.dragged() {
|
||||
self.is_scrubbing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Distinguish between mouse wheel (discrete) and trackpad (smooth)
|
||||
let mut handled = false;
|
||||
ui.input(|i| {
|
||||
for event in &i.raw.events {
|
||||
if let egui::Event::MouseWheel { unit, delta, modifiers, .. } = event {
|
||||
match unit {
|
||||
egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => {
|
||||
// Real mouse wheel (discrete clicks) -> always zoom horizontally
|
||||
let zoom_delta = if ctrl_held || modifiers.ctrl {
|
||||
delta.y * 0.01 // Ctrl+wheel: faster zoom
|
||||
} else {
|
||||
delta.y * 0.005 // Normal zoom
|
||||
};
|
||||
self.apply_zoom_at_point(zoom_delta, mouse_x);
|
||||
handled = true;
|
||||
}
|
||||
egui::MouseWheelUnit::Point => {
|
||||
// Trackpad (smooth scrolling)
|
||||
if ctrl_held || modifiers.ctrl {
|
||||
// Ctrl held: zoom
|
||||
let zoom_delta = delta.y * 0.005;
|
||||
self.apply_zoom_at_point(zoom_delta, mouse_x);
|
||||
handled = true;
|
||||
}
|
||||
// Otherwise let scroll_delta handle panning (below)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle scroll_delta for trackpad panning (when Ctrl not held)
|
||||
if !handled {
|
||||
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
|
||||
if scroll_delta.x.abs() > 0.0 || scroll_delta.y.abs() > 0.0 {
|
||||
// Horizontal scroll: pan timeline (inverted: positive delta scrolls left/earlier in time)
|
||||
let delta_time = scroll_delta.x / self.pixels_per_second;
|
||||
self.viewport_start_time = (self.viewport_start_time - delta_time as f64).max(0.0);
|
||||
|
||||
// Vertical scroll: scroll layers vertically (clamped to content bounds)
|
||||
self.viewport_scroll_y = (self.viewport_scroll_y - scroll_delta.y).clamp(0.0, max_scroll_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle panning with Alt+Drag (timeline scrolls left/right, layers scroll up/down)
|
||||
if alt_held && response.dragged() && !self.is_scrubbing {
|
||||
if let Some(last_pos) = self.last_pan_pos {
|
||||
if let Some(current_pos) = response.interact_pointer_pos() {
|
||||
let delta = current_pos - last_pos;
|
||||
|
||||
// Horizontal pan: timeline
|
||||
let delta_time = delta.x / self.pixels_per_second;
|
||||
self.viewport_start_time = (self.viewport_start_time - delta_time as f64).max(0.0);
|
||||
|
||||
// Vertical pan: layers (clamped to content bounds)
|
||||
self.viewport_scroll_y = (self.viewport_scroll_y - delta.y).clamp(0.0, max_scroll_y);
|
||||
}
|
||||
}
|
||||
self.last_pan_pos = response.interact_pointer_pos();
|
||||
self.is_panning = true;
|
||||
} else {
|
||||
if !response.dragged() {
|
||||
self.is_panning = false;
|
||||
self.last_pan_pos = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaneRenderer for TimelinePane {
|
||||
fn render_header(&mut self, ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool {
|
||||
// TODO: Add playback controls (play/pause, frame counter, zoom)
|
||||
fn render_header(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool {
|
||||
ui.spacing_mut().item_spacing.x = 2.0; // Small spacing between button groups
|
||||
|
||||
// Main playback controls group
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("⏯");
|
||||
ui.label("Frame: 0");
|
||||
ui.label("FPS: 24");
|
||||
ui.spacing_mut().item_spacing.x = 0.0; // No spacing between buttons
|
||||
let button_size = egui::vec2(32.0, 28.0); // Larger buttons
|
||||
|
||||
// Go to start
|
||||
if ui.add_sized(button_size, egui::Button::new("|◀")).clicked() {
|
||||
self.current_time = 0.0;
|
||||
}
|
||||
|
||||
// Rewind (step backward)
|
||||
if ui.add_sized(button_size, egui::Button::new("◀◀")).clicked() {
|
||||
self.current_time = (self.current_time - 0.1).max(0.0);
|
||||
}
|
||||
|
||||
// Play/Pause toggle
|
||||
let play_pause_text = if self.is_playing { "⏸" } else { "▶" };
|
||||
if ui.add_sized(button_size, egui::Button::new(play_pause_text)).clicked() {
|
||||
self.is_playing = !self.is_playing;
|
||||
// TODO: Actually start/stop playback
|
||||
}
|
||||
|
||||
// Fast forward (step forward)
|
||||
if ui.add_sized(button_size, egui::Button::new("▶▶")).clicked() {
|
||||
self.current_time = (self.current_time + 0.1).min(self.duration);
|
||||
}
|
||||
|
||||
// Go to end
|
||||
if ui.add_sized(button_size, egui::Button::new("▶|")).clicked() {
|
||||
self.current_time = self.duration;
|
||||
}
|
||||
});
|
||||
true // Header was rendered
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Get text color from theme
|
||||
let text_style = shared.theme.style(".text-primary", ui.ctx());
|
||||
let text_color = text_style.text_color.unwrap_or(egui::Color32::from_gray(200));
|
||||
|
||||
// Time display
|
||||
ui.colored_label(text_color, format!("Time: {:.2}s / {:.2}s", self.current_time, self.duration));
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Zoom display
|
||||
ui.colored_label(text_color, format!("Zoom: {:.0}px/s", self.pixels_per_second));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
|
|
@ -32,23 +556,128 @@ impl PaneRenderer for TimelinePane {
|
|||
ui: &mut egui::Ui,
|
||||
rect: egui::Rect,
|
||||
_path: &NodePath,
|
||||
_shared: &mut SharedPaneState,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
// Placeholder rendering
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
0.0,
|
||||
egui::Color32::from_rgb(40, 30, 50),
|
||||
// Split into layer header column (left) and timeline content (right)
|
||||
let header_column_rect = egui::Rect::from_min_size(
|
||||
rect.min,
|
||||
egui::vec2(LAYER_HEADER_WIDTH, rect.height()),
|
||||
);
|
||||
|
||||
let text = "Timeline Pane\n(TODO: Implement frame scrubbing)";
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
egui::FontId::proportional(16.0),
|
||||
egui::Color32::from_gray(150),
|
||||
let timeline_rect = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2(LAYER_HEADER_WIDTH, 0.0),
|
||||
egui::vec2(rect.width() - LAYER_HEADER_WIDTH, rect.height()),
|
||||
);
|
||||
|
||||
// Split timeline into ruler and content areas
|
||||
let ruler_rect = egui::Rect::from_min_size(
|
||||
timeline_rect.min,
|
||||
egui::vec2(timeline_rect.width(), RULER_HEIGHT),
|
||||
);
|
||||
|
||||
let content_rect = egui::Rect::from_min_size(
|
||||
timeline_rect.min + egui::vec2(0.0, RULER_HEIGHT),
|
||||
egui::vec2(timeline_rect.width(), timeline_rect.height() - RULER_HEIGHT),
|
||||
);
|
||||
|
||||
// Split header column into ruler area (top) and layer headers (bottom)
|
||||
let header_ruler_spacer = egui::Rect::from_min_size(
|
||||
header_column_rect.min,
|
||||
egui::vec2(LAYER_HEADER_WIDTH, RULER_HEIGHT),
|
||||
);
|
||||
|
||||
let layer_headers_rect = egui::Rect::from_min_size(
|
||||
header_column_rect.min + egui::vec2(0.0, RULER_HEIGHT),
|
||||
egui::vec2(LAYER_HEADER_WIDTH, header_column_rect.height() - RULER_HEIGHT),
|
||||
);
|
||||
|
||||
// Save original clip rect to restore at the end
|
||||
let original_clip_rect = ui.clip_rect();
|
||||
|
||||
// Render spacer above layer headers (same height as ruler)
|
||||
let spacer_style = shared.theme.style(".timeline-spacer", ui.ctx());
|
||||
let spacer_bg = spacer_style.background_color.unwrap_or(egui::Color32::from_rgb(17, 17, 17));
|
||||
ui.painter().rect_filled(
|
||||
header_ruler_spacer,
|
||||
0.0,
|
||||
spacer_bg,
|
||||
);
|
||||
|
||||
// Render layer header column with clipping
|
||||
ui.set_clip_rect(layer_headers_rect.intersect(original_clip_rect));
|
||||
self.render_layer_headers(ui, layer_headers_rect, shared.theme);
|
||||
|
||||
// Render time ruler (clip to ruler rect)
|
||||
ui.set_clip_rect(ruler_rect.intersect(original_clip_rect));
|
||||
self.render_ruler(ui, ruler_rect, shared.theme);
|
||||
|
||||
// Render layer rows with clipping
|
||||
ui.set_clip_rect(content_rect.intersect(original_clip_rect));
|
||||
self.render_layers(ui, content_rect, shared.theme);
|
||||
|
||||
// Render playhead on top (clip to timeline area)
|
||||
ui.set_clip_rect(timeline_rect.intersect(original_clip_rect));
|
||||
self.render_playhead(ui, timeline_rect, shared.theme);
|
||||
|
||||
// Restore original clip rect
|
||||
ui.set_clip_rect(original_clip_rect);
|
||||
|
||||
// Handle input (use full rect including header column)
|
||||
self.handle_input(ui, rect, ruler_rect, content_rect);
|
||||
|
||||
// Register handler for pending view actions (two-phase dispatch)
|
||||
// Priority: Mouse-over (0-99) > Fallback Timeline(1001)
|
||||
const TIMELINE_MOUSE_OVER_PRIORITY: u32 = 0;
|
||||
const TIMELINE_FALLBACK_PRIORITY: u32 = 1001;
|
||||
|
||||
let mouse_over = ui.rect_contains_pointer(rect);
|
||||
|
||||
// Determine our priority for this action
|
||||
let our_priority = if mouse_over {
|
||||
TIMELINE_MOUSE_OVER_PRIORITY // High priority - mouse is over this pane
|
||||
} else {
|
||||
TIMELINE_FALLBACK_PRIORITY // Low priority - just a fallback option
|
||||
};
|
||||
|
||||
// Check if we should register as a handler (better priority than current best)
|
||||
let should_register = shared.pending_view_action.is_some() &&
|
||||
shared.fallback_pane_priority.map_or(true, |p| our_priority < p);
|
||||
|
||||
if should_register {
|
||||
// Update fallback priority tracker
|
||||
*shared.fallback_pane_priority = Some(our_priority);
|
||||
|
||||
// Register as a handler (don't execute yet - that happens after all panes render)
|
||||
if let Some(action) = &shared.pending_view_action {
|
||||
use crate::menu::MenuAction;
|
||||
|
||||
// Determine zoom center point (use x-position only for timeline horizontal zoom)
|
||||
let center = if mouse_over {
|
||||
// Use mouse position for zoom-to-cursor
|
||||
let mouse_pos = ui.input(|i| i.pointer.hover_pos()).unwrap_or(rect.center());
|
||||
mouse_pos - rect.min
|
||||
} else {
|
||||
// Use center of viewport for fallback
|
||||
rect.size() / 2.0
|
||||
};
|
||||
|
||||
// Only register for actions we can handle
|
||||
match action {
|
||||
MenuAction::ZoomIn | MenuAction::ZoomOut |
|
||||
MenuAction::ActualSize | MenuAction::RecenterView => {
|
||||
shared.pending_handlers.push(super::ViewActionHandler {
|
||||
priority: our_priority,
|
||||
pane_path: _path.clone(),
|
||||
zoom_center: center,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Not a view action we handle - reset priority so others can try
|
||||
*shared.fallback_pane_priority = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,405 @@
|
|||
/// Theme system for Lightningbeam Editor
|
||||
///
|
||||
/// Parses CSS rules from assets/styles.css at runtime
|
||||
/// and provides type-safe access to styles via selectors.
|
||||
|
||||
use eframe::egui;
|
||||
use lightningcss::stylesheet::{ParserOptions, PrinterOptions, StyleSheet};
|
||||
use lightningcss::traits::ToCss;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
Dark,
|
||||
System, // Follow system preference
|
||||
}
|
||||
|
||||
/// Style properties that can be applied to UI elements
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Style {
|
||||
pub background_color: Option<egui::Color32>,
|
||||
pub border_color: Option<egui::Color32>,
|
||||
pub text_color: Option<egui::Color32>,
|
||||
pub width: Option<f32>,
|
||||
pub height: Option<f32>,
|
||||
// Add more properties as needed
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Merge another style into this one (other's properties override if present)
|
||||
pub fn merge(&mut self, other: &Style) {
|
||||
if other.background_color.is_some() {
|
||||
self.background_color = other.background_color;
|
||||
}
|
||||
if other.border_color.is_some() {
|
||||
self.border_color = other.border_color;
|
||||
}
|
||||
if other.text_color.is_some() {
|
||||
self.text_color = other.text_color;
|
||||
}
|
||||
if other.width.is_some() {
|
||||
self.width = other.width;
|
||||
}
|
||||
if other.height.is_some() {
|
||||
self.height = other.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Theme {
|
||||
light_variables: HashMap<String, String>,
|
||||
dark_variables: HashMap<String, String>,
|
||||
light_styles: HashMap<String, Style>,
|
||||
dark_styles: HashMap<String, Style>,
|
||||
current_mode: ThemeMode,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
/// Load theme from CSS file
|
||||
pub fn from_css(css: &str) -> Result<Self, String> {
|
||||
let stylesheet = StyleSheet::parse(
|
||||
css,
|
||||
ParserOptions::default(),
|
||||
).map_err(|e| format!("Failed to parse CSS: {:?}", e))?;
|
||||
|
||||
let mut light_variables = HashMap::new();
|
||||
let mut dark_variables = HashMap::new();
|
||||
let mut light_styles = HashMap::new();
|
||||
let mut dark_styles = HashMap::new();
|
||||
|
||||
// First pass: Extract CSS custom properties from :root
|
||||
for rule in &stylesheet.rules.0 {
|
||||
match rule {
|
||||
lightningcss::rules::CssRule::Style(style_rule) => {
|
||||
let selectors = style_rule.selectors.0.iter()
|
||||
.filter_map(|s| s.to_css_string(PrinterOptions::default()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Check if this is :root
|
||||
if selectors.iter().any(|s| s.contains(":root")) {
|
||||
extract_css_variables(&style_rule.declarations, &mut light_variables)?;
|
||||
}
|
||||
}
|
||||
lightningcss::rules::CssRule::Media(media_rule) => {
|
||||
let media_str = media_rule.query.to_css_string(PrinterOptions::default())
|
||||
.unwrap_or_default();
|
||||
|
||||
if media_str.contains("prefers-color-scheme") && media_str.contains("dark") {
|
||||
for inner_rule in &media_rule.rules.0 {
|
||||
if let lightningcss::rules::CssRule::Style(style_rule) = inner_rule {
|
||||
let selectors = style_rule.selectors.0.iter()
|
||||
.filter_map(|s| s.to_css_string(PrinterOptions::default()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if selectors.iter().any(|s| s.contains(":root")) {
|
||||
extract_css_variables(&style_rule.declarations, &mut dark_variables)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Parse style rules and resolve var() references
|
||||
// We need to parse selectors TWICE - once with light variables, once with dark variables
|
||||
for rule in &stylesheet.rules.0 {
|
||||
match rule {
|
||||
lightningcss::rules::CssRule::Style(style_rule) => {
|
||||
let selectors = style_rule.selectors.0.iter()
|
||||
.filter_map(|s| s.to_css_string(PrinterOptions::default()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for selector in selectors {
|
||||
let selector = selector.trim();
|
||||
// Only process class and ID selectors
|
||||
if selector.starts_with('.') || selector.starts_with('#') {
|
||||
// Parse with light variables
|
||||
let light_style = parse_style_properties(&style_rule.declarations, &light_variables)?;
|
||||
light_styles.insert(selector.to_string(), light_style);
|
||||
|
||||
// Also parse with dark variables (merge dark over light)
|
||||
let mut dark_vars = light_variables.clone();
|
||||
dark_vars.extend(dark_variables.clone());
|
||||
let dark_style = parse_style_properties(&style_rule.declarations, &dark_vars)?;
|
||||
dark_styles.insert(selector.to_string(), dark_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
lightningcss::rules::CssRule::Media(media_rule) => {
|
||||
let media_str = media_rule.query.to_css_string(PrinterOptions::default())
|
||||
.unwrap_or_default();
|
||||
|
||||
eprintln!("🔍 Found media query: {}", media_str);
|
||||
eprintln!(" Contains {} rules", media_rule.rules.0.len());
|
||||
|
||||
if media_str.contains("prefers-color-scheme") && media_str.contains("dark") {
|
||||
eprintln!(" ✓ This is a dark mode media query!");
|
||||
for (i, inner_rule) in media_rule.rules.0.iter().enumerate() {
|
||||
eprintln!(" Rule {}: {:?}", i, std::mem::discriminant(inner_rule));
|
||||
if let lightningcss::rules::CssRule::Style(style_rule) = inner_rule {
|
||||
let selectors = style_rule.selectors.0.iter()
|
||||
.filter_map(|s| s.to_css_string(PrinterOptions::default()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
eprintln!(" Found selectors: {:?}", selectors);
|
||||
|
||||
for selector in selectors {
|
||||
let selector = selector.trim();
|
||||
if selector.starts_with('.') || selector.starts_with('#') {
|
||||
// Merge dark and light variables (dark overrides light)
|
||||
let mut vars = light_variables.clone();
|
||||
vars.extend(dark_variables.clone());
|
||||
let style = parse_style_properties(&style_rule.declarations, &vars)?;
|
||||
dark_styles.insert(selector.to_string(), style);
|
||||
eprintln!(" Added dark style for: {}", selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
light_variables,
|
||||
dark_variables,
|
||||
light_styles,
|
||||
dark_styles,
|
||||
current_mode: ThemeMode::System,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load theme from embedded CSS file
|
||||
pub fn load_default() -> Result<Self, String> {
|
||||
let css = include_str!("../assets/styles.css");
|
||||
Self::from_css(css)
|
||||
}
|
||||
|
||||
/// Set the current theme mode
|
||||
pub fn set_mode(&mut self, mode: ThemeMode) {
|
||||
self.current_mode = mode;
|
||||
}
|
||||
|
||||
/// Get the current theme mode
|
||||
pub fn mode(&self) -> ThemeMode {
|
||||
self.current_mode
|
||||
}
|
||||
|
||||
/// Get style for a selector (e.g., ".panel" or "#timeline-header")
|
||||
pub fn style(&self, selector: &str, ctx: &egui::Context) -> Style {
|
||||
let is_dark = match self.current_mode {
|
||||
ThemeMode::Light => false,
|
||||
ThemeMode::Dark => true,
|
||||
ThemeMode::System => ctx.style().visuals.dark_mode,
|
||||
};
|
||||
|
||||
if is_dark {
|
||||
// Try dark style first, fall back to light style
|
||||
self.dark_styles.get(selector).cloned()
|
||||
.or_else(|| self.light_styles.get(selector).cloned())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
self.light_styles.get(selector).cloned().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a CSS variable value and parse as color (backward compatibility helper)
|
||||
/// This allows old code using theme.color("variable-name") to work
|
||||
pub fn color(&self, var_name: &str) -> Option<egui::Color32> {
|
||||
// Try light variables first, then dark variables
|
||||
let value = self.light_variables.get(var_name)
|
||||
.or_else(|| self.dark_variables.get(var_name))?;
|
||||
parse_hex_color(value)
|
||||
}
|
||||
|
||||
/// Get the number of loaded selectors
|
||||
pub fn len(&self) -> usize {
|
||||
self.light_styles.len()
|
||||
}
|
||||
|
||||
/// Check if theme has no styles
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.light_styles.is_empty()
|
||||
}
|
||||
|
||||
/// Debug: print loaded theme info
|
||||
pub fn debug_print(&self) {
|
||||
println!("📊 Theme Debug Info:");
|
||||
println!(" Light variables: {}", self.light_variables.len());
|
||||
for (k, v) in self.light_variables.iter().take(5) {
|
||||
println!(" --{}: {}", k, v);
|
||||
}
|
||||
println!(" Dark variables: {}", self.dark_variables.len());
|
||||
for (k, v) in self.dark_variables.iter().take(5) {
|
||||
println!(" --{}: {}", k, v);
|
||||
}
|
||||
println!(" Light styles: {}", self.light_styles.len());
|
||||
for k in self.light_styles.keys().take(5) {
|
||||
println!(" {}", k);
|
||||
}
|
||||
println!(" Dark styles: {}", self.dark_styles.len());
|
||||
for k in self.dark_styles.keys().take(5) {
|
||||
println!(" {}", k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract CSS custom properties (--variables) from declarations
|
||||
fn extract_css_variables(
|
||||
declarations: &lightningcss::declaration::DeclarationBlock,
|
||||
variables: &mut HashMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
for property in &declarations.declarations {
|
||||
if let lightningcss::properties::Property::Custom(_) = property {
|
||||
let property_css = property.to_css_string(false, PrinterOptions::default())
|
||||
.map_err(|e| format!("Failed to serialize property: {:?}", e))?;
|
||||
|
||||
if let Some((name, value)) = property_css.split_once(':') {
|
||||
let name = name.trim().strip_prefix("--").unwrap_or(name.trim()).to_string();
|
||||
let value = value.trim().to_string();
|
||||
variables.insert(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse style properties from CSS declarations into a Style struct, resolving var() references
|
||||
fn parse_style_properties(
|
||||
declarations: &lightningcss::declaration::DeclarationBlock,
|
||||
variables: &HashMap<String, String>,
|
||||
) -> Result<Style, String> {
|
||||
let mut style = Style::default();
|
||||
|
||||
for property in &declarations.declarations {
|
||||
// Convert property to CSS string and parse
|
||||
let prop_str = property.to_css_string(false, PrinterOptions::default())
|
||||
.map_err(|e| format!("Failed to serialize property: {:?}", e))?;
|
||||
|
||||
// Parse property name and value
|
||||
if let Some((name, value)) = prop_str.split_once(':') {
|
||||
let name = name.trim();
|
||||
let value = value.trim().trim_end_matches(';');
|
||||
|
||||
match name {
|
||||
"background-color" => {
|
||||
style.background_color = parse_color_value(value, variables);
|
||||
}
|
||||
"border-color" | "border-top-color" => {
|
||||
style.border_color = parse_color_value(value, variables);
|
||||
}
|
||||
"color" => {
|
||||
style.text_color = parse_color_value(value, variables);
|
||||
}
|
||||
"width" => {
|
||||
style.width = parse_dimension_value(value, variables);
|
||||
}
|
||||
"height" => {
|
||||
style.height = parse_dimension_value(value, variables);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(style)
|
||||
}
|
||||
|
||||
/// Parse a CSS color value (hex or var())
|
||||
fn parse_color_value(value: &str, variables: &HashMap<String, String>) -> Option<egui::Color32> {
|
||||
let value = value.trim();
|
||||
|
||||
// Check if it's a var() reference
|
||||
if let Some(var_name) = parse_var_reference(value) {
|
||||
let resolved = variables.get(&var_name)?;
|
||||
return parse_hex_color(resolved);
|
||||
}
|
||||
|
||||
// Try to parse as direct hex color
|
||||
parse_hex_color(value)
|
||||
}
|
||||
|
||||
/// Parse a CSS dimension value (px or var())
|
||||
fn parse_dimension_value(value: &str, variables: &HashMap<String, String>) -> Option<f32> {
|
||||
let value = value.trim();
|
||||
|
||||
// Check if it's a var() reference
|
||||
if let Some(var_name) = parse_var_reference(value) {
|
||||
let resolved = variables.get(&var_name)?;
|
||||
return parse_dimension_string(resolved);
|
||||
}
|
||||
|
||||
// Try to parse as direct dimension
|
||||
parse_dimension_string(value)
|
||||
}
|
||||
|
||||
/// Parse a var() reference to get the variable name
|
||||
fn parse_var_reference(value: &str) -> Option<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.starts_with("var(") && trimmed.ends_with(')') {
|
||||
let inner = trimmed.strip_prefix("var(")?.strip_suffix(')')?;
|
||||
let var_name = inner.trim().strip_prefix("--")?;
|
||||
Some(var_name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse hex color string to egui::Color32
|
||||
fn parse_hex_color(value: &str) -> Option<egui::Color32> {
|
||||
let value = value.trim();
|
||||
if !value.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hex = value.trim_start_matches('#');
|
||||
match hex.len() {
|
||||
3 => {
|
||||
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
|
||||
Some(egui::Color32::from_rgb(r, g, b))
|
||||
}
|
||||
6 => {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
Some(egui::Color32::from_rgb(r, g, b))
|
||||
}
|
||||
8 => {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
|
||||
Some(egui::Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse dimension string (e.g., "50px" or "25")
|
||||
fn parse_dimension_string(value: &str) -> Option<f32> {
|
||||
let value = value.trim();
|
||||
if let Some(stripped) = value.strip_suffix("px") {
|
||||
stripped.trim().parse::<f32>().ok()
|
||||
} else {
|
||||
value.parse::<f32>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_load_default_theme() {
|
||||
let theme = Theme::load_default().expect("Failed to load default theme");
|
||||
assert!(!theme.is_empty(), "Theme should have styles loaded");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue