Merge pull request #8 from pop-os/menu

Add menu and tree view
This commit is contained in:
Jeremy Soller 2023-10-27 15:00:09 -06:00 committed by GitHub
commit 4faee3956e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 855 additions and 826 deletions

9
.gitignore vendored
View file

@ -1 +1,8 @@
/target
/.cargo/
/debian/*debhelper*
/debian/cosmic-greeter.substvars
/debian/cosmic-greeter/
/debian/files
/target/
/vendor.tar
/vendor/

150
Cargo.lock generated
View file

@ -99,9 +99,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd"
dependencies = [
"getrandom",
"once_cell",
@ -110,9 +110,9 @@ dependencies = [
[[package]]
name = "ahash"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72832d73be48bac96a5d7944568f305d829ed55b0ce3b483647089dfaf6cf704"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
dependencies = [
"cfg-if",
"once_cell",
@ -303,7 +303,7 @@ dependencies = [
"log",
"parking",
"polling 2.8.0",
"rustix 0.37.26",
"rustix 0.37.27",
"slab",
"socket2",
"waker-fn",
@ -331,7 +331,7 @@ dependencies = [
"cfg-if",
"event-listener 3.0.0",
"futures-lite",
"rustix 0.38.20",
"rustix 0.38.21",
"windows-sys 0.48.0",
]
@ -358,7 +358,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix 0.38.20",
"rustix 0.38.21",
"signal-hook-registry",
"slab",
"windows-sys 0.48.0",
@ -405,7 +405,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4d45f362125ed144544e57b0ec6de8fd6a296d41a6252fc4a20c0cf12e9ed3a"
dependencies = [
"rustix 0.38.20",
"rustix 0.38.21",
"tempfile",
"windows-sys 0.48.0",
]
@ -624,7 +624,7 @@ dependencies = [
"bitflags 2.4.1",
"log",
"polling 3.2.0",
"rustix 0.38.20",
"rustix 0.38.21",
"slab",
"thiserror",
]
@ -636,7 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02"
dependencies = [
"calloop 0.12.3",
"rustix 0.38.20",
"rustix 0.38.21",
"wayland-backend 0.3.2",
"wayland-client 0.31.1",
]
@ -839,7 +839,7 @@ dependencies = [
[[package]]
name = "cosmic-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"atomicwrites",
"cosmic-config-derive",
@ -853,7 +853,7 @@ dependencies = [
[[package]]
name = "cosmic-config-derive"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"quote",
"syn 1.0.109",
@ -895,7 +895,7 @@ dependencies = [
[[package]]
name = "cosmic-text"
version = "0.10.0"
source = "git+https://github.com/pop-os/cosmic-text?branch=vi-editor#c1e40363ab576c90edb7b78f1f257b3845558b1a"
source = "git+https://github.com/pop-os/cosmic-text?branch=vi-editor#423fc2243930645036ff053e384a3ce64e70255e"
dependencies = [
"fontdb 0.15.0",
"libm",
@ -916,7 +916,7 @@ dependencies = [
[[package]]
name = "cosmic-theme"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"almost",
"cosmic-config",
@ -929,9 +929,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
dependencies = [
"libc",
]
@ -1638,9 +1638,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
dependencies = [
"futures-channel",
"futures-core",
@ -1653,9 +1653,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
dependencies = [
"futures-core",
"futures-sink",
@ -1663,15 +1663,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
[[package]]
name = "futures-executor"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
dependencies = [
"futures-core",
"futures-task",
@ -1681,9 +1681,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
[[package]]
name = "futures-lite"
@ -1702,9 +1702,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [
"proc-macro2",
"quote",
@ -1713,21 +1713,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
[[package]]
name = "futures-task"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
[[package]]
name = "futures-util"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
dependencies = [
"futures-channel",
"futures-core",
@ -1986,7 +1986,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.6",
"ahash 0.7.7",
]
[[package]]
@ -1995,7 +1995,7 @@ version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash 0.8.4",
"ahash 0.8.6",
"allocator-api2",
]
@ -2047,7 +2047,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iced"
version = "0.10.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_accessibility",
"iced_core",
@ -2063,7 +2063,7 @@ dependencies = [
[[package]]
name = "iced_accessibility"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"accesskit",
"accesskit_unix",
@ -2073,7 +2073,7 @@ dependencies = [
[[package]]
name = "iced_core"
version = "0.10.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"bitflags 1.3.2",
"iced_accessibility",
@ -2088,7 +2088,7 @@ dependencies = [
[[package]]
name = "iced_futures"
version = "0.7.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"futures",
"iced_core",
@ -2100,7 +2100,7 @@ dependencies = [
[[package]]
name = "iced_graphics"
version = "0.9.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"bitflags 1.3.2",
"bytemuck",
@ -2118,7 +2118,7 @@ dependencies = [
[[package]]
name = "iced_renderer"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_graphics",
"iced_tiny_skia",
@ -2131,7 +2131,7 @@ dependencies = [
[[package]]
name = "iced_runtime"
version = "0.1.1"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_accessibility",
"iced_core",
@ -2143,7 +2143,7 @@ dependencies = [
[[package]]
name = "iced_sctk"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"enum-repr",
"float-cmp",
@ -2167,7 +2167,7 @@ dependencies = [
[[package]]
name = "iced_style"
version = "0.9.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_core",
"once_cell",
@ -2177,7 +2177,7 @@ dependencies = [
[[package]]
name = "iced_tiny_skia"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"bytemuck",
"cosmic-text 0.9.0",
@ -2195,7 +2195,7 @@ dependencies = [
[[package]]
name = "iced_wgpu"
version = "0.11.1"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"bitflags 1.3.2",
"bytemuck",
@ -2217,7 +2217,7 @@ dependencies = [
[[package]]
name = "iced_widget"
version = "0.1.3"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_renderer",
"iced_runtime",
@ -2232,7 +2232,7 @@ dependencies = [
[[package]]
name = "iced_winit"
version = "0.10.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"iced_graphics",
"iced_runtime",
@ -2357,7 +2357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.20",
"rustix 0.38.21",
"windows-sys 0.48.0",
]
@ -2479,7 +2479,7 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libcosmic"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#a2bda491520093c3343018204bfc8c4169b5e3b9"
source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c"
dependencies = [
"apply",
"ashpd",
@ -3483,7 +3483,7 @@ dependencies = [
"cfg-if",
"concurrent-queue",
"pin-project-lite",
"rustix 0.38.20",
"rustix 0.38.21",
"tracing",
"windows-sys 0.48.0",
]
@ -3841,9 +3841,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.37.26"
version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995"
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags 1.3.2",
"errno",
@ -3855,9 +3855,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.20"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.1",
"errno",
@ -3991,18 +3991,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
[[package]]
name = "serde"
version = "1.0.189"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.189"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
@ -4375,14 +4375,14 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
[[package]]
name = "tempfile"
version = "3.8.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"cfg-if",
"fastrand 2.0.1",
"redox_syscall 0.3.5",
"rustix 0.38.20",
"redox_syscall 0.4.1",
"rustix 0.38.21",
"windows-sys 0.48.0",
]
@ -4548,21 +4548,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.3"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b150d2f463da7b52f12110d3995dc86598bf90d535e929e5f5af15ab89155011"
checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.20.3",
"toml_edit 0.20.7",
]
[[package]]
name = "toml_datetime"
version = "0.6.4"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51cc078118ed25af325985ff674c00c8416b0f962be67da4946854ebfc99f334"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
@ -4580,9 +4580,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.20.3"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a2534c1aa199edef7108fb7d970facaa17f8f8cc5ce6bde75372cfa1051ed91"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap 2.0.2",
"serde",
@ -5854,18 +5854,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
[[package]]
name = "zerocopy"
version = "0.7.11"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c19fae0c8a9efc6a8281f2e623db8af1db9e57852e04cde3e754dd2dc29340f"
checksum = "a1808c6e5aa42cb2b67578276190258f08679c275d92a19735aad5d359e1b154"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.11"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc56589e9ddd1f1c28d4b4b5c773ce232910a6bb67a70133d61c9e347585efe9"
checksum = "ea597733532c780ce3e110401fc3b359b984cc78e17ac0917d3f371ffd1c1618"
dependencies = [
"proc-macro2",
"quote",

View file

@ -20,7 +20,9 @@ features = ["syntect", "vi"]
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
branch = "menu"
default-features = false
#path = "../libcosmic"
[features]
default = ["wayland"]

5
debian/changelog vendored Normal file
View file

@ -0,0 +1,5 @@
cosmic-edit (0.1.0) jammy; urgency=medium
* Initial release.
-- Jeremy Soller <jeremy@system76.com> Tue, 03 Oct 2023 07:37:28 -0600

19
debian/control vendored Normal file
View file

@ -0,0 +1,19 @@
Source: cosmic-edit
Section: admin
Priority: optional
Maintainer: Jeremy Soller <jeremy@system76.com>
Build-Depends:
debhelper-compat (=13),
just (>= 1.13.0),
libclang-dev,
libwayland-dev,
libxkbcommon-dev,
pkg-config,
rust-all,
Standards-Version: 4.6.2
Homepage: https://github.com/pop-os/cosmic-edit
Package: cosmic-edit
Architecture: amd64 arm64
Depends: ${misc:Depends}, ${shlibs:Depends}
Description: Cosmic Text Editor

7
debian/copyright vendored Normal file
View file

@ -0,0 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cosmic-edit
Upstream-Contact: Jeremy Soller <jeremy@system76.com>
Source: https://github.com/pop-os/cosmic-edit
Files: *
Copyright: System76 <info@system76.com>
License: GPL-3.0

22
debian/rules vendored Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/make -f
export DESTDIR = debian/cosmic-edit
export VENDOR ?= 1
%:
dh $@
override_dh_auto_clean:
if ! ischroot && test "${VENDOR}" = "1"; then \
mkdir -p .cargo; \
cargo vendor | head -n -1 > .cargo/config; \
echo 'directory = "vendor"' >> .cargo/config; \
tar pcf vendor.tar vendor; \
rm -rf vendor; \
fi
override_dh_auto_build:
just build-vendored
override_dh_auto_install:
just rootdir=$(DESTDIR) install

1
debian/source/format vendored Normal file
View file

@ -0,0 +1 @@
3.0 (native)

4
debian/source/options vendored Normal file
View file

@ -0,0 +1,4 @@
tar-ignore=.github
tar-ignore=.vscode
tar-ignore=vendor
tar-ignore=target

66
justfile Normal file
View file

@ -0,0 +1,66 @@
name := 'cosmic-edit'
export APPID := 'com.system76.CosmicEdit'
rootdir := ''
prefix := '/usr'
base-dir := absolute_path(clean(rootdir / prefix))
export INSTALL_DIR := base-dir / 'share'
bin-src := 'target' / 'release' / name
bin-dst := base-dir / 'bin' / name
# Default recipe which runs `just build-release`
default: build-release
# Runs `cargo clean`
clean:
cargo clean
# `cargo clean` and removes vendored dependencies
clean-dist: clean
rm -rf .cargo vendor vendor.tar
# Compiles with debug profile
build-debug *args:
cargo build {{args}}
# Compiles with release profile
build-release *args: (build-debug '--release' args)
# Compiles release profile with vendored dependencies
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Runs a clippy check
check *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Runs a clippy check with JSON message format
check-json: (check '--message-format=json')
# Run with debug logs
run *args:
env RUST_LOG=debug RUST_BACKTRACE=full cargo run --release {{args}}
# Installs files
install:
install -Dm0755 {{bin-src}} {{bin-dst}}
# Uninstalls installed files
uninstall:
rm {{bin-dst}}
# Vendor dependencies locally
vendor:
mkdir -p .cargo
cargo vendor --sync Cargo.toml \
| head -n -1 > .cargo/config
echo 'directory = "vendor"' >> .cargo/config
tar pcf vendor.tar vendor
rm -rf vendor
# Extracts vendored dependencies
vendor-extract:
rm -rf vendor
tar pxf vendor.tar

View file

@ -1,40 +1,41 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
app::{Command, Core, Settings},
app::{message, Command, Core, Settings},
executor,
iced::{
widget::{column, row, text},
widget::{row, text},
Alignment, Length, Limits,
},
widget::{self, icon, segmented_button, view_switcher},
style,
widget::{self, button, icon, nav_bar, segmented_button, view_switcher},
ApplicationExt, Element,
};
use cosmic_text::{
Attrs, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, ViEditor, ViMode,
use cosmic_text::{FontSystem, SyntaxSystem, ViMode};
use std::{
env, fs,
path::{Path, PathBuf},
sync::Mutex,
};
use std::{env, fs, path::PathBuf, sync::Mutex};
use self::menu_list::MenuList;
mod menu_list;
use self::menu::menu_bar;
mod menu;
use self::project::ProjectNode;
mod project;
use self::tab::Tab;
mod tab;
use self::text_box::text_box;
mod text_box;
//TODO: re-use iced FONT_SYSTEM
lazy_static::lazy_static! {
static ref FONT_SYSTEM: Mutex<FontSystem> = Mutex::new(FontSystem::new());
static ref SYNTAX_SYSTEM: SyntaxSystem = SyntaxSystem::new();
}
static FONT_SIZES: &'static [Metrics] = &[
Metrics::new(10.0, 14.0), // Caption
Metrics::new(14.0, 20.0), // Body
Metrics::new(20.0, 28.0), // Title 4
Metrics::new(24.0, 32.0), // Title 3
Metrics::new(28.0, 36.0), // Title 2
Metrics::new(32.0, 44.0), // Title 1
];
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
@ -45,99 +46,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
pub struct Tab {
path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
editor: Mutex<ViEditor<'static>>,
#[derive(Clone, Debug)]
pub struct Config {
wrap: bool,
}
impl Tab {
impl Config {
//TODO: load from cosmic-config
pub fn new() -> Self {
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
let editor = SyntaxEditor::new(
Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), FONT_SIZES[1 /* Body */]),
&SYNTAX_SYSTEM,
"base16-eighties.dark",
)
.unwrap();
let mut editor = ViEditor::new(editor);
editor.set_passthrough(false);
Self {
path_opt: None,
attrs,
editor: Mutex::new(editor),
}
}
pub fn open(&mut self, path: PathBuf) {
let mut editor = self.editor.lock().unwrap();
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
match editor.load_text(&path, self.attrs) {
Ok(()) => {
log::info!("opened '{}'", path.display());
self.path_opt = Some(path);
}
Err(err) => {
log::error!("failed to open '{}': {}", path.display(), err);
self.path_opt = None;
}
}
}
pub fn save(&mut self) {
if let Some(path) = &self.path_opt {
let editor = self.editor.lock().unwrap();
let mut text = String::new();
for line in editor.buffer().lines.iter() {
text.push_str(line.text());
text.push('\n');
}
match fs::write(path, text) {
Ok(()) => {
log::info!("saved '{}'", path.display());
}
Err(err) => {
log::error!("failed to save '{}': {}", path.display(), err);
}
}
} else {
log::warn!("tab has no path yet");
}
}
pub fn title(&self) -> String {
//TODO: show full title when there is a conflict
if let Some(path) = &self.path_opt {
match path.file_name() {
Some(file_name_os) => match file_name_os.to_str() {
Some(file_name) => file_name.to_string(),
None => format!("{}", path.display()),
},
None => format!("{}", path.display()),
}
} else {
"New document".to_string()
}
Self { wrap: false }
}
}
pub struct App {
core: Core,
nav_model: segmented_button::SingleSelectModel,
tab_model: segmented_button::SingleSelectModel,
config: Config,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub enum Message {
Open,
New,
OpenFileDialog,
OpenFile(PathBuf),
OpenProjectDialog,
OpenProject(PathBuf),
Save,
TabActivate(segmented_button::Entity),
TabClose(segmented_button::Entity),
Todo,
Wrap(bool),
}
impl App {
@ -149,25 +89,168 @@ impl App {
self.tab_model.active_data_mut()
}
fn open_folder<P: AsRef<Path>>(&mut self, path: P, mut position: u16, indent: u16) {
let read_dir = match fs::read_dir(&path) {
Ok(ok) => ok,
Err(err) => {
log::error!("failed to read directory {:?}: {}", path.as_ref(), err);
return;
}
};
let mut nodes = Vec::new();
for entry_res in read_dir {
let entry = match entry_res {
Ok(ok) => ok,
Err(err) => {
log::error!(
"failed to read entry in directory {:?}: {}",
path.as_ref(),
err
);
continue;
}
};
let entry_path = entry.path();
let node = match ProjectNode::new(&entry_path) {
Ok(ok) => ok,
Err(err) => {
log::error!(
"failed to open directory {:?} entry {:?}: {}",
path.as_ref(),
entry_path,
err
);
continue;
}
};
nodes.push(node);
}
nodes.sort();
for node in nodes {
self.nav_model
.insert()
.position(position)
.indent(indent)
.icon(icon::from_name(node.icon_name()).size(16).icon())
.text(node.name().to_string())
.data(node);
position += 1;
}
}
pub fn open_project<P: AsRef<Path>>(&mut self, path: P) {
let node = match ProjectNode::new(&path) {
Ok(mut node) => {
match &mut node {
ProjectNode::Folder { open, root, .. } => {
*open = true;
*root = true;
}
_ => {
log::error!(
"failed to open project {:?}: not a directory",
path.as_ref()
);
return;
}
}
node
}
Err(err) => {
log::error!("failed to open project {:?}: {}", path.as_ref(), err);
return;
}
};
let id = self
.nav_model
.insert()
.icon(icon::from_name(node.icon_name()).size(16).icon())
.text(node.name().to_string())
.data(node)
.id();
let position = self.nav_model.position(id).unwrap_or(0);
self.open_folder(&path, position + 1, 1);
}
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) {
let mut tab = Tab::new();
tab.set_config(&self.config);
if let Some(path) = path_opt {
tab.open(path);
}
self.tab_model
.insert()
.text(tab.title())
.icon(icon::from_name("text-x-generic").icon())
.data(tab)
.icon(icon::from_name("text-x-generic").size(16).icon())
.data::<Tab>(tab)
.closable()
.activate();
}
fn update_nav_bar_active(&mut self) {
let tab_path_opt = match self.active_tab() {
Some(tab) => tab.path_opt.clone(),
None => None,
};
// Locate tree node to activate
let mut active_id = segmented_button::Entity::default();
match tab_path_opt {
Some(tab_path) => {
// Automatically expand tree to find and select active file
loop {
let mut expand_opt = None;
for id in self.nav_model.iter() {
match self.nav_model.data(id) {
Some(node) => match node {
ProjectNode::Folder { path, open, .. } => {
if tab_path.starts_with(path) && !*open {
expand_opt = Some(id);
break;
}
}
ProjectNode::File { path, .. } => {
if path == &tab_path {
active_id = id;
break;
}
}
},
None => {}
}
}
match expand_opt {
Some(id) => {
//TODO: can this be optimized?
cosmic::Application::on_nav_select(self, id);
}
None => {
break;
}
}
}
}
None => {}
}
self.nav_model.activate(active_id);
}
pub fn update_title(&mut self) -> Command<Message> {
self.update_nav_bar_active();
let title = match self.active_tab() {
Some(tab) => tab.title(),
None => format!("No Open File"),
};
let window_title = format!("{title} - COSMIC Text Editor");
self.set_header_title(title.clone());
self.set_window_title(window_title)
@ -186,7 +269,7 @@ impl cosmic::Application for App {
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "com.system76.CosmicTextEditor";
const APP_ID: &'static str = "com.system76.CosmicEdit";
fn core(&self) -> &Core {
&self.core
@ -200,11 +283,27 @@ impl cosmic::Application for App {
fn init(core: Core, _flags: Self::Flags) -> (Self, Command<Self::Message>) {
let mut app = App {
core,
nav_model: nav_bar::Model::builder().build(),
tab_model: segmented_button::Model::builder().build(),
config: Config::new(),
};
for path in env::args().skip(1) {
app.open_tab(Some(PathBuf::from(path)));
for arg in env::args().skip(1) {
let path = PathBuf::from(arg);
if path.is_dir() {
app.open_project(path);
} else {
app.open_tab(Some(path));
}
}
// Show nav bar only if project is provided
if app.core.nav_bar_active() != app.nav_model.iter().next().is_some() {
app.core.nav_bar_toggle();
app.nav_model
.insert()
.icon(icon::from_name("folder-open-symbolic").size(16).icon())
.text("Open project");
}
// Open an empty file if no arguments provided
@ -216,13 +315,104 @@ impl cosmic::Application for App {
(app, command)
}
fn update(&mut self, message: Message) -> Command<Self::Message> {
match message {
Message::Open => {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.open_tab(Some(path));
return self.update_title();
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav_model)
}
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
// Toggle open state and get clone of node data
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
Some(node) => {
match node {
ProjectNode::Folder { open, .. } => {
*open = !*open;
}
_ => {}
}
Some(node.clone())
}
None => None,
};
match node_opt {
Some(node) => {
// Update icon
self.nav_model
.icon_set(id, icon::from_name(node.icon_name()).size(16).icon());
match node {
ProjectNode::Folder { path, open, .. } => {
let position = self.nav_model.position(id).unwrap_or(0);
let indent = self.nav_model.indent(id).unwrap_or(0);
if open {
// Open folder
self.open_folder(path, position + 1, indent + 1);
} else {
// Close folder
loop {
let child_id = match self.nav_model.entity_at(position + 1) {
Some(some) => some,
None => break,
};
if self.nav_model.indent(child_id).unwrap_or(0) > indent {
self.nav_model.remove(child_id);
} else {
break;
}
}
}
Command::none()
}
ProjectNode::File { path, .. } => {
//TODO: go to already open file if possible
self.update(Message::OpenFile(path))
}
}
}
None => {
// Open project
self.update(Message::OpenProjectDialog)
}
}
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::New => {
self.open_tab(None);
return self.update_title();
}
Message::OpenFileDialog => {
return Command::perform(
async {
if let Some(handle) = rfd::AsyncFileDialog::new().pick_file().await {
message::app(Message::OpenFile(handle.path().to_owned()))
} else {
message::none()
}
},
|x| x,
);
}
Message::OpenFile(path) => {
self.open_tab(Some(path));
return self.update_title();
}
Message::OpenProjectDialog => {
return Command::perform(
async {
if let Some(handle) = rfd::AsyncFileDialog::new().pick_folder().await {
message::app(Message::OpenProject(handle.path().to_owned()))
} else {
message::none()
}
},
|x| x,
);
}
Message::OpenProject(path) => {
self.open_project(path);
}
Message::Save => {
let mut title_opt = None;
@ -230,6 +420,7 @@ impl cosmic::Application for App {
match self.active_tab_mut() {
Some(tab) => {
if tab.path_opt.is_none() {
//TODO: use async file dialog
tab.path_opt = rfd::FileDialog::new().save_file();
title_opt = Some(tab.title());
}
@ -271,37 +462,40 @@ impl cosmic::Application for App {
Message::Todo => {
log::warn!("TODO");
}
Message::Wrap(wrap) => {
self.config.wrap = wrap;
//TODO: provide iterator over data
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
tab.set_config(&self.config);
}
}
}
}
Command::none()
}
fn view(&self) -> Element<Message> {
let menu_bar = row![
MenuList::new(vec!["Open", "Save"], None, |item| {
match item {
"Open" => Message::Open,
"Save" => Message::Save,
_ => Message::Todo,
}
})
.padding(8)
.placeholder("File"),
MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("Edit"),
MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("View"),
MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("Help"),
]
.align_items(Alignment::Start)
.padding(4)
.spacing(16);
fn header_start(&self) -> Vec<Element<Message>> {
vec![menu_bar(&self.config)]
}
fn view(&self) -> Element<Message> {
let mut tab_column = widget::column::with_capacity(3).padding([0, 16]);
tab_column = tab_column.push(
view_switcher::horizontal(&self.tab_model)
.on_activate(Message::TabActivate)
.on_close(Message::TabClose)
.width(Length::Shrink),
row![
view_switcher::horizontal(&self.tab_model)
.on_activate(Message::TabActivate)
.on_close(Message::TabClose)
.width(Length::Shrink),
button(icon::from_name("list-add-symbolic").size(16).icon())
.on_press(Message::New)
.padding(8)
.style(style::Button::Icon)
]
.align_items(Alignment::Center),
);
match self.active_tab() {
@ -337,10 +531,10 @@ impl cosmic::Application for App {
}
};
let content: Element<_> = column![menu_bar, tab_column].into();
let content: Element<_> = tab_column.into();
// Uncomment to debug layout:
//content.explain(Color::WHITE)
//content.explain(cosmic::iced::Color::WHITE)
content
}
}

124
src/menu.rs Normal file
View file

@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
//TODO: export in cosmic::widget
iced::{widget::horizontal_rule, Alignment, Length},
theme,
widget::{
self, horizontal_space,
menu::{ItemHeight, ItemWidth, MenuBar, MenuTree},
},
Element,
};
use crate::{Config, Message};
pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
//TODO: port to libcosmic
let menu_root = |label| {
widget::button(label)
.padding([4, 12])
.style(theme::Button::MenuRoot)
};
macro_rules! menu_button {
($($x:expr),+ $(,)?) => (
widget::button(
widget::Row::with_children(
vec![$(Element::from($x)),+]
)
.align_items(Alignment::Center)
)
.height(Length::Fixed(32.0))
.padding([4, 16])
.width(Length::Fill)
.style(theme::Button::MenuItem)
);
}
let menu_folder = |label| menu_button!(label, horizontal_space(Length::Fill), ">");
let menu_item = |label, message| MenuTree::new(menu_button!(label).on_press(message));
let menu_key = |label, key, message| {
MenuTree::new(menu_button!(label, horizontal_space(Length::Fill), key).on_press(message))
};
MenuBar::new(vec![
MenuTree::with_children(
menu_root("File"),
vec![
menu_key("New file", "Ctrl + N", Message::New),
menu_key("New window", "Ctrl + Shift + N", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_key("Open file...", "Ctrl + O", Message::OpenFileDialog),
MenuTree::with_children(
menu_folder("Open recent"),
vec![menu_item("TODO", Message::Todo)],
),
MenuTree::new(horizontal_rule(1)),
menu_key("Save", "Ctrl + S", Message::Save),
menu_key("Save as...", "Ctrl + Shift + S", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Revert all changes", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Document statistics...", Message::Todo),
menu_item("Document type...", Message::Todo),
menu_item("Encoding...", Message::Todo),
menu_item("Print", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_key("Quit", "Ctrl + Q", Message::Todo),
],
),
MenuTree::with_children(
menu_root("Edit"),
vec![
menu_key("Undo", "Ctrl + Z", Message::Todo),
menu_key("Redo", "Ctrl + Shift + Z", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_key("Cut", "Ctrl + X", Message::Todo),
menu_key("Copy", "Ctrl + C", Message::Todo),
menu_key("Paste", "Ctrl + V", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_key("Find", "Ctrl + F", Message::Todo),
menu_key("Replace", "Ctrl + H", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Spell check...", Message::Todo),
],
),
MenuTree::with_children(
menu_root("View"),
vec![
MenuTree::with_children(
menu_folder("Indentation"),
vec![
menu_item("Automatic indentation", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Tab width: 1", Message::Todo),
menu_item("Tab width: 2", Message::Todo),
menu_item("Tab width: 4", Message::Todo),
menu_item("Tab width: 8", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Convert indentation to spaces", Message::Todo),
menu_item("Convert indentation to tabs", Message::Todo),
],
),
MenuTree::new(horizontal_rule(1)),
menu_item("Word wrap", Message::Todo),
menu_item("Show line numbers", Message::Todo),
menu_item("Highlight current line", Message::Todo),
menu_item("Syntax highlighting...", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_key("Settings...", "Ctrl + ,", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("Keyboard shortcuts...", Message::Todo),
MenuTree::new(horizontal_rule(1)),
menu_item("About COSMIC Text Editor", Message::Todo),
],
),
])
.item_height(ItemHeight::Dynamic(40))
.item_width(ItemWidth::Uniform(240))
.spacing(4.0)
.into()
}

View file

@ -1,622 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
iced::{
alignment,
event::{self, Event},
keyboard, mouse, overlay,
overlay::menu::{self, Menu},
touch,
widget::container,
widget::scrollable,
Background, Color, Element, Length, Padding, Rectangle, Size,
},
iced_core::{
clipboard::Clipboard,
layout::{self, Layout},
renderer,
text::{self, LineHeight, Shaping, Text},
widget::tree::{self, Tree},
Shell, Widget,
},
};
use std::borrow::Cow;
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
pub placeholder_color: Color,
/// The [`Background`] of the pick list.
pub background: Background,
/// The border radius of the pick list.
pub border_radius: f32,
/// The border width of the pick list.
pub border_width: f32,
/// The border color of the pick list.
pub border_color: Color,
}
/// A set of rules that dictate the style of a container.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default + Clone;
/// Produces the active [`Appearance`] of a pick list.
fn active(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
/// Produces the hovered [`Appearance`] of a pick list.
fn hovered(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
}
impl StyleSheet for cosmic::theme::Theme {
type Style = ();
fn active(&self, _style: &()) -> Appearance {
let cosmic = &self.cosmic().primary.component;
Appearance {
text_color: cosmic.on.into(),
background: Color::TRANSPARENT.into(),
placeholder_color: cosmic.on.into(),
border_radius: 8.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
fn hovered(&self, style: &()) -> Appearance {
let cosmic = &self.cosmic().primary.component;
Appearance {
background: Background::Color(cosmic.hover.into()),
..self.active(style)
}
}
}
/// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)]
pub struct MenuList<'a, T, Message, Renderer>
where
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
on_selected: Box<dyn Fn(T) -> Message + 'a>,
options: Cow<'a, [T]>,
placeholder: Option<String>,
selected: Option<T>,
width: Length,
padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T: 'a, Message, Renderer> MenuList<'a, T, Message, Renderer>
where
T: ToString + Eq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
<Renderer as text::Renderer>::Font: std::default::Default,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
/// The default padding of a [`MenuList`].
pub const DEFAULT_PADDING: Padding = Padding::new(8.0);
/// Creates a new [`MenuList`] with the given list of options, the current
/// selected value, and the message to produce when an option is selected.
pub fn new(
options: impl Into<Cow<'a, [T]>>,
selected: Option<T>,
on_selected: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
on_selected: Box::new(on_selected),
options: options.into(),
placeholder: None,
selected,
width: Length::Shrink,
text_size: None,
padding: Self::DEFAULT_PADDING,
font: Default::default(),
style: Default::default(),
}
}
/// Sets the placeholder of the [`MenuList`].
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
/// Sets the width of the [`MenuList`].
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the [`Padding`] of the [`MenuList`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the text size of the [`MenuList`].
pub fn text_size(mut self, size: u16) -> Self {
self.text_size = Some(size);
self
}
/// Sets the font of the [`MenuList`].
pub fn font(mut self, font: Renderer::Font) -> Self {
self.font = font;
self
}
/// Sets the style of the [`MenuList`].
pub fn style(mut self, style: impl Into<<Renderer::Theme as StyleSheet>::Style>) -> Self {
self.style = style.into();
self
}
}
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for MenuList<'a, T, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<T>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<T>::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.padding,
self.text_size,
&self.font,
self.placeholder.as_deref(),
&self.options,
)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle<f32>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
|| tree.state.downcast_mut::<State<T>>(),
)
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor_position)
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
renderer,
theme,
layout,
cursor_position,
self.padding,
self.text_size,
&self.font,
self.placeholder.as_deref(),
self.selected.as_ref(),
&self.style,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_mut::<State<T>>();
overlay(
renderer,
layout,
state,
self.padding,
self.text_size,
self.font.clone(),
&self.options,
&self.on_selected,
self.style.clone(),
)
}
}
impl<'a, T: 'a, Message, Renderer> From<MenuList<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
fn from(menu_list: MenuList<'a, T, Message, Renderer>) -> Self {
Self::new(menu_list)
}
}
/// The local state of a [`MenuList`].
#[derive(Debug)]
pub struct State<T> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
last_selection: Option<T>,
}
impl<T> State<T> {
/// Creates a new [`State`] for a [`MenuList`].
pub fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
last_selection: Option::default(),
}
}
}
impl<T> Default for State<T> {
fn default() -> Self {
Self::new()
}
}
/// Computes the layout of a [`MenuList`].
pub fn layout<Renderer, T>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
text_size: Option<u16>,
font: &Renderer::Font,
placeholder: Option<&str>,
options: &[T],
) -> layout::Node
where
Renderer: text::Renderer,
T: ToString,
{
use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding);
let text_size = text_size.unwrap_or_else(|| renderer.default_size() as u16);
let max_width = match width {
Length::Shrink => {
let measure = |label: &str| -> u32 {
let size = renderer.measure(
label,
text_size as f32,
LineHeight::default(),
font.clone(),
Size::new(f32::INFINITY, f32::INFINITY),
Shaping::Advanced,
);
size.width.round() as u32
};
placeholder.map(measure).unwrap_or(100)
}
_ => 0,
};
let size = {
let intrinsic = Size::new(max_width as f32, f32::from(text_size));
limits.resolve(intrinsic).pad(padding)
};
layout::Node::new(size)
}
/// Processes an [`Event`] and updates the [`State`] of a [`MenuList`]
/// accordingly.
pub fn update<'a, T, Message>(
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
state: impl FnOnce() -> &'a mut State<T>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
let event_status = if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside it's
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
event::Status::Captured
} else if cursor_position.is_over(layout.bounds()) {
state.is_open = true;
state.hovered_option = options.iter().position(|option| Some(option) == selected);
event::Status::Captured
} else {
event::Status::Ignored
};
if let Some(last_selection) = state.last_selection.take() {
shell.publish((on_selected)(last_selection));
state.is_open = false;
event::Status::Captured
} else {
event_status
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state = state();
if state.keyboard_modifiers.command()
&& cursor_position.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((on_selected)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = state();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
}
/// Returns the current [`mouse::Interaction`] of a [`MenuList`].
pub fn mouse_interaction(layout: Layout<'_>, cursor_position: mouse::Cursor) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor_position.is_over(bounds);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
/// Returns the current overlay of a [`MenuList`].
pub fn overlay<'a, T, Message, Renderer>(
renderer: &Renderer,
layout: Layout<'_>,
state: &'a mut State<T>,
padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
options: &'a [T],
on_selected: &'a dyn Fn(T) -> Message,
style: <Renderer::Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Renderer>>
where
T: Clone + ToString,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
if state.is_open {
let bounds = layout.bounds();
let text_size = text_size.unwrap_or_else(|| renderer.default_size() as u16);
let width = {
let measure = |label: &str| -> u32 {
let size = renderer.measure(
label,
text_size as f32,
LineHeight::default(),
font.clone(),
Size::new(f32::INFINITY, f32::INFINITY),
Shaping::Advanced,
);
size.width.round() as u32
};
let labels = options.iter().map(ToString::to_string);
let labels_width = labels.map(|label| measure(&label)).max().unwrap_or(100);
labels_width as f32 + padding.left + padding.right
};
let menu = Menu::new(
&mut state.menu,
options,
&mut state.hovered_option,
|option| {
state.is_open = false;
(on_selected)(option)
},
None,
)
.width(width)
.padding(padding)
.font(font)
.style(style)
.text_size(text_size);
Some(menu.overlay(layout.position(), bounds.height))
} else {
None
}
}
/// Draws a [`MenuList`].
pub fn draw<T, Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
padding: Padding,
text_size: Option<u16>,
font: &Renderer::Font,
placeholder: Option<&str>,
selected: Option<&T>,
style: &<Renderer::Theme as StyleSheet>::Style,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
T: ToString,
{
let bounds = layout.bounds();
let is_mouse_over = cursor_position.is_over(bounds);
let is_selected = selected.is_some();
let style = if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: style.border_color,
border_width: style.border_width,
border_radius: style.border_radius.into(),
},
style.background,
);
let label = selected.map(ToString::to_string);
if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.map_or_else(|| renderer.default_size(), f32::from);
renderer.fill_text(Text {
content: label,
bounds: Rectangle {
x: bounds.x + f32::from(padding.left),
y: bounds.center_y() - text_size / 2.0,
width: bounds.width - f32::from(padding.horizontal()),
height: text_size,
},
size: text_size,
line_height: LineHeight::default(),
color: if is_selected {
style.text_color
} else {
style.placeholder_color
},
font: font.clone(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: Shaping::Advanced,
});
}
}

90
src/project.rs Normal file
View file

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
fs, io,
path::{Path, PathBuf},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProjectNode {
Folder {
name: String,
path: PathBuf,
open: bool,
root: bool,
},
File {
name: String,
path: PathBuf,
},
}
impl ProjectNode {
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let path = fs::canonicalize(path)?;
let name = path
.file_name()
.ok_or(io::Error::new(
io::ErrorKind::Other,
format!("path {:?} has no file name", path),
))?
.to_str()
.ok_or(io::Error::new(
io::ErrorKind::Other,
format!("path {:?} is not valid UTF-8", path),
))?
.to_string();
Ok(if path.is_dir() {
Self::Folder {
path,
name,
open: false,
root: false,
}
} else {
Self::File { path, name }
})
}
pub fn icon_name(&self) -> &str {
match self {
//TODO: different icon for project root?
Self::Folder { open, .. } => {
if *open {
"go-down-symbolic"
} else {
"go-next-symbolic"
}
}
Self::File { .. } => "text-x-generic",
}
}
pub fn name(&self) -> &str {
match self {
Self::Folder { name, .. } => name,
Self::File { name, .. } => name,
}
}
}
impl Ord for ProjectNode {
fn cmp(&self, other: &Self) -> Ordering {
match self {
// Folders are always before files
Self::Folder { .. } => match other {
Self::File { .. } => return Ordering::Less,
_ => {}
},
_ => {}
}
self.name().cmp(other.name())
}
}
impl PartialOrd for ProjectNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

110
src/tab.rs Normal file
View file

@ -0,0 +1,110 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_text::{Attrs, Buffer, Edit, Metrics, SyntaxEditor, ViEditor, Wrap};
use std::{fs, path::PathBuf, sync::Mutex};
use crate::{Config, FONT_SYSTEM, SYNTAX_SYSTEM};
static FONT_SIZES: &'static [Metrics] = &[
Metrics::new(10.0, 14.0), // Caption
Metrics::new(14.0, 20.0), // Body
Metrics::new(20.0, 28.0), // Title 4
Metrics::new(24.0, 32.0), // Title 3
Metrics::new(28.0, 36.0), // Title 2
Metrics::new(32.0, 44.0), // Title 1
];
pub struct Tab {
pub path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
pub editor: Mutex<ViEditor<'static>>,
}
impl Tab {
pub fn new() -> Self {
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
let editor = SyntaxEditor::new(
Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), FONT_SIZES[1 /* Body */]),
&SYNTAX_SYSTEM,
"base16-eighties.dark",
)
.unwrap();
let mut editor = ViEditor::new(editor);
editor.set_passthrough(false);
Self {
path_opt: None,
attrs,
editor: Mutex::new(editor),
}
}
pub fn set_config(&mut self, config: &Config) {
let mut editor = self.editor.lock().unwrap();
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
editor
.buffer_mut()
.set_wrap(if config.wrap { Wrap::Word } else { Wrap::None });
}
pub fn open(&mut self, path: PathBuf) {
let mut editor = self.editor.lock().unwrap();
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
match editor.load_text(&path, self.attrs) {
Ok(()) => {
log::info!("opened {:?}", path);
self.path_opt = match fs::canonicalize(&path) {
Ok(ok) => Some(ok),
Err(err) => {
log::error!("failed to canonicalize {:?}: {}", path, err);
Some(path)
}
};
}
Err(err) => {
log::error!("failed to open {:?}: {}", path, err);
self.path_opt = None;
}
}
}
pub fn save(&mut self) {
if let Some(path) = &self.path_opt {
let editor = self.editor.lock().unwrap();
let mut text = String::new();
for line in editor.buffer().lines.iter() {
text.push_str(line.text());
text.push('\n');
}
match fs::write(path, text) {
Ok(()) => {
log::info!("saved {:?}", path);
}
Err(err) => {
log::error!("failed to save {:?}: {}", path, err);
}
}
} else {
log::warn!("tab has no path yet");
}
}
pub fn title(&self) -> String {
//TODO: show full title when there is a conflict
if let Some(path) = &self.path_opt {
match path.file_name() {
Some(file_name_os) => match file_name_os.to_str() {
Some(file_name) => file_name.to_string(),
None => format!("{}", path.display()),
},
None => format!("{}", path.display()),
}
} else {
"New document".to_string()
}
}
}