├── .cargo └── config.toml ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── analysis_options.yaml ├── build.rs ├── lib ├── main.dart ├── main_window.dart ├── pages │ ├── drag_drop.dart │ ├── file_open_dialog.dart │ ├── flutter_plugins.dart │ ├── menu.dart │ ├── modal_window.dart │ ├── other_window.dart │ ├── platform_channels.dart │ └── window_management.dart ├── util.dart └── widgets │ ├── animated_visibility.dart │ ├── button.dart │ ├── intrinsic_limited_box.dart │ ├── page.dart │ └── veil.dart ├── pubspec.lock ├── pubspec.yaml ├── resources └── mac_icon.icns └── src ├── file_open_dialog.rs ├── file_open_dialog_linux.rs ├── file_open_dialog_mac.rs ├── file_open_dialog_win.rs ├── main.rs └── platform_channels.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_os = "macos")'] 2 | rustflags = ["-C", "link-arg=-Wl,-rpath,@executable_path"] 3 | 4 | [target.'cfg(target_os = "linux")'] 5 | rustflags = ["-C", "link-arg=-Wl,-rpath,$ORIGIN/lib"] 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: knopp 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .dart_tool 3 | .cache 4 | .DS_Store 5 | .packages 6 | build 7 | .flutter-plugins 8 | .flutter-plugins-dependencies 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter: Attach to Process", 9 | "type": "dart", 10 | "request": "attach", 11 | "osx": { 12 | "deviceId": "macos", 13 | "serviceInfoFile": "${env:TMPDIR}vmservice.nativeshell_examples", 14 | "vmServiceInfoFile": "${env:TMPDIR}vmservice.nativeshell_examples" 15 | }, 16 | "windows": { 17 | "deviceId": "windows", 18 | "serviceInfoFile": "${env:TEMP}/vmservice.nativeshell_examples", 19 | "vmServiceInfoFile": "${env:TEMP}/vmservice.nativeshell_examples" 20 | }, 21 | "linux": { 22 | "deviceId": "linux", 23 | "serviceInfoFile": "${env:XDG_RUNTIME_DIR}/vmservice.nativeshell_examples", 24 | "vmServiceInfoFile": "${env:XDG_RUNTIME_DIR}/vmservice.nativeshell_examples" 25 | } 26 | }, 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.41" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.51" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "atk" 33 | version = "0.14.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba" 36 | dependencies = [ 37 | "atk-sys", 38 | "bitflags", 39 | "glib", 40 | "libc", 41 | ] 42 | 43 | [[package]] 44 | name = "atk-sys" 45 | version = "0.14.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" 48 | dependencies = [ 49 | "glib-sys", 50 | "gobject-sys", 51 | "libc", 52 | "system-deps", 53 | ] 54 | 55 | [[package]] 56 | name = "atty" 57 | version = "0.2.14" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 60 | dependencies = [ 61 | "hermit-abi", 62 | "libc", 63 | "winapi 0.3.9", 64 | ] 65 | 66 | [[package]] 67 | name = "autocfg" 68 | version = "1.0.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 71 | 72 | [[package]] 73 | name = "base64" 74 | version = "0.13.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 77 | 78 | [[package]] 79 | name = "bitflags" 80 | version = "1.2.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 83 | 84 | [[package]] 85 | name = "block" 86 | version = "0.1.6" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 89 | 90 | [[package]] 91 | name = "byte-slice-cast" 92 | version = "1.0.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "65c1bf4a04a88c54f589125563643d773f3254b5c38571395e2b591c693bbc81" 95 | 96 | [[package]] 97 | name = "cairo-rs" 98 | version = "0.14.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "8d32eecb1e806433cf68063c4548bbdc15cc56d35db19d685ab60909c4c85206" 101 | dependencies = [ 102 | "bitflags", 103 | "cairo-sys-rs", 104 | "glib", 105 | "libc", 106 | "thiserror", 107 | ] 108 | 109 | [[package]] 110 | name = "cairo-sys-rs" 111 | version = "0.14.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d7c9c3928781e8a017ece15eace05230f04b647457d170d2d9641c94a444ff80" 114 | dependencies = [ 115 | "glib-sys", 116 | "libc", 117 | "system-deps", 118 | ] 119 | 120 | [[package]] 121 | name = "cargo-emit" 122 | version = "0.2.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" 125 | 126 | [[package]] 127 | name = "cc" 128 | version = "1.0.67" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 131 | 132 | [[package]] 133 | name = "cfg-expr" 134 | version = "0.7.4" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "30aa9e2ffbb838c6b451db14f3cd8e63ed622bf859f9956bc93845a10fafc26a" 137 | dependencies = [ 138 | "smallvec", 139 | ] 140 | 141 | [[package]] 142 | name = "cfg-if" 143 | version = "1.0.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 146 | 147 | [[package]] 148 | name = "cmake" 149 | version = "0.1.45" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" 152 | dependencies = [ 153 | "cc", 154 | ] 155 | 156 | [[package]] 157 | name = "cocoa" 158 | version = "0.24.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" 161 | dependencies = [ 162 | "bitflags", 163 | "block", 164 | "cocoa-foundation", 165 | "core-foundation", 166 | "core-graphics", 167 | "foreign-types", 168 | "libc", 169 | "objc", 170 | ] 171 | 172 | [[package]] 173 | name = "cocoa-foundation" 174 | version = "0.1.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" 177 | dependencies = [ 178 | "bitflags", 179 | "block", 180 | "core-foundation", 181 | "core-graphics-types", 182 | "foreign-types", 183 | "libc", 184 | "objc", 185 | ] 186 | 187 | [[package]] 188 | name = "const-cstr" 189 | version = "0.3.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" 192 | 193 | [[package]] 194 | name = "convert_case" 195 | version = "0.4.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 198 | 199 | [[package]] 200 | name = "copy_dir" 201 | version = "0.1.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "6e4281031634644843bd2f5aa9c48cf98fc48d6b083bd90bb11becf10deaf8b0" 204 | dependencies = [ 205 | "walkdir", 206 | ] 207 | 208 | [[package]] 209 | name = "core-foundation" 210 | version = "0.9.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" 213 | dependencies = [ 214 | "core-foundation-sys", 215 | "libc", 216 | ] 217 | 218 | [[package]] 219 | name = "core-foundation-sys" 220 | version = "0.8.2" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" 223 | 224 | [[package]] 225 | name = "core-graphics" 226 | version = "0.22.2" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "269f35f69b542b80e736a20a89a05215c0ce80c2c03c514abb2e318b78379d86" 229 | dependencies = [ 230 | "bitflags", 231 | "core-foundation", 232 | "core-graphics-types", 233 | "foreign-types", 234 | "libc", 235 | ] 236 | 237 | [[package]] 238 | name = "core-graphics-types" 239 | version = "0.1.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 242 | dependencies = [ 243 | "bitflags", 244 | "core-foundation", 245 | "foreign-types", 246 | "libc", 247 | ] 248 | 249 | [[package]] 250 | name = "detour" 251 | version = "0.8.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "6c901a56aa37ddfb686a69e671af52c22b978b7569065a2e0fde159ed83b2af4" 254 | dependencies = [ 255 | "cfg-if", 256 | "generic-array", 257 | "lazy_static", 258 | "libc", 259 | "libudis86-sys", 260 | "mmap-fixed", 261 | "region", 262 | "slice-pool", 263 | ] 264 | 265 | [[package]] 266 | name = "diff" 267 | version = "0.1.12" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" 270 | 271 | [[package]] 272 | name = "dispatch" 273 | version = "0.2.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" 276 | 277 | [[package]] 278 | name = "dunce" 279 | version = "1.0.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4" 282 | 283 | [[package]] 284 | name = "either" 285 | version = "1.6.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 288 | 289 | [[package]] 290 | name = "env_logger" 291 | version = "0.9.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 294 | dependencies = [ 295 | "atty", 296 | "humantime", 297 | "log", 298 | "regex", 299 | "termcolor", 300 | ] 301 | 302 | [[package]] 303 | name = "errno" 304 | version = "0.2.7" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe" 307 | dependencies = [ 308 | "errno-dragonfly", 309 | "libc", 310 | "winapi 0.3.9", 311 | ] 312 | 313 | [[package]] 314 | name = "errno-dragonfly" 315 | version = "0.1.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" 318 | dependencies = [ 319 | "gcc", 320 | "libc", 321 | ] 322 | 323 | [[package]] 324 | name = "exec" 325 | version = "0.3.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" 328 | dependencies = [ 329 | "errno", 330 | "libc", 331 | ] 332 | 333 | [[package]] 334 | name = "field-offset" 335 | version = "0.3.4" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" 338 | dependencies = [ 339 | "memoffset", 340 | "rustc_version", 341 | ] 342 | 343 | [[package]] 344 | name = "filetime" 345 | version = "0.2.14" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" 348 | dependencies = [ 349 | "cfg-if", 350 | "libc", 351 | "redox_syscall", 352 | "winapi 0.3.9", 353 | ] 354 | 355 | [[package]] 356 | name = "foreign-types" 357 | version = "0.3.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 360 | dependencies = [ 361 | "foreign-types-shared", 362 | ] 363 | 364 | [[package]] 365 | name = "foreign-types-shared" 366 | version = "0.1.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 369 | 370 | [[package]] 371 | name = "form_urlencoded" 372 | version = "1.0.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 375 | dependencies = [ 376 | "matches", 377 | "percent-encoding", 378 | ] 379 | 380 | [[package]] 381 | name = "futures" 382 | version = "0.3.17" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" 385 | dependencies = [ 386 | "futures-channel", 387 | "futures-core", 388 | "futures-executor", 389 | "futures-io", 390 | "futures-sink", 391 | "futures-task", 392 | "futures-util", 393 | ] 394 | 395 | [[package]] 396 | name = "futures-channel" 397 | version = "0.3.17" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" 400 | dependencies = [ 401 | "futures-core", 402 | "futures-sink", 403 | ] 404 | 405 | [[package]] 406 | name = "futures-core" 407 | version = "0.3.17" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" 410 | 411 | [[package]] 412 | name = "futures-executor" 413 | version = "0.3.17" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" 416 | dependencies = [ 417 | "futures-core", 418 | "futures-task", 419 | "futures-util", 420 | ] 421 | 422 | [[package]] 423 | name = "futures-io" 424 | version = "0.3.17" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" 427 | 428 | [[package]] 429 | name = "futures-macro" 430 | version = "0.3.17" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" 433 | dependencies = [ 434 | "autocfg", 435 | "proc-macro-hack", 436 | "proc-macro2", 437 | "quote", 438 | "syn", 439 | ] 440 | 441 | [[package]] 442 | name = "futures-sink" 443 | version = "0.3.17" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" 446 | 447 | [[package]] 448 | name = "futures-task" 449 | version = "0.3.17" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" 452 | 453 | [[package]] 454 | name = "futures-util" 455 | version = "0.3.17" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" 458 | dependencies = [ 459 | "autocfg", 460 | "futures-channel", 461 | "futures-core", 462 | "futures-io", 463 | "futures-macro", 464 | "futures-sink", 465 | "futures-task", 466 | "memchr", 467 | "pin-project-lite", 468 | "pin-utils", 469 | "proc-macro-hack", 470 | "proc-macro-nested", 471 | "slab", 472 | ] 473 | 474 | [[package]] 475 | name = "gcc" 476 | version = "0.3.55" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 479 | 480 | [[package]] 481 | name = "gdk" 482 | version = "0.14.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "679e22651cd15888e7acd01767950edca2ee9fcd6421fbf5b3c3b420d4e88bb0" 485 | dependencies = [ 486 | "bitflags", 487 | "cairo-rs", 488 | "gdk-pixbuf", 489 | "gdk-sys", 490 | "gio", 491 | "glib", 492 | "libc", 493 | "pango", 494 | ] 495 | 496 | [[package]] 497 | name = "gdk-pixbuf" 498 | version = "0.14.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "534192cb8f01daeb8fab2c8d4baa8f9aae5b7a39130525779f5c2608e235b10f" 501 | dependencies = [ 502 | "gdk-pixbuf-sys", 503 | "gio", 504 | "glib", 505 | "libc", 506 | ] 507 | 508 | [[package]] 509 | name = "gdk-pixbuf-sys" 510 | version = "0.14.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" 513 | dependencies = [ 514 | "gio-sys", 515 | "glib-sys", 516 | "gobject-sys", 517 | "libc", 518 | "system-deps", 519 | ] 520 | 521 | [[package]] 522 | name = "gdk-sys" 523 | version = "0.14.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" 526 | dependencies = [ 527 | "cairo-sys-rs", 528 | "gdk-pixbuf-sys", 529 | "gio-sys", 530 | "glib-sys", 531 | "gobject-sys", 532 | "libc", 533 | "pango-sys", 534 | "pkg-config", 535 | "system-deps", 536 | ] 537 | 538 | [[package]] 539 | name = "generic-array" 540 | version = "0.14.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 543 | dependencies = [ 544 | "typenum", 545 | "version_check", 546 | ] 547 | 548 | [[package]] 549 | name = "gio" 550 | version = "0.14.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "86c6823b39d46d22cac2466de261f28d7f049ebc18f7b35296a42c7ed8a88325" 553 | dependencies = [ 554 | "bitflags", 555 | "futures-channel", 556 | "futures-core", 557 | "futures-io", 558 | "gio-sys", 559 | "glib", 560 | "libc", 561 | "once_cell", 562 | "thiserror", 563 | ] 564 | 565 | [[package]] 566 | name = "gio-sys" 567 | version = "0.14.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" 570 | dependencies = [ 571 | "glib-sys", 572 | "gobject-sys", 573 | "libc", 574 | "system-deps", 575 | "winapi 0.3.9", 576 | ] 577 | 578 | [[package]] 579 | name = "glib" 580 | version = "0.14.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "f0028bbfd270d0778540294abca11141d59cb474da4c1f61ca1e11f579c49247" 583 | dependencies = [ 584 | "bitflags", 585 | "futures-channel", 586 | "futures-core", 587 | "futures-executor", 588 | "futures-task", 589 | "glib-macros", 590 | "glib-sys", 591 | "gobject-sys", 592 | "libc", 593 | "once_cell", 594 | "smallvec", 595 | ] 596 | 597 | [[package]] 598 | name = "glib-macros" 599 | version = "0.14.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "9eb7bdf41972a6f6dab5d72c23d22789f400059a43ba0d72b4bb2f8664d946a9" 602 | dependencies = [ 603 | "anyhow", 604 | "heck", 605 | "proc-macro-crate", 606 | "proc-macro-error", 607 | "proc-macro2", 608 | "quote", 609 | "syn", 610 | ] 611 | 612 | [[package]] 613 | name = "glib-sys" 614 | version = "0.14.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" 617 | dependencies = [ 618 | "libc", 619 | "system-deps", 620 | ] 621 | 622 | [[package]] 623 | name = "gobject-sys" 624 | version = "0.14.0" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" 627 | dependencies = [ 628 | "glib-sys", 629 | "libc", 630 | "system-deps", 631 | ] 632 | 633 | [[package]] 634 | name = "gtk" 635 | version = "0.14.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "10ae864e5eab8bc8b6b8544ed259eb02dd61b25323b20e777a77aa289c05fd0c" 638 | dependencies = [ 639 | "atk", 640 | "bitflags", 641 | "cairo-rs", 642 | "field-offset", 643 | "futures-channel", 644 | "gdk", 645 | "gdk-pixbuf", 646 | "gio", 647 | "glib", 648 | "gtk-sys", 649 | "gtk3-macros", 650 | "libc", 651 | "once_cell", 652 | "pango", 653 | "pkg-config", 654 | ] 655 | 656 | [[package]] 657 | name = "gtk-sys" 658 | version = "0.14.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" 661 | dependencies = [ 662 | "atk-sys", 663 | "cairo-sys-rs", 664 | "gdk-pixbuf-sys", 665 | "gdk-sys", 666 | "gio-sys", 667 | "glib-sys", 668 | "gobject-sys", 669 | "libc", 670 | "pango-sys", 671 | "system-deps", 672 | ] 673 | 674 | [[package]] 675 | name = "gtk3-macros" 676 | version = "0.14.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" 679 | dependencies = [ 680 | "anyhow", 681 | "heck", 682 | "proc-macro-crate", 683 | "proc-macro-error", 684 | "proc-macro2", 685 | "quote", 686 | "syn", 687 | ] 688 | 689 | [[package]] 690 | name = "heck" 691 | version = "0.3.2" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 694 | dependencies = [ 695 | "unicode-segmentation", 696 | ] 697 | 698 | [[package]] 699 | name = "hermit-abi" 700 | version = "0.1.18" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 703 | dependencies = [ 704 | "libc", 705 | ] 706 | 707 | [[package]] 708 | name = "humantime" 709 | version = "2.1.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 712 | 713 | [[package]] 714 | name = "idna" 715 | version = "0.2.3" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 718 | dependencies = [ 719 | "matches", 720 | "unicode-bidi", 721 | "unicode-normalization", 722 | ] 723 | 724 | [[package]] 725 | name = "itertools" 726 | version = "0.10.1" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" 729 | dependencies = [ 730 | "either", 731 | ] 732 | 733 | [[package]] 734 | name = "itoa" 735 | version = "0.4.7" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 738 | 739 | [[package]] 740 | name = "kernel32-sys" 741 | version = "0.2.2" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 744 | dependencies = [ 745 | "winapi 0.2.8", 746 | "winapi-build", 747 | ] 748 | 749 | [[package]] 750 | name = "lazy_static" 751 | version = "1.4.0" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 754 | 755 | [[package]] 756 | name = "libc" 757 | version = "0.2.94" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" 760 | 761 | [[package]] 762 | name = "libudis86-sys" 763 | version = "0.2.1" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "139bbf9ddb1bfc90c1ac64dd2923d9c957cd433cee7315c018125d72ab08a6b0" 766 | dependencies = [ 767 | "cc", 768 | "libc", 769 | ] 770 | 771 | [[package]] 772 | name = "linked-hash-map" 773 | version = "0.5.4" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 776 | 777 | [[package]] 778 | name = "log" 779 | version = "0.4.14" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 782 | dependencies = [ 783 | "cfg-if", 784 | ] 785 | 786 | [[package]] 787 | name = "mach" 788 | version = "0.3.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 791 | dependencies = [ 792 | "libc", 793 | ] 794 | 795 | [[package]] 796 | name = "malloc_buf" 797 | version = "0.0.6" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 800 | dependencies = [ 801 | "libc", 802 | ] 803 | 804 | [[package]] 805 | name = "matches" 806 | version = "0.1.8" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 809 | 810 | [[package]] 811 | name = "memchr" 812 | version = "2.4.0" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 815 | 816 | [[package]] 817 | name = "memoffset" 818 | version = "0.6.4" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" 821 | dependencies = [ 822 | "autocfg", 823 | ] 824 | 825 | [[package]] 826 | name = "mmap-fixed" 827 | version = "0.1.5" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "27c1ae264d6343d3b4079549f6bc9e6d074dc4106cb1324c7753c6ce11d07b21" 830 | dependencies = [ 831 | "kernel32-sys", 832 | "libc", 833 | "winapi 0.2.8", 834 | ] 835 | 836 | [[package]] 837 | name = "nativeshell" 838 | version = "0.1.16" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "8b718ebe83a2bf593f1ce0499fec1fb4037b47fceebb3dd34cd757201e0c7b14" 841 | dependencies = [ 842 | "anyhow", 843 | "async-trait", 844 | "base64", 845 | "block", 846 | "byte-slice-cast", 847 | "cairo-rs", 848 | "cargo-emit", 849 | "cc", 850 | "cocoa", 851 | "const-cstr", 852 | "core-foundation", 853 | "core-graphics", 854 | "detour", 855 | "diff", 856 | "dispatch", 857 | "exec", 858 | "futures", 859 | "gdk", 860 | "gdk-sys", 861 | "gio", 862 | "gio-sys", 863 | "glib", 864 | "glib-sys", 865 | "gobject-sys", 866 | "gtk", 867 | "gtk-sys", 868 | "libc", 869 | "log", 870 | "nativeshell_build", 871 | "objc", 872 | "once_cell", 873 | "percent-encoding", 874 | "process_path", 875 | "serde", 876 | "serde_bytes", 877 | "serde_json", 878 | "url", 879 | "utf16_lit", 880 | "velcro", 881 | "widestring", 882 | "windows", 883 | ] 884 | 885 | [[package]] 886 | name = "nativeshell_build" 887 | version = "0.1.16" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "0467f3145c92a425a940615ae4d1af716e5a47381061bec01fae28df2f62cd7d" 890 | dependencies = [ 891 | "base64", 892 | "cargo-emit", 893 | "cmake", 894 | "convert_case", 895 | "copy_dir", 896 | "dunce", 897 | "path-slash", 898 | "pathdiff", 899 | "serde", 900 | "serde_json", 901 | "tar", 902 | "yaml-rust", 903 | ] 904 | 905 | [[package]] 906 | name = "nativeshell_examples" 907 | version = "0.1.0" 908 | dependencies = [ 909 | "block", 910 | "cargo-emit", 911 | "cocoa", 912 | "env_logger", 913 | "gtk", 914 | "nativeshell", 915 | "nativeshell_build", 916 | "objc", 917 | "serde", 918 | "widestring", 919 | "windows", 920 | ] 921 | 922 | [[package]] 923 | name = "objc" 924 | version = "0.2.7" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 927 | dependencies = [ 928 | "malloc_buf", 929 | ] 930 | 931 | [[package]] 932 | name = "once_cell" 933 | version = "1.8.0" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 936 | 937 | [[package]] 938 | name = "pango" 939 | version = "0.14.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "415823a4fb9f1789785cd6e2d2413816f2ecff92380382969aaca9c400e13a19" 942 | dependencies = [ 943 | "bitflags", 944 | "glib", 945 | "libc", 946 | "once_cell", 947 | "pango-sys", 948 | ] 949 | 950 | [[package]] 951 | name = "pango-sys" 952 | version = "0.14.0" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" 955 | dependencies = [ 956 | "glib-sys", 957 | "gobject-sys", 958 | "libc", 959 | "system-deps", 960 | ] 961 | 962 | [[package]] 963 | name = "path-slash" 964 | version = "0.1.4" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619" 967 | 968 | [[package]] 969 | name = "pathdiff" 970 | version = "0.2.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" 973 | 974 | [[package]] 975 | name = "percent-encoding" 976 | version = "2.1.0" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 979 | 980 | [[package]] 981 | name = "pest" 982 | version = "2.1.3" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" 985 | dependencies = [ 986 | "ucd-trie", 987 | ] 988 | 989 | [[package]] 990 | name = "pin-project-lite" 991 | version = "0.2.6" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 994 | 995 | [[package]] 996 | name = "pin-utils" 997 | version = "0.1.0" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1000 | 1001 | [[package]] 1002 | name = "pkg-config" 1003 | version = "0.3.19" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 1006 | 1007 | [[package]] 1008 | name = "proc-macro-crate" 1009 | version = "1.0.0" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" 1012 | dependencies = [ 1013 | "thiserror", 1014 | "toml", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "proc-macro-error" 1019 | version = "1.0.4" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1022 | dependencies = [ 1023 | "proc-macro-error-attr", 1024 | "proc-macro2", 1025 | "quote", 1026 | "syn", 1027 | "version_check", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "proc-macro-error-attr" 1032 | version = "1.0.4" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1035 | dependencies = [ 1036 | "proc-macro2", 1037 | "quote", 1038 | "version_check", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "proc-macro-hack" 1043 | version = "0.5.19" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 1046 | 1047 | [[package]] 1048 | name = "proc-macro-nested" 1049 | version = "0.1.7" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 1052 | 1053 | [[package]] 1054 | name = "proc-macro2" 1055 | version = "1.0.27" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 1058 | dependencies = [ 1059 | "unicode-xid", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "process_path" 1064 | version = "0.1.3" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "abe7f344db15ba012152680e204d5869ec60cb0d8acfda230069484f4fa3811b" 1067 | dependencies = [ 1068 | "libc", 1069 | "winapi 0.3.9", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "quote" 1074 | version = "1.0.9" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 1077 | dependencies = [ 1078 | "proc-macro2", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "redox_syscall" 1083 | version = "0.2.8" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" 1086 | dependencies = [ 1087 | "bitflags", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "regex" 1092 | version = "1.5.4" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 1095 | dependencies = [ 1096 | "aho-corasick", 1097 | "memchr", 1098 | "regex-syntax", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "regex-syntax" 1103 | version = "0.6.25" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 1106 | 1107 | [[package]] 1108 | name = "region" 1109 | version = "2.2.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "877e54ea2adcd70d80e9179344c97f93ef0dffd6b03e1f4529e6e83ab2fa9ae0" 1112 | dependencies = [ 1113 | "bitflags", 1114 | "libc", 1115 | "mach", 1116 | "winapi 0.3.9", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "rustc_version" 1121 | version = "0.3.3" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" 1124 | dependencies = [ 1125 | "semver", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "ryu" 1130 | version = "1.0.5" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1133 | 1134 | [[package]] 1135 | name = "semver" 1136 | version = "0.11.0" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" 1139 | dependencies = [ 1140 | "semver-parser", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "semver-parser" 1145 | version = "0.10.2" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" 1148 | dependencies = [ 1149 | "pest", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "serde" 1154 | version = "1.0.126" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 1157 | dependencies = [ 1158 | "serde_derive", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "serde_bytes" 1163 | version = "0.11.5" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" 1166 | dependencies = [ 1167 | "serde", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "serde_derive" 1172 | version = "1.0.126" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" 1175 | dependencies = [ 1176 | "proc-macro2", 1177 | "quote", 1178 | "syn", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "serde_json" 1183 | version = "1.0.64" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 1186 | dependencies = [ 1187 | "itoa", 1188 | "ryu", 1189 | "serde", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "slab" 1194 | version = "0.4.3" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" 1197 | 1198 | [[package]] 1199 | name = "slice-pool" 1200 | version = "0.4.1" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "733fc6e5f1bd3a8136f842c9bdea4e5f17c910c2fcc98c90c3aa7604ef5e2e7a" 1203 | 1204 | [[package]] 1205 | name = "smallvec" 1206 | version = "1.6.1" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 1209 | 1210 | [[package]] 1211 | name = "strum" 1212 | version = "0.20.0" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" 1215 | 1216 | [[package]] 1217 | name = "strum_macros" 1218 | version = "0.20.1" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" 1221 | dependencies = [ 1222 | "heck", 1223 | "proc-macro2", 1224 | "quote", 1225 | "syn", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "syn" 1230 | version = "1.0.72" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 1233 | dependencies = [ 1234 | "proc-macro2", 1235 | "quote", 1236 | "unicode-xid", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "system-deps" 1241 | version = "3.1.1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "c248107ad7bc1ac07066a4d003cae9e9a7bc2e27d3418f7a9cdcdc8699dbea70" 1244 | dependencies = [ 1245 | "anyhow", 1246 | "cfg-expr", 1247 | "heck", 1248 | "itertools", 1249 | "pkg-config", 1250 | "strum", 1251 | "strum_macros", 1252 | "thiserror", 1253 | "toml", 1254 | "version-compare", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "tar" 1259 | version = "0.4.35" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "7d779dc6aeff029314570f666ec83f19df7280bb36ef338442cfa8c604021b80" 1262 | dependencies = [ 1263 | "filetime", 1264 | "libc", 1265 | "xattr", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "termcolor" 1270 | version = "1.1.2" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1273 | dependencies = [ 1274 | "winapi-util", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "thiserror" 1279 | version = "1.0.25" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" 1282 | dependencies = [ 1283 | "thiserror-impl", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "thiserror-impl" 1288 | version = "1.0.25" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" 1291 | dependencies = [ 1292 | "proc-macro2", 1293 | "quote", 1294 | "syn", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "tinyvec" 1299 | version = "1.2.0" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 1302 | dependencies = [ 1303 | "tinyvec_macros", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "tinyvec_macros" 1308 | version = "0.1.0" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1311 | 1312 | [[package]] 1313 | name = "toml" 1314 | version = "0.5.8" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 1317 | dependencies = [ 1318 | "serde", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "typenum" 1323 | version = "1.13.0" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" 1326 | 1327 | [[package]] 1328 | name = "ucd-trie" 1329 | version = "0.1.3" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" 1332 | 1333 | [[package]] 1334 | name = "unicode-bidi" 1335 | version = "0.3.5" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 1338 | dependencies = [ 1339 | "matches", 1340 | ] 1341 | 1342 | [[package]] 1343 | name = "unicode-normalization" 1344 | version = "0.1.17" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" 1347 | dependencies = [ 1348 | "tinyvec", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "unicode-segmentation" 1353 | version = "1.7.1" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 1356 | 1357 | [[package]] 1358 | name = "unicode-xid" 1359 | version = "0.2.2" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1362 | 1363 | [[package]] 1364 | name = "url" 1365 | version = "2.2.2" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1368 | dependencies = [ 1369 | "form_urlencoded", 1370 | "idna", 1371 | "matches", 1372 | "percent-encoding", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "utf16_lit" 1377 | version = "2.0.2" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "14706d2a800ee8ff38c1d3edb873cd616971ea59eb7c0d046bb44ef59b06a1ae" 1380 | 1381 | [[package]] 1382 | name = "velcro" 1383 | version = "0.5.3" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "85554e2a4b7527966b335b31c7c525e1dd6531cbf41b63470c952da53b7e07bf" 1386 | dependencies = [ 1387 | "velcro_macros", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "velcro_core" 1392 | version = "0.5.2" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "99125cbb1d0dc3ac382bc77cea20335e8608998bd03f7232ac870adf23b0d498" 1395 | dependencies = [ 1396 | "proc-macro2", 1397 | "quote", 1398 | "syn", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "velcro_macros" 1403 | version = "0.5.2" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "d4d6cb3e9601d03fc75e39bf0515373162cc8a3e2bbfe8836709bb1ede341904" 1406 | dependencies = [ 1407 | "syn", 1408 | "velcro_core", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "version-compare" 1413 | version = "0.0.11" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" 1416 | 1417 | [[package]] 1418 | name = "version_check" 1419 | version = "0.9.3" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1422 | 1423 | [[package]] 1424 | name = "walkdir" 1425 | version = "0.1.8" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "c66c0b9792f0a765345452775f3adbd28dde9d33f30d13e5dcc5ae17cf6f3780" 1428 | dependencies = [ 1429 | "kernel32-sys", 1430 | "winapi 0.2.8", 1431 | ] 1432 | 1433 | [[package]] 1434 | name = "widestring" 1435 | version = "0.5.1" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" 1438 | 1439 | [[package]] 1440 | name = "winapi" 1441 | version = "0.2.8" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 1444 | 1445 | [[package]] 1446 | name = "winapi" 1447 | version = "0.3.9" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1450 | dependencies = [ 1451 | "winapi-i686-pc-windows-gnu", 1452 | "winapi-x86_64-pc-windows-gnu", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "winapi-build" 1457 | version = "0.1.1" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 1460 | 1461 | [[package]] 1462 | name = "winapi-i686-pc-windows-gnu" 1463 | version = "0.4.0" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1466 | 1467 | [[package]] 1468 | name = "winapi-util" 1469 | version = "0.1.5" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1472 | dependencies = [ 1473 | "winapi 0.3.9", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "winapi-x86_64-pc-windows-gnu" 1478 | version = "0.4.0" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1481 | 1482 | [[package]] 1483 | name = "windows" 1484 | version = "0.28.0" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "054d31561409bbf7e1ee4a4f0a1233ac2bb79cfadf2a398438a04d8dda69225f" 1487 | dependencies = [ 1488 | "windows-sys", 1489 | "windows_gen", 1490 | "windows_macros", 1491 | "windows_reader", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "windows-sys" 1496 | version = "0.28.0" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" 1499 | dependencies = [ 1500 | "windows_aarch64_msvc", 1501 | "windows_i686_gnu", 1502 | "windows_i686_msvc", 1503 | "windows_x86_64_gnu", 1504 | "windows_x86_64_msvc", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "windows_aarch64_msvc" 1509 | version = "0.28.0" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" 1512 | 1513 | [[package]] 1514 | name = "windows_gen" 1515 | version = "0.28.0" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "7035f7cd53a34e7f3f8eca1bc073cfc18e0c3a40e3c240c40af88694c0f1066d" 1518 | dependencies = [ 1519 | "windows_quote", 1520 | "windows_reader", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "windows_i686_gnu" 1525 | version = "0.28.0" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" 1528 | 1529 | [[package]] 1530 | name = "windows_i686_msvc" 1531 | version = "0.28.0" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" 1534 | 1535 | [[package]] 1536 | name = "windows_macros" 1537 | version = "0.28.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "8223a8c4fd6813d0f0d4223611f2a28507eeb009701d13a029b0241176a678b1" 1540 | dependencies = [ 1541 | "syn", 1542 | "windows_gen", 1543 | "windows_quote", 1544 | "windows_reader", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "windows_quote" 1549 | version = "0.28.0" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "8d7dc698fe6170adc3cb1339f317b2579698364125b5f7a6c20239fa586001ea" 1552 | 1553 | [[package]] 1554 | name = "windows_reader" 1555 | version = "0.28.0" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "00e45d55a9b5ab200ba4412eafff0b25be0d2638003b1510edd4ca71a3566d1a" 1558 | 1559 | [[package]] 1560 | name = "windows_x86_64_gnu" 1561 | version = "0.28.0" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" 1564 | 1565 | [[package]] 1566 | name = "windows_x86_64_msvc" 1567 | version = "0.28.0" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" 1570 | 1571 | [[package]] 1572 | name = "xattr" 1573 | version = "0.2.2" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" 1576 | dependencies = [ 1577 | "libc", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "yaml-rust" 1582 | version = "0.4.5" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1585 | dependencies = [ 1586 | "linked-hash-map", 1587 | ] 1588 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nativeshell_examples" 3 | version = "0.1.0" 4 | authors = ["Matej Knopp "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [build-dependencies] 10 | cargo-emit = "0.2.1" 11 | nativeshell_build = { version = "0.1.16" } 12 | 13 | [dependencies] 14 | nativeshell = { version = "0.1.16" } 15 | env_logger = "0.9.0" 16 | serde = { version = "1.0.119", features = ["derive"] } 17 | 18 | [target.'cfg(target_os = "macos")'.dependencies] 19 | cocoa = "0.24" 20 | objc = "0.2.7" 21 | block = "0.1.6" 22 | 23 | [target.'cfg(target_os = "windows")'.dependencies] 24 | widestring = "0.5.1" 25 | 26 | [target.'cfg(target_os = "windows")'.dependencies.windows] 27 | version = "0.28.0" 28 | features = [ 29 | "Win32_UI_Controls_Dialogs" 30 | ] 31 | 32 | [target.'cfg(target_os = "linux")'.dependencies] 33 | gtk = "0.14.0" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Matej Knopp 2 | 3 | ================================================================================ 4 | 5 | MIT LICENSE 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do 12 | so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 20 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | ================================================================================ 25 | 26 | APACHE LICENSE, VERSION 2.0 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NativeShell Examples 2 | 3 | ![](https://nativeshell.dev/screenshot-dev.png "Screenshot") 4 | 5 | ## Prerequisites 6 | 7 | 1. [Install Rust](https://www.rust-lang.org/tools/install) 8 | 2. [Install Flutter](https://flutter.dev/docs/get-started/install) 9 | 3. [Enable Flutter desktop support](https://flutter.dev/desktop#set-up) 10 | 4. Switch to Fluttter Master (`flutter channel master; flutter upgrade`) 11 | 12 | ## Getting Started 13 | 14 | Launch it with `cargo run`. 15 | 16 | To debug or hot reload dart code, start the 17 | `Flutter: Attach to Process` launch configuration once the application runs. 18 | 19 | For more information go to [nativeshell.dev](https://nativeshell.dev). 20 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | analyzer: 3 | exclude: [target/**] 4 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use nativeshell_build::{AppBundleOptions, BuildResult, Flutter, FlutterOptions, MacOSBundle}; 2 | 3 | fn build_flutter() -> BuildResult<()> { 4 | Flutter::build(FlutterOptions { 5 | ..Default::default() 6 | })?; 7 | 8 | if cfg!(target_os = "macos") { 9 | let options = AppBundleOptions { 10 | bundle_name: "NativeShellExamples.app".into(), 11 | bundle_display_name: "NativeShell Examples".into(), 12 | icon_file: "icons/AppIcon.icns".into(), 13 | ..Default::default() 14 | }; 15 | let resources = MacOSBundle::build(options)?; 16 | resources.mkdir("icons")?; 17 | resources.link("resources/mac_icon.icns", "icons/AppIcon.icns")?; 18 | } 19 | 20 | Ok(()) 21 | } 22 | 23 | fn main() { 24 | if let Err(error) = build_flutter() { 25 | println!("\n** Build failed with error **\n\n{}", error); 26 | panic!(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: undefined_hidden_name, // not in main 2 | import 'package:flutter/material.dart' hide MenuItem; 3 | import 'package:nativeshell/nativeshell.dart'; 4 | 5 | import 'pages/other_window.dart'; 6 | import 'pages/platform_channels.dart'; 7 | import 'main_window.dart'; 8 | import 'pages/modal_window.dart'; 9 | import 'widgets/veil.dart'; 10 | 11 | void main() async { 12 | // Disable shader warmup - it delays producing first frame, which we want to 13 | // produce as soon as possible to reduce time to open new windows. 14 | disableShaderWarmUp(); 15 | runApp(Main()); 16 | } 17 | 18 | // Common scaffold code used by each window 19 | class ExamplesWindow extends StatelessWidget { 20 | const ExamplesWindow({Key? key, required this.child}) : super(key: key); 21 | 22 | final Widget child; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MaterialApp( 27 | home: DefaultTextStyle( 28 | style: TextStyle( 29 | color: Colors.white, 30 | fontSize: 14, 31 | ), 32 | child: WindowLayoutProbe(child: child), 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | class Main extends StatelessWidget { 39 | @override 40 | Widget build(BuildContext context) { 41 | return Veil( 42 | child: Container( 43 | color: Color.fromARGB(255, 30, 30, 35), 44 | child: WindowWidget( 45 | onCreateState: (initData) { 46 | WindowState? state; 47 | 48 | state ??= PlatformChannelsWindowState.fromInitData(initData); 49 | state ??= ModalWindowState.fromInitData(initData); 50 | state ??= OtherWindowState.fromInitData(initData); 51 | state ??= MainWindowState(); 52 | 53 | return state; 54 | }, 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/main_window.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | // ignore: undefined_hidden_name, // not in main 4 | import 'package:flutter/material.dart' hide MenuItem; 5 | import 'package:nativeshell/nativeshell.dart'; 6 | import 'package:nativeshell_examples/pages/flutter_plugins.dart'; 7 | 8 | import 'main.dart'; 9 | import 'widgets/button.dart'; 10 | import 'pages/window_management.dart'; 11 | import 'pages/drag_drop.dart'; 12 | import 'pages/file_open_dialog.dart'; 13 | import 'pages/menu.dart'; 14 | import 'widgets/page.dart'; 15 | import 'pages/platform_channels.dart'; 16 | 17 | class MainWindowState extends WindowState { 18 | @override 19 | Widget build(BuildContext context) { 20 | return ExamplesWindow(child: MainWindow()); 21 | } 22 | 23 | @override 24 | WindowSizingMode get windowSizingMode => 25 | WindowSizingMode.atLeastIntrinsicSize; 26 | 27 | @override 28 | Future initializeWindow(Size intrinsicContentSize) async { 29 | if (Platform.isMacOS) { 30 | await Menu(_buildMenu).setAsAppMenu(); 31 | } 32 | await window.setTitle('NativeShell Examples'); 33 | return super.initializeWindow(intrinsicContentSize); 34 | } 35 | } 36 | 37 | // This will be the default "fallback" app menu used for any window that doesn't 38 | // have other menu 39 | List _buildMenu() => [ 40 | MenuItem.children(title: 'App', children: [ 41 | MenuItem.withRole(role: MenuItemRole.hide), 42 | MenuItem.withRole(role: MenuItemRole.hideOtherApplications), 43 | MenuItem.withRole(role: MenuItemRole.showAll), 44 | MenuItem.separator(), 45 | MenuItem.withRole(role: MenuItemRole.quitApplication), 46 | ]), 47 | MenuItem.children(title: 'Window', role: MenuRole.window, children: [ 48 | MenuItem.withRole(role: MenuItemRole.minimizeWindow), 49 | MenuItem.withRole(role: MenuItemRole.zoomWindow), 50 | ]), 51 | ]; 52 | 53 | class Page { 54 | Page({ 55 | required this.title, 56 | required this.builder, 57 | }); 58 | 59 | final String title; 60 | final WidgetBuilder builder; 61 | } 62 | 63 | final pages = [ 64 | Page( 65 | title: 'Platform Channels', 66 | builder: (BuildContext c) { 67 | return PlatformChannelsPage(); 68 | }, 69 | ), 70 | Page( 71 | title: 'Window Management', 72 | builder: (BuildContext c) { 73 | return WindowManagementPage(); 74 | }, 75 | ), 76 | Page( 77 | title: 'Drag & Drop', 78 | builder: (BuildContext c) { 79 | return DragDropPage(); 80 | }, 81 | ), 82 | Page( 83 | title: 'Menu & MenuBar', 84 | builder: (BuildContext c) { 85 | return MenuPage(); 86 | }, 87 | ), 88 | Page( 89 | title: 'File Open Dialog', 90 | builder: (BuildContext c) { 91 | return FileOpenDialogPage(); 92 | }, 93 | ), 94 | Page( 95 | title: 'Flutter Plugins', 96 | builder: (BuildContext c) { 97 | return FlutterPluginsPage(); 98 | }, 99 | ), 100 | ]; 101 | 102 | class MainWindow extends StatefulWidget { 103 | const MainWindow(); 104 | 105 | @override 106 | State createState() { 107 | return _MainWindowState(); 108 | } 109 | } 110 | 111 | class _MainWindowState extends State { 112 | @override 113 | void initState() { 114 | super.initState(); 115 | selectedPage = pages[0]; 116 | } 117 | 118 | late Page selectedPage; 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return Row( 123 | crossAxisAlignment: CrossAxisAlignment.stretch, 124 | children: [ 125 | IntrinsicWidth( 126 | child: Container( 127 | decoration: BoxDecoration( 128 | color: Colors.blueGrey.shade600, 129 | ), 130 | child: Column( 131 | mainAxisSize: MainAxisSize.min, 132 | crossAxisAlignment: CrossAxisAlignment.stretch, 133 | children: [ 134 | Header(), 135 | SizedBox(height: 15), 136 | PageSelector( 137 | pages: pages, 138 | selectedPage: selectedPage, 139 | onSelected: (Page page) { 140 | setState(() { 141 | selectedPage = page; 142 | }); 143 | }, 144 | ), 145 | SizedBox(height: 20), 146 | ], 147 | ), 148 | ), 149 | ), 150 | Expanded( 151 | child: PageContainer( 152 | child: selectedPage.builder(context), 153 | ), 154 | ) 155 | ], 156 | ); 157 | } 158 | } 159 | 160 | class Header extends StatelessWidget { 161 | const Header(); 162 | 163 | @override 164 | Widget build(BuildContext context) { 165 | return Container( 166 | color: Colors.black.withAlpha(120), 167 | padding: EdgeInsets.all(20), 168 | child: Column( 169 | children: [ 170 | RichText( 171 | text: TextSpan( 172 | style: TextStyle( 173 | fontSize: 20, 174 | fontStyle: FontStyle.italic, 175 | shadows: [ 176 | Shadow( 177 | color: Colors.white.withOpacity(0.7), 178 | blurRadius: 25, 179 | ) 180 | ], 181 | ), 182 | children: [ 183 | TextSpan( 184 | text: 'native', 185 | style: TextStyle( 186 | color: Colors.lightBlue.shade300, 187 | )), 188 | TextSpan( 189 | text: 'shell', 190 | style: TextStyle( 191 | fontWeight: FontWeight.bold, 192 | color: Colors.lightBlue.shade100, 193 | // color: Colors.lightBlue.shade100 194 | )), 195 | ]), 196 | ), 197 | SizedBox(height: 8), 198 | Text( 199 | 'EXAMPLES', 200 | style: TextStyle( 201 | color: Colors.white.withOpacity(0.8), 202 | fontSize: 12, 203 | fontWeight: FontWeight.w600, 204 | letterSpacing: 1.2, 205 | ), 206 | ) 207 | ], 208 | )); 209 | } 210 | } 211 | 212 | class PageSelectorButton extends AbstractButton { 213 | const PageSelectorButton({ 214 | Key? key, 215 | required this.title, 216 | VoidCallback? onPressed, 217 | this.selected = false, 218 | }) : super(key: key, onPressed: onPressed); 219 | 220 | @override 221 | Widget buildContents(BuildContext context, ButtonState state) { 222 | var border = Border.all(color: Colors.white.withOpacity(0), width: 1); 223 | if (state.focused) { 224 | border = Border.all(color: Colors.white.withOpacity(0.4), width: 1); 225 | } 226 | 227 | var color = Colors.white.withOpacity(0); 228 | if (selected) { 229 | color = Colors.white.withOpacity(0.4); 230 | } else if (state.active) { 231 | color = Colors.white.withOpacity(0.6); 232 | } else if (state.hovered) { 233 | color = Colors.white.withOpacity(0.2); 234 | } 235 | 236 | return AnimatedContainer( 237 | duration: Duration(milliseconds: 150), 238 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), 239 | decoration: BoxDecoration( 240 | color: color, 241 | border: border, 242 | ), 243 | child: Text(title), 244 | ); 245 | } 246 | 247 | final String title; 248 | final bool selected; 249 | } 250 | 251 | class PageSelector extends StatelessWidget { 252 | const PageSelector({ 253 | Key? key, 254 | required this.pages, 255 | required this.selectedPage, 256 | required this.onSelected, 257 | }) : super(key: key); 258 | 259 | final List pages; 260 | final Page selectedPage; 261 | final void Function(Page) onSelected; 262 | 263 | @override 264 | Widget build(BuildContext context) { 265 | return Column( 266 | mainAxisSize: MainAxisSize.min, 267 | crossAxisAlignment: CrossAxisAlignment.stretch, 268 | children: pages.map((page) { 269 | return PageSelectorButton( 270 | title: page.title, 271 | selected: selectedPage == page, 272 | onPressed: () { 273 | onSelected(page); 274 | }, 275 | ); 276 | }).toList(), 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/pages/drag_drop.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nativeshell/nativeshell.dart'; 3 | import '../widgets/page.dart'; 4 | 5 | class DragDropPage extends StatefulWidget { 6 | const DragDropPage(); 7 | 8 | @override 9 | State createState() { 10 | return DragDropPageState(); 11 | } 12 | } 13 | 14 | final customDragData = DragDataKey('custom-drag-data'); 15 | 16 | class DragDropPageState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | crossAxisAlignment: CrossAxisAlignment.stretch, 21 | children: [ 22 | PageHeader(child: Text('Drag & Drop Example')), 23 | PageSourceLocation(locations: [ 24 | 'lib/pages/drag_drop.dart', 25 | ]), 26 | PageBlurb(paragraphs: [ 27 | 'Drag & drop suppors files, URIs, application specific data ' 28 | 'and can be extended to support other platform specific formats.', 29 | ]), 30 | Expanded( 31 | child: Row( 32 | crossAxisAlignment: CrossAxisAlignment.stretch, 33 | children: [ 34 | IntrinsicWidth( 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.start, 37 | children: [ 38 | DragSource( 39 | title: 'File Drag Source', 40 | data: DragData([ 41 | DragData.files([ 42 | '/fictional/file/path_1.dart', 43 | '/fictional/file/path_2.dart', 44 | ]), 45 | customDragData({ 46 | 'key1': 'value1', 47 | 'key2': '20', 48 | }) 49 | ]), 50 | ), 51 | SizedBox(height: 15), 52 | DragSource( 53 | title: 'URL Drag Source', 54 | data: DragData([ 55 | DragData.uris([ 56 | Uri.parse('https://google.com'), 57 | ]), 58 | customDragData({ 59 | 'key3': 'value3', 60 | 'key4': '50', 61 | }) 62 | ]), 63 | ) 64 | ], 65 | ), 66 | ), 67 | SizedBox(width: 15), 68 | Expanded(child: DropTarget()) 69 | ], 70 | ), 71 | ), 72 | ], 73 | ); 74 | } 75 | } 76 | 77 | class DragSource extends StatelessWidget { 78 | final String title; 79 | final DragData data; 80 | 81 | const DragSource({Key? key, required this.title, required this.data}) 82 | : super(key: key); 83 | 84 | void startDrag(BuildContext context) async { 85 | final session = await DragSession.beginWithContext( 86 | context: context, 87 | data: data, 88 | allowedEffects: {DragEffect.Copy, DragEffect.Link, DragEffect.Move}); 89 | final res = await session.waitForResult(); 90 | print('Drop result: $res'); 91 | } 92 | 93 | @override 94 | Widget build(BuildContext context) { 95 | return RepaintBoundary( 96 | child: Builder( 97 | builder: (context) { 98 | return _buildInner(context); 99 | }, 100 | ), 101 | ); 102 | } 103 | 104 | Widget _buildInner(BuildContext context) { 105 | return GestureDetector( 106 | onPanStart: (e) { 107 | startDrag(context); 108 | }, 109 | child: Container( 110 | padding: EdgeInsets.all(10), 111 | decoration: BoxDecoration( 112 | color: Colors.white, 113 | borderRadius: BorderRadius.circular(14), 114 | border: Border.all( 115 | color: Colors.lightBlueAccent, 116 | ), 117 | ), 118 | child: DefaultTextStyle.merge( 119 | style: TextStyle(fontSize: 13), 120 | child: Center(child: Text(title)), 121 | ), 122 | ), 123 | ); 124 | } 125 | } 126 | 127 | class DropTarget extends StatefulWidget { 128 | @override 129 | State createState() { 130 | return _DropTargetState(); 131 | } 132 | } 133 | 134 | class _DropTargetState extends State { 135 | DragEffect pickEffect(Set allowedEffects) { 136 | if (allowedEffects.contains(DragEffect.Copy)) { 137 | return DragEffect.Copy; 138 | } else if (allowedEffects.contains(DragEffect.Link)) { 139 | return DragEffect.Link; 140 | } else { 141 | return allowedEffects.isNotEmpty ? allowedEffects.first : DragEffect.None; 142 | } 143 | } 144 | 145 | @override 146 | Widget build(BuildContext context) { 147 | return DropRegion( 148 | onDropOver: (event) async { 149 | final res = pickEffect(event.info.allowedEffects); 150 | 151 | final data = event.info.data; 152 | _files = await data.get(DragData.files); 153 | _uris = await data.get(DragData.uris); 154 | _customData = await data.get(customDragData); 155 | 156 | return res; 157 | }, 158 | onDropExit: () { 159 | setState(() { 160 | _files = null; 161 | _uris = null; 162 | _customData = null; 163 | dropping = false; 164 | }); 165 | }, 166 | onDropEnter: () { 167 | setState(() { 168 | dropping = true; 169 | }); 170 | }, 171 | onPerformDrop: (e) { 172 | print('Performed drop!'); 173 | }, 174 | child: AnimatedContainer( 175 | decoration: BoxDecoration( 176 | color: dropping 177 | ? Colors.amber.withAlpha(70) 178 | : Colors.amber.withAlpha(20), 179 | borderRadius: BorderRadius.circular(14), 180 | border: Border.all( 181 | color: Colors.amber, 182 | ), 183 | ), 184 | duration: Duration(milliseconds: 200), 185 | padding: EdgeInsets.all(20), 186 | child: ClipRect( 187 | child: ConstrainedBox( 188 | constraints: BoxConstraints(minHeight: 100), 189 | child: dropping 190 | ? DefaultTextStyle.merge( 191 | style: 192 | TextStyle(fontSize: 13, color: Colors.grey.shade900), 193 | child: Text(_describeDragData()), 194 | ) 195 | : Center( 196 | child: DefaultTextStyle.merge( 197 | style: TextStyle( 198 | fontWeight: FontWeight.w600, 199 | fontSize: 20, 200 | color: Colors.amber.shade800), 201 | child: Text('Drop Target'), 202 | ), 203 | )), 204 | ), 205 | ), 206 | ); 207 | } 208 | 209 | String _describeDragData() { 210 | final res = StringBuffer(); 211 | 212 | for (final f in _files ?? []) { 213 | res.writeln('$f'); 214 | } 215 | for (final uri in _uris ?? []) { 216 | res.writeln('$uri'); 217 | } 218 | final custom = _customData; 219 | if (custom != null) { 220 | if (res.isNotEmpty) { 221 | res.writeln(); 222 | } 223 | res.writeln('Custom Data:'); 224 | res.writeln('$custom'); 225 | } 226 | return res.toString(); 227 | } 228 | 229 | List? _uris; 230 | List? _files; 231 | Map? _customData; 232 | 233 | bool dropping = false; 234 | } 235 | -------------------------------------------------------------------------------- /lib/pages/file_open_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:nativeshell/nativeshell.dart'; 4 | import 'package:path/path.dart'; 5 | import '../widgets/button.dart'; 6 | import '../widgets/page.dart'; 7 | 8 | final _channel = MethodChannel('file_open_dialog_channel'); 9 | 10 | class FileOpenRequest { 11 | FileOpenRequest({ 12 | required this.parentWindow, 13 | }); 14 | 15 | final WindowHandle parentWindow; 16 | 17 | Map serialize() => { 18 | 'parentWindow': parentWindow.value, 19 | }; 20 | } 21 | 22 | Future showFileOpenDialog(FileOpenRequest request) async { 23 | return await _channel.invokeMethod('showFileOpenDialog', request.serialize()); 24 | } 25 | 26 | class FileOpenDialogPage extends StatefulWidget { 27 | const FileOpenDialogPage(); 28 | 29 | @override 30 | State createState() { 31 | return FileOpenDialogPageState(); 32 | } 33 | } 34 | 35 | class FileOpenDialogPageState extends State { 36 | @override 37 | Widget build(BuildContext context) { 38 | return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 39 | PageHeader(child: Text('File Open Dialog Example')), 40 | PageSourceLocation(locations: [ 41 | 'lib/pages/file_open_dialog.dart', 42 | 'src/file_open_dialog.rs', 43 | ]), 44 | PageBlurb(paragraphs: [ 45 | 'This is an example of showing native platform dialog to select files. In ' 46 | 'future this should be provided directly by nativeshell itself.' 47 | ]), 48 | Row( 49 | children: [ 50 | Column( 51 | crossAxisAlignment: CrossAxisAlignment.start, 52 | children: [ 53 | Button( 54 | onPressed: () async { 55 | _selectFile(context); 56 | }, 57 | child: Text('Select File...'), 58 | ), 59 | if (_selectedFileName != null) ...[ 60 | SizedBox(height: 10), 61 | Text(_selectedFileName!), 62 | ], 63 | ], 64 | ), 65 | ], 66 | ), 67 | ]); 68 | } 69 | 70 | void _selectFile(BuildContext context) async { 71 | final request = FileOpenRequest(parentWindow: Window.of(context).handle); 72 | final file = await showFileOpenDialog(request); 73 | setState(() { 74 | _selectedFileName = file != null ? basename(file) : null; 75 | }); 76 | } 77 | 78 | String? _selectedFileName; 79 | } 80 | -------------------------------------------------------------------------------- /lib/pages/flutter_plugins.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nativeshell_examples/widgets/button.dart'; 3 | import 'package:path_provider/path_provider.dart'; 4 | import 'package:package_info_plus/package_info_plus.dart'; 5 | import 'package:url_launcher/url_launcher.dart'; 6 | 7 | import '../widgets/page.dart'; 8 | 9 | class FlutterPluginsPage extends StatefulWidget { 10 | const FlutterPluginsPage(); 11 | 12 | @override 13 | State createState() { 14 | return FlutterPluginsPageState(); 15 | } 16 | } 17 | 18 | class FlutterPluginsPageState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 22 | PageHeader(child: Text('Flutter Plugins Example')), 23 | PageSourceLocation(locations: [ 24 | 'lib/pages/flutter_plugins.dart', 25 | ]), 26 | PageBlurb(paragraphs: [ 27 | 'NativeShell support Flutter plugins (packages containing native code).' 28 | ]), 29 | Row( 30 | children: [ 31 | Button( 32 | onPressed: () async { 33 | await launch('https://nativeshell.dev'); 34 | }, 35 | child: Text('Open URL'), 36 | ) 37 | ], 38 | ), 39 | if (documentsDirectory != null) ...[ 40 | SizedBox(height: 10), 41 | Text('Documents Directory: $documentsDirectory'), 42 | ], 43 | if (packageName != null) ...[ 44 | SizedBox(height: 10), 45 | Text('Package: $packageName'), 46 | ] 47 | ]); 48 | } 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | doStuff(); 54 | } 55 | 56 | Future _packageName() async { 57 | try { 58 | // FFI call fails on windows 59 | final name = (await PackageInfo.fromPlatform()).packageName; 60 | return name.isNotEmpty ? name : 'Unknown'; 61 | } on Exception { 62 | return 'Failed to retrieve'; 63 | } 64 | } 65 | 66 | void doStuff() async { 67 | final documentsDirectory = await getApplicationDocumentsDirectory(); 68 | final packageName = await _packageName(); 69 | 70 | setState(() { 71 | this.documentsDirectory = documentsDirectory.path; 72 | this.packageName = packageName; 73 | }); 74 | } 75 | 76 | String? documentsDirectory; 77 | String? packageName; 78 | } 79 | -------------------------------------------------------------------------------- /lib/pages/menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | // ignore: undefined_hidden_name, // not in main 5 | import 'package:flutter/material.dart' hide MenuBar, MenuItem; 6 | import 'package:nativeshell/accelerators.dart'; 7 | import 'package:nativeshell/nativeshell.dart'; 8 | 9 | import '../widgets/page.dart'; 10 | 11 | class MenuPage extends StatefulWidget { 12 | const MenuPage(); 13 | 14 | @override 15 | State createState() => _MenuPageState(); 16 | } 17 | 18 | class _MenuPageState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return Column( 22 | crossAxisAlignment: CrossAxisAlignment.stretch, 23 | children: [ 24 | PageHeader(child: Text('Menu & MenuBar Example')), 25 | PageSourceLocation(locations: ['lib/pages/menu.dart']), 26 | PageBlurb(paragraphs: [ 27 | 'NativeShell provides support for native context menus and a MenuBar widget, ' 28 | 'which is a Flutter component that opens into native submenus.' 29 | ]), 30 | Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Container( 34 | decoration: BoxDecoration( 35 | border: Border.all(color: Colors.purple, width: 1), 36 | color: Colors.purple.withOpacity(0.15)), 37 | child: Column( 38 | children: [ 39 | MenuBar( 40 | menu: menu, 41 | itemBuilder: _buildMenuBarItem, 42 | ), 43 | if (Platform.isMacOS) 44 | Padding( 45 | padding: const EdgeInsets.symmetric( 46 | horizontal: 10, vertical: 6), 47 | child: Text( 48 | 'Look up! On macOS the MenuBar is at the top of screen.'), 49 | ) 50 | ], 51 | ), 52 | ), 53 | SizedBox( 54 | height: 20, 55 | ), 56 | GestureDetector( 57 | onSecondaryTapDown: _showContextMenu, 58 | child: Container( 59 | decoration: BoxDecoration( 60 | border: Border.all(color: Colors.blue.shade300), 61 | color: Colors.blue.shade100, 62 | ), 63 | child: Padding( 64 | padding: const EdgeInsets.all(38.0), 65 | child: Text('Right-click here for context menu'), 66 | ), 67 | ), 68 | ), 69 | ], 70 | ), 71 | ], 72 | ); 73 | } 74 | 75 | int _counter = 0; 76 | 77 | void _showContextMenu(TapDownDetails e) async { 78 | final menu = Menu(_buildContextMenu); 79 | 80 | // Show the Loading... item every time context menu is displayed 81 | _lazyMenuLoaded = false; 82 | 83 | // Menu can be updated while visible 84 | final timer = Timer.periodic(Duration(milliseconds: 500), (timer) { 85 | ++_counter; 86 | menu.update(); 87 | }); 88 | 89 | await Window.of(context).showPopupMenu(menu, e.globalPosition); 90 | 91 | timer.cancel(); 92 | } 93 | 94 | // 95 | // Context Menu 96 | // 97 | 98 | late Menu _lazyMenu; // created in initState, used in _buildContextMenu 99 | bool _lazyMenuLoaded = false; 100 | 101 | List _buildLazyMenuItems() => !_lazyMenuLoaded 102 | ? [ 103 | MenuItem(title: 'Loading...', action: null), 104 | ] 105 | : [ 106 | MenuItem(title: 'Menu items can be', action: () {}), 107 | MenuItem(title: 'loaded on demand.', action: () {}), 108 | MenuItem.separator(), 109 | MenuItem(title: 'Counter $_counter', action: null), 110 | ]; 111 | 112 | void _onLazyMenuOpen() async { 113 | await Future.delayed(Duration(seconds: 1)); 114 | _lazyMenuLoaded = true; 115 | _lazyMenu.update(); 116 | } 117 | 118 | List _buildContextMenu() => [ 119 | MenuItem(title: 'A Context Menu Item', action: () {}), 120 | MenuItem(title: 'Update Counter $_counter', action: null), 121 | MenuItem.separator(), 122 | ..._buildCheckAndRadioItems(), 123 | MenuItem.separator(), 124 | MenuItem.children(title: 'Submenu', children: [ 125 | MenuItem(title: 'Submenu Item 1', action: () {}), 126 | MenuItem(title: 'Submenu Item 2', action: () {}), 127 | ]), 128 | MenuItem.separator(), 129 | MenuItem.menu(title: 'Lazy Loaded Submenu', submenu: _lazyMenu), 130 | ]; 131 | 132 | // 133 | // MenuBar 134 | // 135 | 136 | Widget _buildMenuBarItem( 137 | BuildContext context, Widget child, MenuItemState itemState) { 138 | Color background; 139 | Color foreground; 140 | switch (itemState) { 141 | case MenuItemState.regular: 142 | background = Colors.transparent; 143 | foreground = Colors.grey.shade800; 144 | break; 145 | case MenuItemState.hovered: 146 | background = Colors.purple.withOpacity(0.2); 147 | foreground = Colors.grey.shade800; 148 | break; 149 | case MenuItemState.selected: 150 | background = Colors.purple.withOpacity(0.8); 151 | foreground = Colors.white; 152 | break; 153 | case MenuItemState.disabled: 154 | background = Colors.transparent; 155 | foreground = Colors.grey.shade800.withOpacity(0.5); 156 | break; 157 | } 158 | return Container( 159 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 160 | color: background, 161 | child: DefaultTextStyle.merge( 162 | style: TextStyle(color: foreground), 163 | child: child, 164 | ), 165 | ); 166 | } 167 | 168 | late Menu menu; 169 | 170 | @override 171 | void initState() { 172 | super.initState(); 173 | 174 | // MenuBar menu 175 | menu = Menu(_buildMenu); 176 | 177 | // Lazily loaded sub-menu used in context menu 178 | _lazyMenu = Menu(_buildLazyMenuItems, onOpen: _onLazyMenuOpen); 179 | } 180 | 181 | bool check1 = true; 182 | bool check2 = false; 183 | int radioValue = 0; 184 | 185 | // MenuBar items 186 | List _buildMenu() => [ 187 | if (Platform.isMacOS) 188 | MenuItem.children(title: 'App', children: [ 189 | MenuItem.withRole(role: MenuItemRole.hide), 190 | MenuItem.withRole(role: MenuItemRole.hideOtherApplications), 191 | MenuItem.withRole(role: MenuItemRole.showAll), 192 | MenuItem.separator(), 193 | MenuItem.withRole(role: MenuItemRole.quitApplication), 194 | ]), 195 | MenuItem.children(title: '&File', children: [ 196 | MenuItem(title: 'New', accelerator: cmdOrCtrl + 'n', action: () {}), 197 | MenuItem(title: 'Open', accelerator: cmdOrCtrl + 'o', action: () {}), 198 | MenuItem.separator(), 199 | MenuItem(title: 'Save', accelerator: cmdOrCtrl + 's', action: null), 200 | MenuItem(title: 'Save As', action: null), 201 | MenuItem.separator(), 202 | MenuItem(title: 'Close', action: () {}), 203 | ]), 204 | MenuItem.children(title: '&Edit', children: [ 205 | MenuItem(title: 'Cut', accelerator: cmdOrCtrl + 'x', action: () {}), 206 | MenuItem(title: 'Copy', accelerator: cmdOrCtrl + 'c', action: () {}), 207 | MenuItem(title: 'Paste', accelerator: cmdOrCtrl + 'v', action: () {}), 208 | MenuItem.separator(), 209 | MenuItem(title: 'Find', accelerator: cmdOrCtrl + 'f', action: () {}), 210 | MenuItem(title: 'Replace', action: () {}), 211 | ]), 212 | MenuItem.children(title: 'Another Menu', children: [ 213 | ..._buildCheckAndRadioItems(), 214 | MenuItem.separator(), 215 | MenuItem.children(title: 'Submenu', children: [ 216 | MenuItem(title: 'More of the same, I guess?', action: null), 217 | MenuItem.separator(), 218 | ..._buildCheckAndRadioItems(), 219 | ]), 220 | ]), 221 | if (Platform.isMacOS) 222 | MenuItem.children(title: 'Window', role: MenuRole.window, children: [ 223 | MenuItem.withRole(role: MenuItemRole.minimizeWindow), 224 | MenuItem.withRole(role: MenuItemRole.zoomWindow), 225 | ]), 226 | MenuItem.children(title: '&Help', children: [ 227 | MenuItem(title: 'About', action: () {}), 228 | ]), 229 | ]; 230 | 231 | // Used in both MenuBar and ContextMenu 232 | List _buildCheckAndRadioItems() => [ 233 | MenuItem( 234 | title: 'Checkable Item 1', 235 | checkStatus: check1 ? CheckStatus.checkOn : CheckStatus.checkOff, 236 | action: () { 237 | check1 = !check1; 238 | menu.update(); 239 | }), 240 | MenuItem( 241 | title: 'Checkable Item 2', 242 | checkStatus: check2 ? CheckStatus.checkOn : CheckStatus.checkOff, 243 | action: () { 244 | check2 = !check2; 245 | menu.update(); 246 | }), 247 | MenuItem.separator(), 248 | MenuItem( 249 | title: 'Radio Item 1', 250 | checkStatus: 251 | radioValue == 0 ? CheckStatus.radioOn : CheckStatus.radioOff, 252 | action: () { 253 | radioValue = 0; 254 | menu.update(); 255 | }), 256 | MenuItem( 257 | title: 'Radio Item 2', 258 | checkStatus: 259 | radioValue == 1 ? CheckStatus.radioOn : CheckStatus.radioOff, 260 | action: () { 261 | radioValue = 1; 262 | menu.update(); 263 | }), 264 | MenuItem( 265 | title: 'Radio Item 3', 266 | checkStatus: 267 | radioValue == 2 ? CheckStatus.radioOn : CheckStatus.radioOff, 268 | action: () { 269 | radioValue = 2; 270 | menu.update(); 271 | }), 272 | ]; 273 | } 274 | -------------------------------------------------------------------------------- /lib/pages/modal_window.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nativeshell/nativeshell.dart'; 3 | 4 | import '../main.dart'; 5 | import '../widgets/animated_visibility.dart'; 6 | import '../widgets/button.dart'; 7 | import '../widgets/page.dart'; 8 | 9 | class ModalWindowState extends WindowState { 10 | @override 11 | Widget build(BuildContext context) { 12 | return ExamplesWindow(child: ModalWindow()); 13 | } 14 | 15 | static ModalWindowState? fromInitData(dynamic initData) { 16 | if (initData is Map && initData['class'] == 'modalWindow') { 17 | return ModalWindowState(); 18 | } 19 | return null; 20 | } 21 | 22 | static dynamic toInitData() => { 23 | 'class': 'modalWindow', 24 | }; 25 | 26 | @override 27 | WindowSizingMode get windowSizingMode => WindowSizingMode.sizeToContents; 28 | 29 | @override 30 | Future initializeWindow(Size intrinsicContentSize) async { 31 | await window.setStyle(WindowStyle(canResize: false)); 32 | await window.setGeometry(await centerInParent(intrinsicContentSize)); 33 | await window.show(); 34 | } 35 | } 36 | 37 | class ModalWindow extends StatelessWidget { 38 | const ModalWindow(); 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return DefaultTextStyle.merge( 43 | style: TextStyle(color: Colors.grey.shade900), 44 | child: Container( 45 | padding: EdgeInsets.all(24), 46 | color: Colors.white, 47 | child: Column( 48 | // This is necessary when using autoSizeWindow, as there are no 49 | // incoming constraints from the window itself 50 | mainAxisSize: MainAxisSize.min, 51 | children: [ 52 | PageBlurb(paragraphs: [ 53 | 'This is a Modal Dialog. It is sized to fit.', 54 | 'Pick the result:' 55 | ]), 56 | Row( 57 | mainAxisSize: MainAxisSize.min, 58 | children: [ 59 | Button( 60 | onPressed: () { 61 | // Result can be anything serializable with StandardMethodCodec 62 | Window.of(context).closeWithResult(true); 63 | }, 64 | child: Text('Yes'), 65 | ), 66 | SizedBox( 67 | width: 10, 68 | ), 69 | Button( 70 | onPressed: () { 71 | Window.of(context).closeWithResult(false); 72 | }, 73 | child: Text('No'), 74 | ), 75 | ], 76 | ), 77 | SizedBox(height: 10), 78 | ExtraOptions(), 79 | ], 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | class ExtraOptions extends StatefulWidget { 87 | @override 88 | State createState() { 89 | return ExtraOptionsState(); 90 | } 91 | } 92 | 93 | class ExtraOptionsState extends State { 94 | @override 95 | Widget build(BuildContext context) { 96 | return Column( 97 | mainAxisSize: MainAxisSize.min, 98 | children: [ 99 | TextButton( 100 | onPressed: () { 101 | setState(() { 102 | extraOptionsVisible = !extraOptionsVisible; 103 | }); 104 | }, 105 | child: !extraOptionsVisible 106 | ? Text('Show more options...') 107 | : Text('Hide more options')), 108 | AnimatedVisibility( 109 | visible: extraOptionsVisible, 110 | alignment: Alignment.topCenter, 111 | duration: Duration(milliseconds: 200), 112 | direction: Axis.vertical, 113 | child: Padding( 114 | padding: const EdgeInsets.only(top: 8.0), 115 | child: Button( 116 | onPressed: () { 117 | Window.of(context).closeWithResult('Maybe'); 118 | }, 119 | child: Text('Maybe'), 120 | ), 121 | )), 122 | ], 123 | ); 124 | } 125 | 126 | bool extraOptionsVisible = false; 127 | } 128 | -------------------------------------------------------------------------------- /lib/pages/other_window.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nativeshell/nativeshell.dart'; 3 | import 'package:nativeshell_examples/widgets/button.dart'; 4 | 5 | import '../main.dart'; 6 | 7 | class OtherWindowState extends WindowState { 8 | @override 9 | Widget build(BuildContext context) { 10 | return ExamplesWindow(child: OtherWindow()); 11 | } 12 | 13 | @override 14 | Future initializeWindow(Size intrinsicContentSize) async { 15 | // If possible, show the window to the right of parent window 16 | Offset? origin; 17 | final parentGeometry = await window.parentWindow?.getGeometry(); 18 | if (parentGeometry?.frameOrigin != null && 19 | parentGeometry?.frameSize != null) { 20 | origin = parentGeometry!.frameOrigin! 21 | .translate(parentGeometry.frameSize!.width + 20, 0); 22 | } 23 | await window.setGeometry(Geometry( 24 | frameOrigin: origin, 25 | contentSize: intrinsicContentSize, 26 | )); 27 | await window.setStyle(WindowStyle(canResize: false)); 28 | await window.show(); 29 | } 30 | 31 | @override 32 | WindowSizingMode get windowSizingMode => WindowSizingMode.sizeToContents; 33 | 34 | static dynamic toInitData() => { 35 | 'class': 'otherWindow', 36 | }; 37 | 38 | static OtherWindowState? fromInitData(dynamic initData) { 39 | if (initData is Map && initData['class'] == 'otherWindow') { 40 | return OtherWindowState(); 41 | } 42 | return null; 43 | } 44 | } 45 | 46 | class OtherWindow extends StatefulWidget { 47 | const OtherWindow(); 48 | 49 | @override 50 | State createState() { 51 | return _OtherWindowState(); 52 | } 53 | } 54 | 55 | class _OtherWindowState extends State 56 | with WindowMethodCallHandlerMixin { 57 | @override 58 | Widget build(BuildContext context) { 59 | // can't call Window.of(context) in initState 60 | if (firstBuild) { 61 | firstBuild = false; 62 | 63 | // Disable the button when parent window gets closed 64 | Window.of(context).parentWindow?.closeEvent.addListener(() { 65 | setState(() {}); 66 | }); 67 | } 68 | 69 | return Container( 70 | color: Colors.blueGrey.shade50, 71 | padding: EdgeInsets.all(20), 72 | child: DefaultTextStyle.merge( 73 | style: TextStyle(color: Colors.black), 74 | child: Column( 75 | children: [ 76 | Button( 77 | onPressed: Window.of(context).parentWindow != null 78 | ? callMethodOnParentWindow 79 | : null, 80 | child: Text( 81 | 'Call method on parent window', 82 | ), 83 | ), 84 | if (messageFromParentWindow != null) ...[ 85 | SizedBox(height: 15), 86 | Text('Parent window says:'), 87 | SizedBox(height: 5), 88 | Text('$messageFromParentWindow'), 89 | ] 90 | ], 91 | ), 92 | ), 93 | ); 94 | } 95 | 96 | void callMethodOnParentWindow() async { 97 | await Window.of(context).parentWindow?.callMethod('showMessage', 'Hello'); 98 | } 99 | 100 | bool firstBuild = true; 101 | 102 | String? messageFromParentWindow; 103 | 104 | @override 105 | MethodCallHandler? onMethodCall(String method) { 106 | if (method == 'showMessage') { 107 | return showMessage; 108 | } else { 109 | return null; 110 | } 111 | } 112 | 113 | void showMessage(dynamic message) { 114 | setState(() { 115 | messageFromParentWindow = message; 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/pages/platform_channels.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:nativeshell/nativeshell.dart'; 4 | 5 | import '../main.dart'; 6 | import '../widgets/button.dart'; 7 | import '../widgets/page.dart'; 8 | 9 | final _channel = MethodChannel('example_channel'); 10 | 11 | class PlatformChannelsPage extends StatefulWidget { 12 | const PlatformChannelsPage(); 13 | 14 | @override 15 | State createState() => _PlatformChannelsPageState(); 16 | } 17 | 18 | class _PlatformChannelsPageState extends State { 19 | Object? helloReply; 20 | Object? backgroundTaskReply; 21 | bool backgroundTaskInProgress = false; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Column( 26 | crossAxisAlignment: CrossAxisAlignment.stretch, 27 | children: [ 28 | PageHeader(child: Text('Platform Channels Example')), 29 | PageSourceLocation(locations: [ 30 | 'lib/pages/platform_channels.dart', 31 | 'src/platform_channels.rs' 32 | ]), 33 | PageBlurb(paragraphs: [ 34 | 'NativeShell provides consistent platform agnostic API to register platform channel handlers.', 35 | 'You only need to register handler once and it can be called from any isolate (window).' 36 | ]), 37 | Table( 38 | columnWidths: { 39 | 0: IntrinsicColumnWidth(), 40 | 1: FlexColumnWidth(), 41 | }, 42 | defaultVerticalAlignment: TableCellVerticalAlignment.middle, 43 | children: [ 44 | TableRow( 45 | children: [ 46 | Button( 47 | onPressed: onHello, 48 | child: Text('Echo: Send \'Hello\''), 49 | ), 50 | Row( 51 | children: [ 52 | if (helloReply != null) ...[ 53 | SizedBox( 54 | width: 20, 55 | ), 56 | Text('Received reply: '), 57 | Text('$helloReply') 58 | ] 59 | ], 60 | ) 61 | ], 62 | ), 63 | TableRow(children: [SizedBox(height: 10), Container()]), 64 | TableRow(children: [ 65 | Button( 66 | onPressed: backgroundTaskInProgress ? null : onBackgroundTask, 67 | child: Text('Long Running Task'), 68 | ), 69 | Row( 70 | children: [ 71 | SizedBox(width: 20), 72 | if (backgroundTaskInProgress) CupertinoActivityIndicator(), 73 | if (backgroundTaskReply != null) ...[ 74 | Text('Received reply: '), 75 | Text('$backgroundTaskReply') 76 | ], 77 | ], 78 | ) 79 | ]), 80 | TableRow(children: [SizedBox(height: 10), Container()]), 81 | TableRow(children: [ 82 | Button( 83 | onPressed: onOpenWindow, 84 | child: Text('Open in New Window'), 85 | ), 86 | Container(), 87 | ]) 88 | ], 89 | ), 90 | ], 91 | ); 92 | } 93 | 94 | void onHello() async { 95 | final reply = await _channel.invokeMethod('echo', 'Hello'); 96 | setState(() { 97 | helloReply = reply; 98 | }); 99 | } 100 | 101 | void onBackgroundTask() async { 102 | setState(() { 103 | backgroundTaskReply = null; 104 | backgroundTaskInProgress = true; 105 | }); 106 | final reply = await _channel.invokeMethod('backgroundTask'); 107 | setState(() { 108 | backgroundTaskInProgress = false; 109 | backgroundTaskReply = reply; 110 | }); 111 | } 112 | 113 | void onOpenWindow() async { 114 | await Window.create(PlatformChannelsWindowState.toInitData()); 115 | } 116 | } 117 | 118 | class PlatformChannelsWindowState extends WindowState { 119 | @override 120 | Widget build(BuildContext context) { 121 | return ExamplesWindow( 122 | child: PageContainer( 123 | child: IntrinsicWidth(child: PlatformChannelsPage()), 124 | ), 125 | ); 126 | } 127 | 128 | @override 129 | Future initializeWindow(Size intrinsicContentSize) async { 130 | await window.setStyle(WindowStyle(canResize: false)); 131 | final geometry = await centerInParent(intrinsicContentSize); 132 | // translate the window slightly. it may have same size as parent window 133 | // so centering it looks weird 134 | await window.setGeometry(geometry.translate(20, 20)); 135 | await window.show(); 136 | } 137 | 138 | @override 139 | WindowSizingMode get windowSizingMode => WindowSizingMode.sizeToContents; 140 | 141 | static dynamic toInitData() => { 142 | 'class': 'platformChannelsWindow', 143 | }; 144 | 145 | static PlatformChannelsWindowState? fromInitData(dynamic initData) { 146 | if (initData is Map && initData['class'] == 'platformChannelsWindow') { 147 | return PlatformChannelsWindowState(); 148 | } 149 | return null; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/pages/window_management.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nativeshell/nativeshell.dart'; 3 | import 'package:nativeshell_examples/pages/other_window.dart'; 4 | 5 | import 'modal_window.dart'; 6 | import '../widgets/veil.dart'; 7 | import '../widgets/button.dart'; 8 | import '../widgets/page.dart'; 9 | 10 | class WindowManagementPage extends StatefulWidget { 11 | const WindowManagementPage({Key? key}) : super(key: key); 12 | 13 | @override 14 | State createState() { 15 | return WindowManagementPageState(); 16 | } 17 | } 18 | 19 | class WindowManagementPageState extends State 20 | with WindowMethodCallHandlerMixin { 21 | Object? modalWindowResult; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Column( 26 | crossAxisAlignment: CrossAxisAlignment.stretch, 27 | children: [ 28 | PageHeader(child: Text('Window Management Example')), 29 | PageSourceLocation(locations: [ 30 | 'lib/pages/window_management.dart', 31 | 'lib/pages/modal_window.dart' 32 | ]), 33 | PageBlurb(paragraphs: [ 34 | 'NativeShell lets you create, show, hide, position windows, set their attributes and style. ' 35 | 'You can also show windows as modal dialogs (sheets on macOS).', 36 | 'Windows can track content size, or be resizable with automatic minimum size, like this window.' 37 | ]), 38 | Table( 39 | columnWidths: { 40 | 0: IntrinsicColumnWidth(), 41 | 1: FlexColumnWidth(), 42 | }, 43 | defaultVerticalAlignment: TableCellVerticalAlignment.top, 44 | children: [ 45 | TableRow(children: [ 46 | Button( 47 | onPressed: showModalDialog, 48 | child: Text('Show Modal Dialog'), 49 | ), 50 | TableCell( 51 | verticalAlignment: TableCellVerticalAlignment.middle, 52 | child: Row( 53 | children: [ 54 | if (modalWindowResult != null) ...[ 55 | SizedBox( 56 | width: 10, 57 | ), 58 | Text('Received result: '), 59 | Text('$modalWindowResult') 60 | ] 61 | ], 62 | ), 63 | ), 64 | ]), 65 | TableRow(children: [SizedBox(height: 10), Container()]), 66 | TableRow(children: [ 67 | otherWindow == null 68 | ? Button( 69 | onPressed: showOtherWindow, 70 | child: Text('Show Other Window'), 71 | ) 72 | : Button( 73 | onPressed: closeOtherWindow, 74 | child: Text('Hide Other Window'), 75 | ), 76 | Row(children: [ 77 | SizedBox( 78 | width: 10, 79 | ), 80 | if (otherWindow != null) 81 | Column( 82 | crossAxisAlignment: CrossAxisAlignment.start, 83 | children: [ 84 | Button( 85 | onPressed: callMethodOnOtherWindow, 86 | child: Text('Call Method'), 87 | ), 88 | if (messageFromOtherWindow != null) ...[ 89 | SizedBox(height: 10), 90 | Text('Other window says: $messageFromOtherWindow'), 91 | ] 92 | ], 93 | ) 94 | ]), 95 | ]), 96 | ]), 97 | ], 98 | ); 99 | } 100 | 101 | Window? otherWindow; 102 | String? messageFromOtherWindow; 103 | 104 | void showOtherWindow() async { 105 | // use veil to prevent double events while waiting for window to initialize 106 | await Veil.show(context, () async { 107 | final window = await Window.create(OtherWindowState.toInitData()); 108 | setState(() { 109 | otherWindow = window; 110 | }); 111 | 112 | // get notification when user closes other window 113 | window.closeEvent.addListener(() { 114 | // when hiding window from dispose the close event will be fired, but 115 | // but at that point we're not mounted anymore 116 | if (mounted) { 117 | setState(() { 118 | otherWindow = null; 119 | messageFromOtherWindow = null; 120 | }); 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | void closeOtherWindow() async { 127 | await otherWindow?.close(); 128 | setState(() { 129 | otherWindow = null; 130 | messageFromOtherWindow = null; 131 | }); 132 | } 133 | 134 | void callMethodOnOtherWindow() async { 135 | await otherWindow?.callMethod('showMessage', 'Hello from parent window!'); 136 | } 137 | 138 | // handles method call on this window 139 | @override 140 | MethodCallHandler? onMethodCall(String name) { 141 | if (name == 'showMessage') { 142 | return showMessage; 143 | } else { 144 | return null; 145 | } 146 | } 147 | 148 | void showMessage(dynamic arguments) { 149 | setState(() { 150 | messageFromOtherWindow = arguments; 151 | }); 152 | } 153 | 154 | @override 155 | void dispose() { 156 | otherWindow?.close(); 157 | super.dispose(); 158 | } 159 | 160 | // 161 | // Modal Window 162 | // 163 | 164 | void showModalDialog() async { 165 | final res = await Veil.show(context, () async { 166 | final win = await Window.create(ModalWindowState.toInitData()); 167 | return await win.showModal(); 168 | }); 169 | setState(() { 170 | modalWindowResult = res; 171 | }); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/util.dart: -------------------------------------------------------------------------------- 1 | extension Intersperse on Iterable { 2 | Iterable intersperse(T element) sync* { 3 | final iterator = this.iterator; 4 | if (iterator.moveNext()) { 5 | yield iterator.current; 6 | while (iterator.moveNext()) { 7 | yield element; 8 | yield iterator.current; 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/widgets/animated_visibility.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | class AnimatedVisibility extends ImplicitlyAnimatedWidget { 5 | AnimatedVisibility({ 6 | Key? key, 7 | required this.visible, 8 | required Duration duration, 9 | required this.child, 10 | required this.direction, 11 | this.alignment = Alignment.topLeft, 12 | }) : super( 13 | key: key, 14 | duration: duration, 15 | curve: Curves.linear, 16 | ); 17 | 18 | @override 19 | ImplicitlyAnimatedWidgetState createState() => _AnimatedVisibilityState(); 20 | 21 | final bool visible; 22 | final Widget child; 23 | final Axis direction; 24 | final Alignment alignment; 25 | } 26 | 27 | class _AnimatedVisibilityState 28 | extends AnimatedWidgetBaseState { 29 | Tween? _factor; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | } 35 | 36 | @override 37 | void forEachTween(TweenVisitor visitor) { 38 | var factor = widget.visible ? 1.0 : 0.0; 39 | 40 | _factor = visitor( 41 | _factor, factor, (dynamic value) => Tween(begin: value)); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | double factor = _factor!.evaluate(animation); 47 | 48 | if (factor == 0) { 49 | return Container( 50 | width: widget.direction == Axis.horizontal ? 0.0 : null, 51 | height: widget.direction == Axis.vertical ? 0.0 : null, 52 | ); 53 | } 54 | 55 | Widget child = BetterAlign( 56 | alignment: widget.alignment, 57 | widthFactor: widget.direction == Axis.horizontal ? factor : null, 58 | heightFactor: widget.direction == Axis.vertical ? factor : null, 59 | child: widget.child, 60 | ); 61 | if (factor < 1.0) { 62 | child = Opacity( 63 | opacity: factor, 64 | child: ClipRect(child: child), 65 | ); 66 | } 67 | return child; 68 | } 69 | } 70 | 71 | class BetterAlign extends Align { 72 | const BetterAlign({ 73 | Key? key, 74 | Alignment alignment = Alignment.center, 75 | double? widthFactor, 76 | double? heightFactor, 77 | Widget? child, 78 | }) : super( 79 | key: key, 80 | alignment: alignment, 81 | widthFactor: widthFactor, 82 | heightFactor: heightFactor, 83 | child: child); 84 | 85 | @override 86 | _RenderAlign createRenderObject(BuildContext context) { 87 | return _RenderAlign( 88 | alignment: alignment, 89 | widthFactor: widthFactor, 90 | heightFactor: heightFactor, 91 | textDirection: Directionality.of(context), 92 | ); 93 | } 94 | } 95 | 96 | class _RenderAlign extends RenderPositionedBox { 97 | _RenderAlign({ 98 | RenderBox? child, 99 | double? widthFactor, 100 | double? heightFactor, 101 | AlignmentGeometry alignment = Alignment.center, 102 | TextDirection? textDirection, 103 | }) : super( 104 | child: child, 105 | widthFactor: widthFactor, 106 | heightFactor: heightFactor, 107 | alignment: alignment, 108 | textDirection: textDirection, 109 | ); 110 | 111 | @override 112 | double computeMinIntrinsicHeight(double width) => heightFactor == 0.0 113 | ? 0.0 114 | : (heightFactor ?? 1.0) * super.computeMinIntrinsicHeight(width); 115 | 116 | @override 117 | double computeMaxIntrinsicHeight(double width) => heightFactor == 0.0 118 | ? 0.0 119 | : (heightFactor ?? 1.0) * super.computeMaxIntrinsicHeight(width); 120 | 121 | @override 122 | double computeMinIntrinsicWidth(double height) => widthFactor == 0.0 123 | ? 0.0 124 | : (widthFactor ?? 1.0) * super.computeMinIntrinsicWidth(height); 125 | 126 | @override 127 | double computeMaxIntrinsicWidth(double height) => widthFactor == 0.0 128 | ? 0.0 129 | : (widthFactor ?? 1.0) * super.computeMaxIntrinsicWidth(height); 130 | 131 | @override 132 | void performLayout() { 133 | final shrinkWrapWidth = 134 | widthFactor != null || constraints.maxWidth == double.infinity; 135 | final shrinkWrapHeight = 136 | heightFactor != null || constraints.maxHeight == double.infinity; 137 | 138 | if (child != null) { 139 | child!.layout( 140 | constraints.copyWith( 141 | // only loosen constraints when shrink wrapping 142 | minWidth: shrinkWrapWidth ? 0.0 : null, 143 | minHeight: shrinkWrapHeight ? 0.0 : null, 144 | ), 145 | parentUsesSize: true); 146 | size = constraints.constrain(Size( 147 | shrinkWrapWidth 148 | ? child!.size.width * (widthFactor ?? 1.0) 149 | : double.infinity, 150 | shrinkWrapHeight 151 | ? child!.size.height * (heightFactor ?? 1.0) 152 | : double.infinity)); 153 | alignChild(); 154 | } else { 155 | size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity, 156 | shrinkWrapHeight ? 0.0 : double.infinity)); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/widgets/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | abstract class ButtonState { 6 | bool get active; 7 | bool get disabled; 8 | bool get enabled; 9 | bool get hovered; 10 | bool get focused; 11 | } 12 | 13 | // Abstract Button class that handles hovered, active, enabled and focused 14 | // states. 15 | abstract class AbstractButton extends StatefulWidget { 16 | const AbstractButton({ 17 | Key? key, 18 | this.onPressed, 19 | }) : super(key: key); 20 | 21 | final VoidCallback? onPressed; 22 | 23 | Widget buildContents(BuildContext context, ButtonState state); 24 | 25 | @override 26 | State createState() { 27 | return AbstractButtonState(); 28 | } 29 | } 30 | 31 | class AbstractButtonState extends State 32 | implements ButtonState { 33 | // Button is pressed, either by holding space key or mouse button 34 | @override 35 | bool get active => _active; 36 | 37 | // Button is disabled 38 | @override 39 | bool get disabled => widget.onPressed == null; 40 | 41 | // Button is enabled 42 | @override 43 | bool get enabled => !disabled; 44 | 45 | // Mouse cursor is over button 46 | @override 47 | bool get hovered => _hovered; 48 | 49 | // Button has keyboard focus 50 | @override 51 | bool get focused => _focused; 52 | 53 | late FocusNode _node; 54 | late FocusAttachment _nodeAttachment; 55 | bool _focused = false; 56 | bool _active = false; 57 | bool _hovered = false; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | _node = FocusNode(debugLabel: 'Button'); 63 | _node.addListener(_onFocusChange); 64 | _nodeAttachment = _node.attach(context, onKey: _handleKeyPress); 65 | } 66 | 67 | void _onFocusChange() { 68 | if (_node.hasFocus != _focused) { 69 | setState(() { 70 | _focused = _node.hasFocus; 71 | }); 72 | } 73 | } 74 | 75 | KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) { 76 | if (event is RawKeyDownEvent && 77 | event.logicalKey == LogicalKeyboardKey.enter) { 78 | if (widget.onPressed != null) { 79 | widget.onPressed!(); 80 | } 81 | return KeyEventResult.handled; 82 | } 83 | if (event is RawKeyDownEvent && 84 | event.logicalKey == LogicalKeyboardKey.space) { 85 | if (_canBecomeActive() && !_active) { 86 | setState(() { 87 | _active = true; 88 | }); 89 | } 90 | return KeyEventResult.handled; 91 | } 92 | if (event is RawKeyUpEvent && 93 | event.logicalKey == LogicalKeyboardKey.space && 94 | _active) { 95 | setState(() { 96 | _active = false; 97 | }); 98 | widget.onPressed!(); 99 | return KeyEventResult.handled; 100 | } 101 | 102 | return KeyEventResult.ignored; 103 | } 104 | 105 | @override 106 | void dispose() { 107 | _node.removeListener(_onFocusChange); 108 | _node.dispose(); 109 | super.dispose(); 110 | } 111 | 112 | void _dragUpdate(BuildContext context, Offset globalPosition) { 113 | final active = _isInside(globalPosition); 114 | if (active != _active) { 115 | setState(() { 116 | _hovered = active; 117 | _active = active; 118 | }); 119 | } 120 | } 121 | 122 | bool _isInside(Offset globalPosition) { 123 | final b = context.findRenderObject() as RenderBox?; 124 | if (b != null) { 125 | final pos = b.globalToLocal(globalPosition); 126 | return pos.dx >= 0 && 127 | pos.dy >= 0 && 128 | pos.dx < b.size.width && 129 | pos.dy < b.size.height; 130 | } else { 131 | return false; 132 | } 133 | } 134 | 135 | bool _canBecomeActive() { 136 | return !disabled; 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | _nodeAttachment.reparent(); 142 | 143 | return GestureDetector( 144 | behavior: HitTestBehavior.opaque, 145 | onPanDown: (DragDownDetails d) { 146 | if (!_canBecomeActive()) return; 147 | _dragUpdate(context, d.globalPosition); 148 | }, 149 | onPanUpdate: (DragUpdateDetails d) { 150 | if (!_canBecomeActive()) return; 151 | _dragUpdate(context, d.globalPosition); 152 | }, 153 | onPanCancel: () { 154 | if (_active) { 155 | setState(() { 156 | _active = false; 157 | }); 158 | } 159 | }, 160 | onPanEnd: (DragEndDetails d) { 161 | if (_active) { 162 | widget.onPressed!(); 163 | setState(() { 164 | _active = false; 165 | }); 166 | } 167 | }, 168 | onTapUp: (TapUpDetails d) { 169 | if (widget.onPressed != null) { 170 | widget.onPressed!(); 171 | } 172 | }, 173 | child: MouseRegion( 174 | onEnter: (PointerEnterEvent e) { 175 | if (!_hovered && e.buttons == 0) { 176 | setState(() { 177 | _hovered = true; 178 | }); 179 | } 180 | }, 181 | onExit: (PointerExitEvent e) { 182 | if (_hovered) { 183 | setState(() { 184 | _hovered = false; 185 | }); 186 | } 187 | }, 188 | child: widget.buildContents(context, this), 189 | ), 190 | ); 191 | } 192 | } 193 | 194 | class Button extends AbstractButton { 195 | const Button({ 196 | Key? key, 197 | required this.child, 198 | VoidCallback? onPressed, 199 | }) : super(key: key, onPressed: onPressed); 200 | 201 | final Widget child; 202 | 203 | @override 204 | Widget buildContents(BuildContext context, ButtonState state) { 205 | final radius = BorderRadius.circular(6); 206 | 207 | Decoration decoration; 208 | if (state.active) { 209 | decoration = BoxDecoration( 210 | borderRadius: radius, 211 | color: Colors.blue.shade100, 212 | border: Border.all(color: Colors.blue.shade400, width: 1), 213 | boxShadow: [ 214 | BoxShadow( 215 | color: Colors.black.withOpacity(0.15), 216 | blurRadius: 2, 217 | spreadRadius: 1) 218 | ]); 219 | } else if (state.hovered && state.enabled) { 220 | decoration = BoxDecoration( 221 | color: Colors.blue.shade100, 222 | border: Border.all(color: Colors.blue.shade300, width: 1), 223 | borderRadius: radius, 224 | boxShadow: [ 225 | BoxShadow( 226 | color: Colors.black.withOpacity(0.15), 227 | blurRadius: 6, 228 | spreadRadius: 4) 229 | ]); 230 | } else { 231 | decoration = BoxDecoration( 232 | borderRadius: radius, 233 | color: Colors.blue.shade50, 234 | border: Border.all(color: Colors.blueGrey.shade200, width: 1), 235 | boxShadow: [ 236 | BoxShadow( 237 | color: Colors.black.withOpacity(0.15), 238 | blurRadius: 4, 239 | spreadRadius: 1) 240 | ], 241 | // border: Border.all(color: Colors.transparent), 242 | ); 243 | } 244 | 245 | return AnimatedContainer( 246 | // Focus decoration 247 | duration: Duration(milliseconds: state.focused ? 300 : 0), 248 | decoration: state.focused && state.enabled 249 | ? BoxDecoration( 250 | borderRadius: BorderRadius.circular(8), 251 | boxShadow: [ 252 | BoxShadow( 253 | color: Colors.blue.withOpacity(0.3), 254 | blurRadius: 0, 255 | spreadRadius: 3) 256 | ]) 257 | : BoxDecoration( 258 | borderRadius: BorderRadius.circular(14), 259 | boxShadow: [ 260 | BoxShadow( 261 | color: Colors.blue.withOpacity(0.0), 262 | blurRadius: 0, 263 | spreadRadius: 12) 264 | ]), 265 | child: AnimatedContainer( 266 | duration: Duration(milliseconds: 150), 267 | decoration: decoration, 268 | padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), 269 | child: DefaultTextStyle.merge( 270 | style: TextStyle( 271 | fontSize: 13, 272 | color: state.enabled ? Colors.black87 : Colors.black45, 273 | fontWeight: FontWeight.w500, 274 | ), 275 | child: ConstrainedBox( 276 | constraints: BoxConstraints(minWidth: 50), 277 | child: Center(child: child), 278 | ), 279 | ), 280 | ), 281 | ); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /lib/widgets/intrinsic_limited_box.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | // LimitedBox that also limits intrinsic width 7 | class IntrinsicLimitedBox extends LimitedBox { 8 | const IntrinsicLimitedBox({ 9 | Key? key, 10 | double maxWidth = double.infinity, 11 | double maxHeight = double.infinity, 12 | Widget? child, 13 | }) : super(key: key, maxWidth: maxWidth, maxHeight: maxWidth, child: child); 14 | 15 | @override 16 | RenderLimitedBox createRenderObject(BuildContext context) { 17 | return RenderIntrinsicLimitedBox( 18 | maxWidth: maxWidth, 19 | maxHeight: maxHeight, 20 | ); 21 | } 22 | } 23 | 24 | class RenderIntrinsicLimitedBox extends RenderLimitedBox { 25 | RenderIntrinsicLimitedBox({ 26 | RenderBox? child, 27 | double maxWidth = double.infinity, 28 | double maxHeight = double.infinity, 29 | }) : super(child: child, maxWidth: maxWidth, maxHeight: maxHeight); 30 | 31 | @override 32 | double computeMaxIntrinsicWidth(double height) { 33 | return min(super.computeMaxIntrinsicWidth(height), maxWidth); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../util.dart'; 3 | import 'intrinsic_limited_box.dart'; 4 | 5 | class PageContainer extends StatelessWidget { 6 | const PageContainer({ 7 | Key? key, 8 | required this.child, 9 | }) : super(key: key); 10 | 11 | final Widget child; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | padding: EdgeInsets.all(25), 17 | color: Colors.grey.shade100, 18 | child: DefaultTextStyle.merge( 19 | style: TextStyle(color: Colors.grey[900]), child: child), 20 | ); 21 | } 22 | } 23 | 24 | class PageHeader extends StatelessWidget { 25 | const PageHeader({ 26 | Key? key, 27 | required this.child, 28 | }) : super(key: key); 29 | 30 | final Widget child; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Container( 35 | decoration: BoxDecoration( 36 | border: Border(bottom: BorderSide(color: Colors.red, width: 2))), 37 | padding: EdgeInsets.only(bottom: 10), 38 | child: DefaultTextStyle.merge( 39 | style: TextStyle(fontSize: 20), 40 | child: child, 41 | ), 42 | ); 43 | } 44 | } 45 | 46 | class PageSourceLocation extends StatelessWidget { 47 | const PageSourceLocation({ 48 | Key? key, 49 | required this.locations, 50 | }) : super(key: key); 51 | 52 | final List locations; 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return Padding( 57 | padding: const EdgeInsets.symmetric(vertical: 10), 58 | child: DefaultTextStyle.merge( 59 | style: TextStyle(fontSize: 13), 60 | child: Row( 61 | mainAxisSize: MainAxisSize.min, 62 | crossAxisAlignment: CrossAxisAlignment.start, 63 | children: [ 64 | Text( 65 | 'source location: ', 66 | style: TextStyle(color: Colors.black54), 67 | ), 68 | Column( 69 | crossAxisAlignment: CrossAxisAlignment.start, 70 | children: locations 71 | .map((e) => SelectableText( 72 | e, 73 | autofocus: false, 74 | )) 75 | .toList(), 76 | ), 77 | ], 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | 84 | class PageBlurb extends StatelessWidget { 85 | const PageBlurb({ 86 | Key? key, 87 | required this.paragraphs, 88 | }) : super(key: key); 89 | 90 | final List paragraphs; 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return Padding( 95 | padding: const EdgeInsets.only(bottom: 20), 96 | child: IntrinsicLimitedBox( 97 | maxWidth: 400, 98 | child: DefaultTextStyle.merge( 99 | style: TextStyle(fontSize: 13.5), 100 | child: Column( 101 | mainAxisSize: MainAxisSize.min, 102 | crossAxisAlignment: CrossAxisAlignment.start, 103 | children: paragraphs 104 | .map((e) => Text(e)) 105 | .intersperse(SizedBox( 106 | height: 8, 107 | )) 108 | .toList(), 109 | ), 110 | ), 111 | ), 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/widgets/veil.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class Veil extends StatefulWidget { 4 | Veil({required this.child}); 5 | 6 | final Widget child; 7 | 8 | @override 9 | State createState() { 10 | return _VeilState(); 11 | } 12 | 13 | static bool visible(BuildContext context) { 14 | final state = context 15 | .dependOnInheritedWidgetOfExactType<_VeilInheritedWidget>() 16 | ?.veilState; 17 | return (state?._absorbCount ?? 0) > 0; 18 | } 19 | 20 | static Future show( 21 | BuildContext context, Future Function() callback) async { 22 | final state = context 23 | .dependOnInheritedWidgetOfExactType<_VeilInheritedWidget>()! 24 | .veilState; 25 | state.enable(); 26 | try { 27 | return await callback(); 28 | } finally { 29 | state.disable(); 30 | } 31 | } 32 | } 33 | 34 | class _VeilState extends State { 35 | @override 36 | Widget build(BuildContext context) { 37 | return _VeilInheritedWidget( 38 | veilState: this, 39 | child: AbsorbPointer( 40 | absorbing: _absorbCount > 0, 41 | child: widget.child, 42 | ), 43 | ); 44 | } 45 | 46 | void enable() { 47 | ++_absorbCount; 48 | if (_absorbCount == 1) { 49 | setState(() {}); 50 | } 51 | } 52 | 53 | void disable() { 54 | --_absorbCount; 55 | if (_absorbCount == 0) { 56 | setState(() {}); 57 | } 58 | } 59 | 60 | int _absorbCount = 0; 61 | } 62 | 63 | class _VeilInheritedWidget extends InheritedWidget { 64 | _VeilInheritedWidget({ 65 | required Widget child, 66 | required this.veilState, 67 | }) : super(child: child); 68 | 69 | final _VeilState veilState; 70 | 71 | @override 72 | bool updateShouldNotify(covariant InheritedWidget oldWidget) { 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | cupertino_icons: 40 | dependency: "direct main" 41 | description: 42 | name: cupertino_icons 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.0.5" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.3.1" 53 | ffi: 54 | dependency: transitive 55 | description: 56 | name: ffi 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.1" 60 | file: 61 | dependency: transitive 62 | description: 63 | name: file 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "6.1.4" 67 | flutter: 68 | dependency: "direct main" 69 | description: flutter 70 | source: sdk 71 | version: "0.0.0" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | flutter_web_plugins: 78 | dependency: transitive 79 | description: flutter 80 | source: sdk 81 | version: "0.0.0" 82 | http: 83 | dependency: transitive 84 | description: 85 | name: http 86 | url: "https://pub.dartlang.org" 87 | source: hosted 88 | version: "0.13.5" 89 | http_parser: 90 | dependency: transitive 91 | description: 92 | name: http_parser 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "4.0.2" 96 | js: 97 | dependency: transitive 98 | description: 99 | name: js 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "0.6.4" 103 | matcher: 104 | dependency: transitive 105 | description: 106 | name: matcher 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "0.12.12" 110 | material_color_utilities: 111 | dependency: transitive 112 | description: 113 | name: material_color_utilities 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "0.1.5" 117 | meta: 118 | dependency: transitive 119 | description: 120 | name: meta 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.8.0" 124 | nativeshell: 125 | dependency: "direct main" 126 | description: 127 | name: nativeshell 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.1.16" 131 | package_info_plus: 132 | dependency: "direct main" 133 | description: 134 | name: package_info_plus 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.4.3+1" 138 | package_info_plus_linux: 139 | dependency: transitive 140 | description: 141 | name: package_info_plus_linux 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.0.5" 145 | package_info_plus_macos: 146 | dependency: transitive 147 | description: 148 | name: package_info_plus_macos 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.3.0" 152 | package_info_plus_platform_interface: 153 | dependency: transitive 154 | description: 155 | name: package_info_plus_platform_interface 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.0.2" 159 | package_info_plus_web: 160 | dependency: transitive 161 | description: 162 | name: package_info_plus_web 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.0.6" 166 | package_info_plus_windows: 167 | dependency: transitive 168 | description: 169 | name: package_info_plus_windows 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.1.0" 173 | path: 174 | dependency: "direct main" 175 | description: 176 | name: path 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "1.8.2" 180 | path_provider: 181 | dependency: "direct main" 182 | description: 183 | name: path_provider 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "2.0.11" 187 | path_provider_android: 188 | dependency: transitive 189 | description: 190 | name: path_provider_android 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "2.0.22" 194 | path_provider_ios: 195 | dependency: transitive 196 | description: 197 | name: path_provider_ios 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "2.0.11" 201 | path_provider_linux: 202 | dependency: transitive 203 | description: 204 | name: path_provider_linux 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "2.1.7" 208 | path_provider_macos: 209 | dependency: transitive 210 | description: 211 | name: path_provider_macos 212 | url: "https://pub.dartlang.org" 213 | source: hosted 214 | version: "2.0.6" 215 | path_provider_platform_interface: 216 | dependency: transitive 217 | description: 218 | name: path_provider_platform_interface 219 | url: "https://pub.dartlang.org" 220 | source: hosted 221 | version: "2.0.5" 222 | path_provider_windows: 223 | dependency: transitive 224 | description: 225 | name: path_provider_windows 226 | url: "https://pub.dartlang.org" 227 | source: hosted 228 | version: "2.1.3" 229 | pedantic: 230 | dependency: "direct main" 231 | description: 232 | name: pedantic 233 | url: "https://pub.dartlang.org" 234 | source: hosted 235 | version: "1.11.1" 236 | platform: 237 | dependency: transitive 238 | description: 239 | name: platform 240 | url: "https://pub.dartlang.org" 241 | source: hosted 242 | version: "3.1.0" 243 | plugin_platform_interface: 244 | dependency: transitive 245 | description: 246 | name: plugin_platform_interface 247 | url: "https://pub.dartlang.org" 248 | source: hosted 249 | version: "2.1.3" 250 | process: 251 | dependency: transitive 252 | description: 253 | name: process 254 | url: "https://pub.dartlang.org" 255 | source: hosted 256 | version: "4.2.4" 257 | shared_preferences: 258 | dependency: "direct main" 259 | description: 260 | name: shared_preferences 261 | url: "https://pub.dartlang.org" 262 | source: hosted 263 | version: "2.0.15" 264 | shared_preferences_android: 265 | dependency: transitive 266 | description: 267 | name: shared_preferences_android 268 | url: "https://pub.dartlang.org" 269 | source: hosted 270 | version: "2.0.14" 271 | shared_preferences_ios: 272 | dependency: transitive 273 | description: 274 | name: shared_preferences_ios 275 | url: "https://pub.dartlang.org" 276 | source: hosted 277 | version: "2.1.1" 278 | shared_preferences_linux: 279 | dependency: transitive 280 | description: 281 | name: shared_preferences_linux 282 | url: "https://pub.dartlang.org" 283 | source: hosted 284 | version: "2.1.2" 285 | shared_preferences_macos: 286 | dependency: transitive 287 | description: 288 | name: shared_preferences_macos 289 | url: "https://pub.dartlang.org" 290 | source: hosted 291 | version: "2.0.5" 292 | shared_preferences_platform_interface: 293 | dependency: transitive 294 | description: 295 | name: shared_preferences_platform_interface 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "2.1.0" 299 | shared_preferences_web: 300 | dependency: transitive 301 | description: 302 | name: shared_preferences_web 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "2.0.4" 306 | shared_preferences_windows: 307 | dependency: transitive 308 | description: 309 | name: shared_preferences_windows 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "2.1.2" 313 | sky_engine: 314 | dependency: transitive 315 | description: flutter 316 | source: sdk 317 | version: "0.0.99" 318 | source_span: 319 | dependency: transitive 320 | description: 321 | name: source_span 322 | url: "https://pub.dartlang.org" 323 | source: hosted 324 | version: "1.9.0" 325 | stack_trace: 326 | dependency: transitive 327 | description: 328 | name: stack_trace 329 | url: "https://pub.dartlang.org" 330 | source: hosted 331 | version: "1.10.0" 332 | stream_channel: 333 | dependency: transitive 334 | description: 335 | name: stream_channel 336 | url: "https://pub.dartlang.org" 337 | source: hosted 338 | version: "2.1.0" 339 | string_scanner: 340 | dependency: transitive 341 | description: 342 | name: string_scanner 343 | url: "https://pub.dartlang.org" 344 | source: hosted 345 | version: "1.1.1" 346 | term_glyph: 347 | dependency: transitive 348 | description: 349 | name: term_glyph 350 | url: "https://pub.dartlang.org" 351 | source: hosted 352 | version: "1.2.1" 353 | test_api: 354 | dependency: transitive 355 | description: 356 | name: test_api 357 | url: "https://pub.dartlang.org" 358 | source: hosted 359 | version: "0.4.12" 360 | typed_data: 361 | dependency: transitive 362 | description: 363 | name: typed_data 364 | url: "https://pub.dartlang.org" 365 | source: hosted 366 | version: "1.3.1" 367 | url_launcher: 368 | dependency: "direct main" 369 | description: 370 | name: url_launcher 371 | url: "https://pub.dartlang.org" 372 | source: hosted 373 | version: "6.1.7" 374 | url_launcher_android: 375 | dependency: transitive 376 | description: 377 | name: url_launcher_android 378 | url: "https://pub.dartlang.org" 379 | source: hosted 380 | version: "6.0.22" 381 | url_launcher_ios: 382 | dependency: transitive 383 | description: 384 | name: url_launcher_ios 385 | url: "https://pub.dartlang.org" 386 | source: hosted 387 | version: "6.0.17" 388 | url_launcher_linux: 389 | dependency: transitive 390 | description: 391 | name: url_launcher_linux 392 | url: "https://pub.dartlang.org" 393 | source: hosted 394 | version: "3.0.1" 395 | url_launcher_macos: 396 | dependency: transitive 397 | description: 398 | name: url_launcher_macos 399 | url: "https://pub.dartlang.org" 400 | source: hosted 401 | version: "3.0.1" 402 | url_launcher_platform_interface: 403 | dependency: transitive 404 | description: 405 | name: url_launcher_platform_interface 406 | url: "https://pub.dartlang.org" 407 | source: hosted 408 | version: "2.1.1" 409 | url_launcher_web: 410 | dependency: transitive 411 | description: 412 | name: url_launcher_web 413 | url: "https://pub.dartlang.org" 414 | source: hosted 415 | version: "2.0.13" 416 | url_launcher_windows: 417 | dependency: transitive 418 | description: 419 | name: url_launcher_windows 420 | url: "https://pub.dartlang.org" 421 | source: hosted 422 | version: "3.0.1" 423 | vector_math: 424 | dependency: transitive 425 | description: 426 | name: vector_math 427 | url: "https://pub.dartlang.org" 428 | source: hosted 429 | version: "2.1.2" 430 | win32: 431 | dependency: transitive 432 | description: 433 | name: win32 434 | url: "https://pub.dartlang.org" 435 | source: hosted 436 | version: "3.1.3" 437 | xdg_directories: 438 | dependency: transitive 439 | description: 440 | name: xdg_directories 441 | url: "https://pub.dartlang.org" 442 | source: hosted 443 | version: "0.2.0+3" 444 | sdks: 445 | dart: ">=2.17.0 <3.0.0" 446 | flutter: ">=3.0.0" 447 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nativeshell_examples 2 | description: NativeShell Examples 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0-214.0.dev <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | pedantic: ^1.9.2 27 | path: ^1.7.0 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.0 32 | nativeshell: ^0.1.16 33 | url_launcher: ^6.0.6 34 | path_provider: ^2.0.2 35 | package_info_plus: ^1.0.1 36 | shared_preferences: ^2.0.6 37 | 38 | dev_dependencies: 39 | flutter_test: 40 | sdk: flutter 41 | 42 | # For information on the generic Dart part of this file, see the 43 | # following page: https://dart.dev/tools/pub/pubspec 44 | 45 | # The following section is specific to Flutter. 46 | flutter: 47 | 48 | # The following line ensures that the Material Icons font is 49 | # included with your application, so that you can use the icons in 50 | # the material Icons class. 51 | uses-material-design: true 52 | 53 | # To add assets to your application, add an assets section, like this: 54 | # assets: 55 | # - images/a_dot_burr.jpeg 56 | # - images/a_dot_ham.jpeg 57 | 58 | # An image asset can refer to one or more resolution-specific "variants", see 59 | # https://flutter.dev/assets-and-images/#resolution-aware. 60 | 61 | # For details regarding adding assets from package dependencies, see 62 | # https://flutter.dev/assets-and-images/#from-packages 63 | 64 | # To add custom fonts to your application, add a fonts section here, 65 | # in this "flutter" section. Each entry in this list should have a 66 | # "family" key with the font family name, and a "fonts" key with a 67 | # list giving the asset and other descriptors for the font. For 68 | # example: 69 | # fonts: 70 | # - family: Schyler 71 | # fonts: 72 | # - asset: fonts/Schyler-Regular.ttf 73 | # - asset: fonts/Schyler-Italic.ttf 74 | # style: italic 75 | # - family: Trajan Pro 76 | # fonts: 77 | # - asset: fonts/TrajanPro.ttf 78 | # - asset: fonts/TrajanPro_Bold.ttf 79 | # weight: 700 80 | # 81 | # For details regarding fonts from package dependencies, 82 | # see https://flutter.dev/custom-fonts/#from-packages 83 | -------------------------------------------------------------------------------- /resources/mac_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeshell/examples/186ae5c9e3ba9e9793f0e238c36cfdcc80287b57/resources/mac_icon.icns -------------------------------------------------------------------------------- /src/file_open_dialog.rs: -------------------------------------------------------------------------------- 1 | use nativeshell::shell::{MethodCallHandler, MethodChannel}; 2 | pub use nativeshell::{ 3 | codec::{value::from_value, MethodCall, MethodCallReply, Value}, 4 | shell::{Context, WindowHandle}, 5 | }; 6 | 7 | #[derive(serde::Deserialize, Debug)] 8 | #[serde(rename_all = "camelCase")] 9 | struct FileOpenRequest { 10 | parent_window: WindowHandle, 11 | } 12 | 13 | #[cfg(target_os = "macos")] 14 | #[path = "file_open_dialog_mac.rs"] 15 | mod platform; 16 | 17 | #[cfg(target_os = "windows")] 18 | #[path = "file_open_dialog_win.rs"] 19 | mod platform; 20 | 21 | #[cfg(target_os = "linux")] 22 | #[path = "file_open_dialog_linux.rs"] 23 | mod platform; 24 | 25 | pub struct FileOpenDialog { 26 | context: Context, 27 | } 28 | 29 | impl FileOpenDialog { 30 | pub fn new(context: Context) -> Self { 31 | Self { context } 32 | } 33 | 34 | pub fn register(self) -> MethodChannel { 35 | MethodChannel::new(self.context.clone(), "file_open_dialog_channel", self) 36 | } 37 | 38 | fn open_file_dialog(&self, request: FileOpenRequest, reply: MethodCallReply) { 39 | if let Some(context) = self.context.get() { 40 | let win = context 41 | .window_manager 42 | .borrow() 43 | .get_platform_window(request.parent_window); 44 | if let Some(win) = win { 45 | platform::open_file_dialog(win, &context, request, |name| { 46 | let value = match name { 47 | Some(name) => Value::String(name), 48 | None => Value::Null, 49 | }; 50 | reply.send(Ok(value)); 51 | }); 52 | } else { 53 | reply.send_error("no_window", Some("Platform window not found"), Value::Null); 54 | } 55 | } 56 | } 57 | } 58 | 59 | impl MethodCallHandler for FileOpenDialog { 60 | fn on_method_call( 61 | &mut self, 62 | call: MethodCall, 63 | reply: MethodCallReply, 64 | _engine: nativeshell::shell::EngineHandle, 65 | ) { 66 | match call.method.as_str() { 67 | "showFileOpenDialog" => { 68 | let request: FileOpenRequest = from_value(&call.args).unwrap(); 69 | self.open_file_dialog(request, reply); 70 | } 71 | _ => { 72 | reply.send_error("invalid_method", Some("Invalid method"), Value::Null); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/file_open_dialog_linux.rs: -------------------------------------------------------------------------------- 1 | use gtk::{ 2 | prelude::{DialogExt, DialogExtManual, FileChooserExt, GtkWindowExt}, 3 | FileChooserDialogBuilder, Window, 4 | }; 5 | use nativeshell::shell::ContextRef; 6 | 7 | use super::FileOpenRequest; 8 | 9 | pub(super) fn open_file_dialog( 10 | win: Window, 11 | context: &ContextRef, 12 | _request: FileOpenRequest, 13 | reply: F, 14 | ) where 15 | F: FnOnce(Option) + 'static, 16 | { 17 | let dialog = FileChooserDialogBuilder::new() 18 | .transient_for(&win) 19 | .modal(true) 20 | .action(gtk::FileChooserAction::Open) 21 | .build(); 22 | 23 | dialog.add_buttons(&[ 24 | ("Open", gtk::ResponseType::Ok), 25 | ("Cancel", gtk::ResponseType::Cancel), 26 | ]); 27 | 28 | // Platform messages will be processed while dialog is running so 29 | // make sure it is cheduled on next run loop turn 30 | context 31 | .run_loop 32 | .borrow() 33 | .schedule_now(move || { 34 | let res = dialog.run(); 35 | let res = match res { 36 | gtk::ResponseType::Ok => { 37 | let path = dialog.filename(); 38 | path.map(|p| p.to_string_lossy().into()) 39 | } 40 | _ => None::, 41 | }; 42 | dialog.close(); 43 | reply(res); 44 | }) 45 | .detach(); 46 | } 47 | -------------------------------------------------------------------------------- /src/file_open_dialog_mac.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | pub use block::ConcreteBlock; 4 | pub use cocoa::{ 5 | base::id, 6 | foundation::{NSArray, NSString, NSUInteger}, 7 | }; 8 | use nativeshell::shell::ContextRef; 9 | pub use objc::{ 10 | msg_send, 11 | rc::{autoreleasepool, StrongPtr}, 12 | }; 13 | 14 | use super::FileOpenRequest; 15 | 16 | fn from_nsstring(ns_string: id) -> String { 17 | use std::os::raw::c_char; 18 | use std::slice; 19 | unsafe { 20 | let bytes: *const c_char = msg_send![ns_string, UTF8String]; 21 | let bytes = bytes as *const u8; 22 | let len = NSString::len(ns_string); 23 | let bytes = slice::from_raw_parts(bytes, len); 24 | std::str::from_utf8(bytes).unwrap().into() 25 | } 26 | } 27 | 28 | pub(super) fn open_file_dialog( 29 | win: StrongPtr, 30 | _context: &ContextRef, 31 | _request: FileOpenRequest, 32 | reply: F, 33 | ) where 34 | F: FnOnce(Option) + 'static, 35 | { 36 | autoreleasepool(|| unsafe { 37 | let panel = StrongPtr::retain(msg_send![class!(NSOpenPanel), openPanel]); 38 | 39 | // We know that the callback will be called only once, but rust doesn't; 40 | let reply = RefCell::new(Some(reply)); 41 | 42 | let panel_copy = panel.clone(); 43 | let cb = move |response: NSUInteger| { 44 | let reply = reply.take(); 45 | if let Some(reply) = reply { 46 | if response == 1 { 47 | let urls: id = msg_send![*panel_copy, URLs]; 48 | if NSArray::count(urls) > 0 { 49 | let url = NSArray::objectAtIndex(urls, 0); 50 | let string: id = msg_send![url, absoluteString]; 51 | let path = from_nsstring(string); 52 | reply(Some(path)); 53 | return; 54 | } 55 | } 56 | reply(None); 57 | } 58 | }; 59 | 60 | let handler = ConcreteBlock::new(cb).copy(); 61 | let () = msg_send![*panel, beginSheetModalForWindow: win completionHandler:&*handler]; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/file_open_dialog_win.rs: -------------------------------------------------------------------------------- 1 | use std::{mem::size_of, ptr::null_mut}; 2 | 3 | use nativeshell::shell::ContextRef; 4 | pub use widestring::WideStr; 5 | use windows::Win32::{ 6 | Foundation::{HINSTANCE, HWND, LPARAM, PWSTR}, 7 | UI::Controls::Dialogs::{ 8 | GetOpenFileNameW, OPENFILENAMEW, OPEN_FILENAME_FLAGS, OPEN_FILENAME_FLAGS_EX, 9 | }, 10 | }; 11 | 12 | use super::FileOpenRequest; 13 | 14 | pub(super) fn open_file_dialog( 15 | win: isize, 16 | context: &ContextRef, 17 | _request: FileOpenRequest, 18 | reply: F, 19 | ) where 20 | F: FnOnce(Option) + 'static, 21 | { 22 | let cb = move || { 23 | let mut file = Vec::::new(); 24 | file.resize(4096, 0); 25 | 26 | let mut ofn = OPENFILENAMEW { 27 | lStructSize: size_of::() as u32, 28 | hwndOwner: HWND(win), 29 | hInstance: HINSTANCE(0), 30 | lpstrFilter: PWSTR::default(), 31 | lpstrCustomFilter: PWSTR::default(), 32 | nMaxCustFilter: 0, 33 | nFilterIndex: 0, 34 | lpstrFile: PWSTR(file.as_mut_ptr()), 35 | nMaxFile: file.len() as u32, 36 | lpstrFileTitle: PWSTR::default(), 37 | nMaxFileTitle: 0, 38 | lpstrInitialDir: PWSTR::default(), 39 | lpstrTitle: PWSTR::default(), 40 | Flags: OPEN_FILENAME_FLAGS(0), 41 | nFileOffset: 0, 42 | nFileExtension: 0, 43 | lpstrDefExt: PWSTR::default(), 44 | lCustData: LPARAM(0), 45 | lpfnHook: None, 46 | lpTemplateName: PWSTR::default(), 47 | pvReserved: null_mut(), 48 | dwReserved: 0, 49 | FlagsEx: OPEN_FILENAME_FLAGS_EX(0), 50 | }; 51 | 52 | let res = unsafe { GetOpenFileNameW(&mut ofn as *mut _).as_bool() }; 53 | if !res { 54 | reply(None); 55 | } else { 56 | let name = WideStr::from_slice(&file).to_string_lossy(); 57 | reply(Some(name)); 58 | } 59 | }; 60 | 61 | context.run_loop.borrow().schedule_now(cb).detach(); 62 | } 63 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use file_open_dialog::FileOpenDialog; 2 | use nativeshell::{ 3 | codec::Value, 4 | shell::{exec_bundle, register_observatory_listener, Context, ContextOptions}, 5 | }; 6 | use platform_channels::PlatformChannels; 7 | 8 | #[cfg(target_os = "macos")] 9 | #[macro_use] 10 | extern crate objc; 11 | 12 | mod file_open_dialog; 13 | mod platform_channels; 14 | 15 | nativeshell::include_flutter_plugins!(); 16 | 17 | fn main() { 18 | exec_bundle(); 19 | register_observatory_listener("nativeshell_examples".into()); 20 | 21 | env_logger::builder().format_timestamp(None).init(); 22 | 23 | let context = Context::new(ContextOptions { 24 | app_namespace: "NativeShellDemo".into(), 25 | flutter_plugins: flutter_get_plugins(), 26 | ..Default::default() 27 | }); 28 | 29 | let context = context.unwrap(); 30 | 31 | let _file_open_dialog = FileOpenDialog::new(context.weak()).register(); 32 | let _platform_channels = PlatformChannels::new(context.weak()).register(); 33 | 34 | context 35 | .window_manager 36 | .borrow_mut() 37 | .create_window(Value::Null, None) 38 | .unwrap(); 39 | 40 | context.run_loop.borrow().run(); 41 | } 42 | -------------------------------------------------------------------------------- /src/platform_channels.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use nativeshell::{ 4 | codec::{MethodCall, MethodCallReply, Value}, 5 | shell::{Context, EngineHandle, MethodCallHandler, MethodChannel}, 6 | util::Capsule, 7 | }; 8 | 9 | pub struct PlatformChannels { 10 | context: Context, 11 | } 12 | 13 | impl PlatformChannels { 14 | pub fn new(context: Context) -> Self { 15 | Self { context } 16 | } 17 | 18 | pub fn register(self) -> MethodChannel { 19 | MethodChannel::new(self.context.clone(), "example_channel", self) 20 | } 21 | } 22 | 23 | impl MethodCallHandler for PlatformChannels { 24 | fn on_method_call( 25 | &mut self, 26 | call: MethodCall, 27 | reply: MethodCallReply, 28 | _engine: EngineHandle, 29 | ) { 30 | match call.method.as_str() { 31 | "echo" => { 32 | reply.send_ok(call.args); 33 | } 34 | "backgroundTask" => { 35 | // reply is not thread safe and can not be sent between threads directly; 36 | // use capsule to move it between threads 37 | let mut reply = Capsule::new(reply); 38 | 39 | let sender = self.context.get().unwrap().run_loop.borrow().new_sender(); 40 | thread::spawn(move || { 41 | // simulate long running task on background thread 42 | thread::sleep(Duration::from_secs(4)); 43 | let value = 3.141592; 44 | // jump back to platform thread to send the reply 45 | sender.send(move || { 46 | // capsule will only let us take the stored value on thread where 47 | // it was created 48 | let reply = reply.take().unwrap(); 49 | reply.send_ok(Value::F64(value)); 50 | }); 51 | }); 52 | } 53 | _ => {} 54 | } 55 | } 56 | 57 | // optionally you can get notifications when an engine gets destroyed, which 58 | // might be useful for clean-up 59 | fn on_engine_destroyed(&mut self, _engine: EngineHandle) {} 60 | } 61 | --------------------------------------------------------------------------------