├── .cargo └── config.toml ├── .dockerignore ├── .github └── funding.yml ├── .gitignore ├── .gitmodules ├── .refloat └── config.js ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── build.rs ├── changelog.md ├── chromium ├── .gclient └── patches │ ├── chromium │ ├── 0001-Add-Carbonyl-library.patch │ ├── 0002-Add-Carbonyl-service.patch │ ├── 0003-Setup-shared-software-rendering-surface.patch │ ├── 0004-Setup-browser-default-settings.patch │ ├── 0005-Remove-some-debug-assertions.patch │ ├── 0006-Setup-display-DPI.patch │ ├── 0007-Disable-text-effects.patch │ ├── 0008-Fix-text-layout.patch │ ├── 0009-Bridge-browser-into-Carbonyl-library.patch │ ├── 0010-Conditionally-enable-text-rendering.patch │ ├── 0011-Rename-carbonyl-Renderer-to-carbonyl-Bridge.patch │ ├── 0012-Create-separate-bridge-for-Blink.patch │ ├── 0013-Refactor-rendering-bridge.patch │ └── 0014-Move-Skia-text-rendering-control-to-bridge.patch │ ├── skia │ ├── 0001-Disable-text-rendering.patch │ └── 0002-Export-some-private-APIs.patch │ └── webrtc │ └── 0001-Disable-GIO-on-Linux.patch ├── cliff.toml ├── license.md ├── package.json ├── readme.md ├── scripts ├── build.sh ├── changelog.sh ├── copy-binaries.sh ├── docker-build.sh ├── docker-push.sh ├── env.sh ├── gclient.sh ├── gn.sh ├── npm-package.mjs ├── npm-package.sh ├── npm-publish.sh ├── patches.sh ├── platform-triple.sh ├── release.sh ├── restore-mtime.sh ├── run.sh ├── runtime-hash.sh ├── runtime-pull.sh └── runtime-push.sh └── src ├── browser.rs ├── browser ├── BUILD.gn ├── args.gn ├── bridge.cc ├── bridge.h ├── bridge.rs ├── carbonyl.mojom ├── export.h ├── host_display_client.cc ├── host_display_client.h ├── render_service_impl.cc ├── render_service_impl.h ├── renderer.cc └── renderer.h ├── cli.rs ├── cli ├── cli.rs ├── program.rs └── usage.txt ├── gfx.rs ├── gfx ├── color.rs ├── point.rs ├── rect.rs ├── size.rs └── vector.rs ├── input.rs ├── input ├── dcs.rs ├── dcs │ ├── control_flow.rs │ ├── parser.rs │ ├── resource.rs │ └── status.rs ├── keyboard.rs ├── listen.rs ├── mouse.rs ├── parser.rs └── tty.rs ├── lib.rs ├── output.rs ├── output ├── cell.rs ├── frame_sync.rs ├── kd_tree.rs ├── painter.rs ├── quad.rs ├── quantizer.rs ├── render_thread.rs ├── renderer.rs ├── window.rs └── xterm.rs ├── ui.rs ├── ui └── navigation.rs ├── utils.rs └── utils ├── four_bits.rs ├── log.rs └── try_block.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target-dir = "build" 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /build 2 | !/build/browser 3 | /chromium 4 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: fathyb 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | 4 | /.ccache 5 | /.git_cache 6 | /.vscode 7 | /build 8 | /chromium/* 9 | !/chromium/.gclient 10 | !/chromium/depot_tools 11 | !/chromium/patches 12 | /packages/*/build 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "chromium/depot_tools"] 2 | path = chromium/depot_tools 3 | url = https://chromium.googlesource.com/chromium/tools/depot_tools.git 4 | -------------------------------------------------------------------------------- /.refloat/config.js: -------------------------------------------------------------------------------- 1 | import { commit } from "refloat"; 2 | import docker from "github.com/refloat-plugins/docker"; 3 | 4 | import pkg from "../package.json"; 5 | 6 | const { version } = JSON.parse(pkg); 7 | const triple = (platform, arch) => `${archs[arch]}-${platforms[platform]}`; 8 | const lib = (platform, arch) => 9 | `build/${triple(platform, arch)}/release/libcarbonyl.${sharedLib[platform]}`; 10 | const sharedLib = { 11 | macos: "dylib", 12 | linux: "so", 13 | }; 14 | const platforms = { 15 | macos: "apple-darwin", 16 | linux: "unknown-linux-gnu", 17 | }; 18 | const archs = { 19 | arm64: "aarch64", 20 | amd64: "x86_64", 21 | }; 22 | 23 | export const jobs = ["macos", "linux"].flatMap((platform) => { 24 | return [ 25 | { 26 | name: `Build runtime (${platform})`, 27 | agent: { tags: ["chromium-src", platform] }, 28 | steps: [ 29 | ...["arm64", "amd64"].map((arch) => ({ 30 | import: { workspace: `core-${triple(platform, arch)}` }, 31 | })), 32 | { 33 | parallel: ["arm64", "amd64"].map((arch) => ({ 34 | name: `Fetch pre-built runtime for ${arch}`, 35 | command: ` 36 | if scripts/runtime-pull.sh ${arch}; then 37 | touch skip-build-${arch} 38 | cp \\ 39 | ${lib(platform, arch)} \\ 40 | build/pre-built/${triple(platform, arch)} 41 | fi 42 | `, 43 | })), 44 | }, 45 | { 46 | name: "Fetch Chromium", 47 | command: ` 48 | if [ -z "$CHROMIUM_ROOT" ]; then 49 | echo "Chromium build environment not setup" 50 | 51 | exit 2 52 | fi 53 | 54 | if [ ! -f skip-build-arm64 ] || [ ! -f skip-build-amd64 ]; then 55 | cp chromium/.gclient "$CHROMIUM_ROOT" 56 | 57 | scripts/gclient.sh sync 58 | scripts/patches.sh apply 59 | 60 | rm -rf "$CHROMIUM_ROOT/src/carbonyl" 61 | mkdir "$CHROMIUM_ROOT/src/carbonyl" 62 | ln -s "$(pwd)/src" "$CHROMIUM_ROOT/src/carbonyl/src" 63 | ln -s "$(pwd)/build" "$CHROMIUM_ROOT/src/carbonyl/build" 64 | fi 65 | `, 66 | }, 67 | { 68 | parallel: ["arm64", "amd64"].map((arch) => { 69 | const target = 70 | platform === "linux" && arch === "amd64" ? "Default" : arch; 71 | 72 | return { 73 | serial: [ 74 | { 75 | name: `Build Chromium (${arch})`, 76 | command: ` 77 | if [ ! -f skip-build-${arch} ]; then 78 | scripts/build.sh ${target} ${arch} 79 | scripts/copy-binaries.sh ${target} ${arch} 80 | fi 81 | `, 82 | env: { 83 | MACOSX_DEPLOYMENT_TARGET: "10.13", 84 | CARBONYL_SKIP_CARGO_BUILD: "true", 85 | AR_AARCH64_UNKNOWN_LINUX_GNU: "aarch64-linux-gnu-ar", 86 | CC_AARCH64_UNKNOWN_LINUX_GNU: "aarch64-linux-gnu-gcc", 87 | CXX_AARCH64_UNKNOWN_LINUX_GNU: "aarch64-linux-gnu-g++", 88 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 89 | "aarch64-linux-gnu-gcc", 90 | AR_X86_64_UNKNOWN_LINUX_GNU: "x86_64-linux-gnu-ar", 91 | CC_X86_64_UNKNOWN_LINUX_GNU: "x86_64-linux-gnu-gcc", 92 | CXX_X86_64_UNKNOWN_LINUX_GNU: "x86_64-linux-gnu-g++", 93 | CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: 94 | "x86_64-linux-gnu-gcc", 95 | }, 96 | }, 97 | 98 | { 99 | parallel: [ 100 | { 101 | name: `Push binaries to CDN (${arch})`, 102 | command: ` 103 | if [ ! -f skip-build-${arch} ]; then 104 | scripts/runtime-push.sh ${arch} 105 | fi 106 | `, 107 | env: { 108 | CDN_ACCESS_KEY_ID: { secret: true }, 109 | CDN_SECRET_ACCESS_KEY: { secret: true }, 110 | }, 111 | }, 112 | { 113 | export: { 114 | workspace: `runtime-${triple(platform, arch)}`, 115 | path: `build/pre-built/${triple(platform, arch)}`, 116 | }, 117 | }, 118 | ], 119 | }, 120 | ], 121 | }; 122 | }), 123 | }, 124 | ], 125 | }, 126 | ...["arm64", "amd64"].flatMap((arch) => { 127 | const triple = `${archs[arch]}-${platforms[platform]}`; 128 | const lib = `build/${triple}/release/libcarbonyl.${sharedLib[platform]}`; 129 | 130 | return [ 131 | { 132 | name: `Build core (${platform}/${arch})`, 133 | docker: 134 | platform === "linux" 135 | ? { 136 | image: "fathyb/rust-cross", 137 | cache: ["/usr/local/cargo/registry"], 138 | } 139 | : undefined, 140 | agent: { tags: platform === "linux" ? ["docker"] : ["macos"] }, 141 | steps: [ 142 | { 143 | name: "Install Rust toolchain", 144 | command: `rustup target add ${triple}`, 145 | }, 146 | { 147 | name: "Build core library", 148 | command: `cargo build --target ${triple} --release`, 149 | env: { MACOSX_DEPLOYMENT_TARGET: "10.13" }, 150 | }, 151 | { 152 | name: "Set core library install name", 153 | command: 154 | platform === "macos" 155 | ? `install_name_tool -id @executable_path/libcarbonyl.dylib ${lib}` 156 | : "echo not necessary", 157 | }, 158 | { 159 | export: { 160 | workspace: `core-${triple}`, 161 | path: "build/*/release/*.{dylib,so,dll}", 162 | }, 163 | }, 164 | ], 165 | }, 166 | { 167 | name: `Package (${platform}/${arch})`, 168 | docker: "fathyb/rust-cross", 169 | agent: { tags: ["docker"] }, 170 | steps: [ 171 | { 172 | import: { workspace: `runtime-${triple}` }, 173 | }, 174 | { 175 | name: "Zip binaries", 176 | command: ` 177 | mkdir build/zip 178 | cp -r build/pre-built/${triple} build/zip/carbonyl-${version} 179 | 180 | cd build/zip 181 | zip -r package.zip carbonyl-${version} 182 | `, 183 | }, 184 | { 185 | export: { 186 | artifact: { 187 | name: `carbonyl.${platform}-${arch}.zip`, 188 | path: "build/zip/package.zip", 189 | }, 190 | }, 191 | }, 192 | ], 193 | }, 194 | ]; 195 | }), 196 | ]; 197 | }); 198 | 199 | if (commit.defaultBranch) { 200 | jobs.push( 201 | { 202 | name: "Publish to Docker", 203 | agent: { tags: ["carbonyl-publish"] }, 204 | docker: "fathyb/rust-cross", 205 | steps: [ 206 | { 207 | serial: ["arm64", "amd64"].map((arch) => ({ 208 | import: { workspace: `runtime-${triple("linux", arch)}` }, 209 | })), 210 | }, 211 | { 212 | parallel: ["arm64", "amd64"].map((arch) => ({ 213 | serial: [ 214 | { 215 | name: `Build ${arch} image`, 216 | command: `scripts/docker-build.sh ${arch}`, 217 | }, 218 | ], 219 | })), 220 | }, 221 | { 222 | name: "Publish images to DockerHub", 223 | command: "scripts/docker-push.sh next", 224 | using: docker.login({ 225 | username: { secret: "DOCKER_PUBLISH_USERNAME" }, 226 | password: { secret: "DOCKER_PUBLISH_TOKEN" }, 227 | }), 228 | }, 229 | ], 230 | }, 231 | { 232 | name: "Publish to npm", 233 | agent: { tags: ["carbonyl-publish"] }, 234 | docker: "node:18", 235 | steps: [ 236 | ...["macos", "linux"].flatMap((platform) => 237 | ["arm64", "amd64"].map((arch) => ({ 238 | import: { workspace: `runtime-${triple(platform, arch)}` }, 239 | })) 240 | ), 241 | { 242 | name: "Package", 243 | command: "scripts/npm-package.sh", 244 | }, 245 | { 246 | name: "Write npm token", 247 | env: { CARBONYL_NPM_PUBLISH_TOKEN: { secret: true } }, 248 | command: 249 | 'echo "//registry.npmjs.org/:_authToken=${CARBONYL_NPM_PUBLISH_TOKEN}" > ~/.npmrc', 250 | }, 251 | { 252 | parallel: ["amd64", "arm64"].flatMap((arch) => 253 | ["linux", "macos"].map((platform) => ({ 254 | name: `Publish ${platform}/${arch} package`, 255 | command: "scripts/npm-publish.sh --tag next", 256 | env: { 257 | CARBONYL_PUBLISH_ARCH: arch, 258 | CARBONYL_PUBLISH_PLATFORM: platform, 259 | }, 260 | })) 261 | ), 262 | }, 263 | { 264 | name: "Publish main package", 265 | command: "scripts/npm-publish.sh --tag next", 266 | }, 267 | ], 268 | } 269 | ); 270 | } 271 | -------------------------------------------------------------------------------- /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 = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bumpalo" 22 | version = "3.12.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 25 | 26 | [[package]] 27 | name = "carbonyl" 28 | version = "0.0.3" 29 | dependencies = [ 30 | "chrono", 31 | "libc", 32 | "unicode-segmentation", 33 | "unicode-width", 34 | ] 35 | 36 | [[package]] 37 | name = "cc" 38 | version = "1.0.79" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 41 | 42 | [[package]] 43 | name = "cfg-if" 44 | version = "1.0.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 47 | 48 | [[package]] 49 | name = "chrono" 50 | version = "0.4.23" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 53 | dependencies = [ 54 | "iana-time-zone", 55 | "js-sys", 56 | "num-integer", 57 | "num-traits", 58 | "time", 59 | "wasm-bindgen", 60 | "winapi", 61 | ] 62 | 63 | [[package]] 64 | name = "codespan-reporting" 65 | version = "0.11.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 68 | dependencies = [ 69 | "termcolor", 70 | "unicode-width", 71 | ] 72 | 73 | [[package]] 74 | name = "core-foundation-sys" 75 | version = "0.8.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 78 | 79 | [[package]] 80 | name = "cxx" 81 | version = "1.0.88" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8" 84 | dependencies = [ 85 | "cc", 86 | "cxxbridge-flags", 87 | "cxxbridge-macro", 88 | "link-cplusplus", 89 | ] 90 | 91 | [[package]] 92 | name = "cxx-build" 93 | version = "1.0.88" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8" 96 | dependencies = [ 97 | "cc", 98 | "codespan-reporting", 99 | "once_cell", 100 | "proc-macro2", 101 | "quote", 102 | "scratch", 103 | "syn", 104 | ] 105 | 106 | [[package]] 107 | name = "cxxbridge-flags" 108 | version = "1.0.88" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971" 111 | 112 | [[package]] 113 | name = "cxxbridge-macro" 114 | version = "1.0.88" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e" 117 | dependencies = [ 118 | "proc-macro2", 119 | "quote", 120 | "syn", 121 | ] 122 | 123 | [[package]] 124 | name = "iana-time-zone" 125 | version = "0.1.53" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 128 | dependencies = [ 129 | "android_system_properties", 130 | "core-foundation-sys", 131 | "iana-time-zone-haiku", 132 | "js-sys", 133 | "wasm-bindgen", 134 | "winapi", 135 | ] 136 | 137 | [[package]] 138 | name = "iana-time-zone-haiku" 139 | version = "0.1.1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 142 | dependencies = [ 143 | "cxx", 144 | "cxx-build", 145 | ] 146 | 147 | [[package]] 148 | name = "js-sys" 149 | version = "0.3.60" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 152 | dependencies = [ 153 | "wasm-bindgen", 154 | ] 155 | 156 | [[package]] 157 | name = "libc" 158 | version = "0.2.139" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 161 | 162 | [[package]] 163 | name = "link-cplusplus" 164 | version = "1.0.8" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 167 | dependencies = [ 168 | "cc", 169 | ] 170 | 171 | [[package]] 172 | name = "log" 173 | version = "0.4.17" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 176 | dependencies = [ 177 | "cfg-if", 178 | ] 179 | 180 | [[package]] 181 | name = "num-integer" 182 | version = "0.1.45" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 185 | dependencies = [ 186 | "autocfg", 187 | "num-traits", 188 | ] 189 | 190 | [[package]] 191 | name = "num-traits" 192 | version = "0.2.15" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 195 | dependencies = [ 196 | "autocfg", 197 | ] 198 | 199 | [[package]] 200 | name = "once_cell" 201 | version = "1.17.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 204 | 205 | [[package]] 206 | name = "proc-macro2" 207 | version = "1.0.50" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" 210 | dependencies = [ 211 | "unicode-ident", 212 | ] 213 | 214 | [[package]] 215 | name = "quote" 216 | version = "1.0.23" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 219 | dependencies = [ 220 | "proc-macro2", 221 | ] 222 | 223 | [[package]] 224 | name = "scratch" 225 | version = "1.0.3" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" 228 | 229 | [[package]] 230 | name = "syn" 231 | version = "1.0.107" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 234 | dependencies = [ 235 | "proc-macro2", 236 | "quote", 237 | "unicode-ident", 238 | ] 239 | 240 | [[package]] 241 | name = "termcolor" 242 | version = "1.2.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 245 | dependencies = [ 246 | "winapi-util", 247 | ] 248 | 249 | [[package]] 250 | name = "time" 251 | version = "0.1.45" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 254 | dependencies = [ 255 | "libc", 256 | "wasi", 257 | "winapi", 258 | ] 259 | 260 | [[package]] 261 | name = "unicode-ident" 262 | version = "1.0.6" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 265 | 266 | [[package]] 267 | name = "unicode-segmentation" 268 | version = "1.10.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" 271 | 272 | [[package]] 273 | name = "unicode-width" 274 | version = "0.1.10" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 277 | 278 | [[package]] 279 | name = "wasi" 280 | version = "0.10.0+wasi-snapshot-preview1" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 283 | 284 | [[package]] 285 | name = "wasm-bindgen" 286 | version = "0.2.83" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 289 | dependencies = [ 290 | "cfg-if", 291 | "wasm-bindgen-macro", 292 | ] 293 | 294 | [[package]] 295 | name = "wasm-bindgen-backend" 296 | version = "0.2.83" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 299 | dependencies = [ 300 | "bumpalo", 301 | "log", 302 | "once_cell", 303 | "proc-macro2", 304 | "quote", 305 | "syn", 306 | "wasm-bindgen-shared", 307 | ] 308 | 309 | [[package]] 310 | name = "wasm-bindgen-macro" 311 | version = "0.2.83" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 314 | dependencies = [ 315 | "quote", 316 | "wasm-bindgen-macro-support", 317 | ] 318 | 319 | [[package]] 320 | name = "wasm-bindgen-macro-support" 321 | version = "0.2.83" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 324 | dependencies = [ 325 | "proc-macro2", 326 | "quote", 327 | "syn", 328 | "wasm-bindgen-backend", 329 | "wasm-bindgen-shared", 330 | ] 331 | 332 | [[package]] 333 | name = "wasm-bindgen-shared" 334 | version = "0.2.83" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 337 | 338 | [[package]] 339 | name = "winapi" 340 | version = "0.3.9" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 343 | dependencies = [ 344 | "winapi-i686-pc-windows-gnu", 345 | "winapi-x86_64-pc-windows-gnu", 346 | ] 347 | 348 | [[package]] 349 | name = "winapi-i686-pc-windows-gnu" 350 | version = "0.4.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 353 | 354 | [[package]] 355 | name = "winapi-util" 356 | version = "0.1.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 359 | dependencies = [ 360 | "winapi", 361 | ] 362 | 363 | [[package]] 364 | name = "winapi-x86_64-pc-windows-gnu" 365 | version = "0.4.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 368 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "carbonyl" 3 | version = "0.0.3" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | libc = "0.2" 8 | unicode-width = "0.1.10" 9 | unicode-segmentation = "1.10.0" 10 | chrono = "0.4.23" 11 | 12 | [lib] 13 | name = "carbonyl" 14 | path = "src/lib.rs" 15 | crate-type = ["cdylib"] 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.67 AS cross-compile 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | zip g++-aarch64-linux-gnu g++-x86-64-linux-gnu libc6-dev-arm64-cross libc6-dev-amd64-cross && \ 6 | rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu && \ 7 | rustup toolchain install stable-aarch64-unknown-linux-gnu stable-x86_64-unknown-linux-gnu && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | ENV AR_AARCH64_UNKNOWN_LINUX_GNU=aarch64-linux-gnu-ar 11 | ENV CC_AARCH64_UNKNOWN_LINUX_GNU=aarch64-linux-gnu-gcc 12 | ENV CXX_AARCH64_UNKNOWN_LINUX_GNU=aarch64-linux-gnu-g++ 13 | ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc 14 | 15 | ENV AR_X86_64_UNKNOWN_LINUX_GNU=x86_64-linux-gnu-ar 16 | ENV CC_X86_64_UNKNOWN_LINUX_GNU=x86_64-linux-gnu-gcc 17 | ENV CXX_X86_64_UNKNOWN_LINUX_GNU=x86_64-linux-gnu-g++ 18 | ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc 19 | 20 | FROM debian:bullseye-slim 21 | 22 | RUN groupadd -r carbonyl && \ 23 | useradd -r -g carbonyl carbonyl && \ 24 | mkdir -p /carbonyl/data && \ 25 | chown -R carbonyl:carbonyl /carbonyl && \ 26 | apt-get update && \ 27 | apt-get install -y libasound2 libexpat1 libfontconfig1 libnss3 && \ 28 | rm -rf /var/lib/apt/lists/* 29 | 30 | USER carbonyl 31 | VOLUME /carbonyl/data 32 | ENV HOME=/carbonyl/data 33 | 34 | COPY . /carbonyl 35 | 36 | RUN /carbonyl/carbonyl --version 37 | 38 | ENTRYPOINT ["/carbonyl/carbonyl", "--no-sandbox", "--disable-dev-shm-usage", "--user-data-dir=/carbonyl/data"] 39 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[cfg(target_arch = "x86_64")] 4 | fn link_sysroot() { 5 | let sysroot_path = PathBuf::from("./chromium/src/build/linux/debian_bullseye_amd64-sysroot"); 6 | 7 | if sysroot_path.is_dir() { 8 | println!("cargo:rustc-link-search=chromium/src/build/linux/debian_bullseye_amd64-sysroot/lib/x86_64-linux-gnu"); 9 | println!("cargo:rustc-link-search=chromium/src/build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu"); 10 | 11 | println!( 12 | "cargo:rustc-link-arg=--sysroot=./chromium/src/build/linux/debian_bullseye_amd64-sysroot" 13 | ); 14 | } else { 15 | println!("cargo:warning={}", "x86_64 debian sysroot provided by chromium was not found!"); 16 | println!("cargo:warning={}", "carbonyl may fail to link against a proper libc!"); 17 | } 18 | } 19 | 20 | #[cfg(target_arch = "x86")] 21 | fn link_sysroot() { 22 | let sysroot_path = PathBuf::from("./chromium/src/build/linux/debian_bullseye_i386-sysroot"); 23 | 24 | if sysroot_path.is_dir() { 25 | println!("cargo:rustc-link-search=chromium/src/build/linux/debian_bullseye_i386-sysroot/lib/i386-linux-gnu"); 26 | println!("cargo:rustc-link-search=chromium/src/build/linux/debian_bullseye_i386-sysroot/usr/lib/i386-linux-gnu"); 27 | 28 | println!( 29 | "cargo:rustc-link-arg=--sysroot=./chromium/src/build/linux/debian_bullseye_i386-sysroot" 30 | ); 31 | } else { 32 | println!("cargo:warning={}", "x86 debian sysroot provided by chromium was not found!"); 33 | println!("cargo:warning={}", "carbonyl may fail to link against a proper libc!"); 34 | } 35 | } 36 | 37 | #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))] 38 | fn link_sysroot() { 39 | // Intentionally left blank. 40 | } 41 | 42 | fn main() { 43 | link_sysroot(); 44 | } 45 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.0.3] - 2023-02-18 6 | 7 | ### 🚀 Features 8 | 9 | - Add `--help` and `--version` ([#105](https://github.com/fathyb/carbonyl/issues/105)) 10 | - Add logo and description to `--help` ([#106](https://github.com/fathyb/carbonyl/issues/106)) 11 | - Use Cmd instead of Alt for navigation shortcuts ([#109](https://github.com/fathyb/carbonyl/issues/109)) 12 | - Enable h.264 support ([#103](https://github.com/fathyb/carbonyl/issues/103)) 13 | - Introduce quadrant rendering ([#120](https://github.com/fathyb/carbonyl/issues/120)) 14 | 15 | ### 🐛 Bug Fixes 16 | 17 | - Fix arguments parsing ([#108](https://github.com/fathyb/carbonyl/issues/108)) 18 | - Fix missing module error on npm package ([#113](https://github.com/fathyb/carbonyl/issues/113)) 19 | - Enable threaded compositing with bitmap mode 20 | - Fix idling CPU usage ([#126](https://github.com/fathyb/carbonyl/issues/126)) 21 | - Package proper library in binaries ([#127](https://github.com/fathyb/carbonyl/issues/127)) 22 | 23 | ### 📖 Documentation 24 | 25 | - Update download links 26 | - Fix commit_preprocessors url ([#102](https://github.com/fathyb/carbonyl/issues/102)) 27 | - Add `--rm` to Docker example ([#101](https://github.com/fathyb/carbonyl/issues/101)) 28 | 29 | ## [0.0.2] - 2023-02-09 30 | 31 | ### 🚀 Features 32 | 33 | - Better true color detection 34 | - Linux support 35 | - Xterm title 36 | - Hide stderr unless crash 37 | - Add `--debug` to print stderr on exit ([#23](https://github.com/fathyb/carbonyl/issues/23)) 38 | - Add navigation UI ([#86](https://github.com/fathyb/carbonyl/issues/86)) 39 | - Handle terminal resize ([#87](https://github.com/fathyb/carbonyl/issues/87)) 40 | 41 | ### 🐛 Bug Fixes 42 | 43 | - Parser fixes 44 | - Properly enter tab and return keys 45 | - Fix some special characters ([#35](https://github.com/fathyb/carbonyl/issues/35)) 46 | - Improve terminal size detection ([#36](https://github.com/fathyb/carbonyl/issues/36)) 47 | - Allow working directories that contain spaces ([#63](https://github.com/fathyb/carbonyl/issues/63)) 48 | - Do not use tags for checkout ([#64](https://github.com/fathyb/carbonyl/issues/64)) 49 | - Do not checkout nacl ([#79](https://github.com/fathyb/carbonyl/issues/79)) 50 | - Wrap zip files in carbonyl folder ([#88](https://github.com/fathyb/carbonyl/issues/88)) 51 | - Fix WebGL support on Linux ([#90](https://github.com/fathyb/carbonyl/issues/90)) 52 | - Fix initial freeze on Docker ([#91](https://github.com/fathyb/carbonyl/issues/91)) 53 | 54 | ### 📖 Documentation 55 | 56 | - Upload demo videos 57 | - Fix video layout 58 | - Fix a typo ([#1](https://github.com/fathyb/carbonyl/issues/1)) 59 | - Fix a typo `ie.` -> `i.e.` ([#9](https://github.com/fathyb/carbonyl/issues/9)) 60 | - Fix build instructions ([#15](https://github.com/fathyb/carbonyl/issues/15)) 61 | - Add ascii logo 62 | - Add comparisons ([#34](https://github.com/fathyb/carbonyl/issues/34)) 63 | - Add OS support ([#50](https://github.com/fathyb/carbonyl/issues/50)) 64 | - Add download link 65 | - Fix linux download links 66 | - Document shared library 67 | - Fix a typo (`know` -> `known`) ([#71](https://github.com/fathyb/carbonyl/issues/71)) 68 | - Add license 69 | 70 | ### Build 71 | 72 | - Various build system fixes ([#20](https://github.com/fathyb/carbonyl/issues/20)) 73 | 74 | -------------------------------------------------------------------------------- /chromium/.gclient: -------------------------------------------------------------------------------- 1 | solutions = [ 2 | { 3 | "name": "src", 4 | "url": "https://chromium.googlesource.com/chromium/src.git@111.0.5511.1", 5 | "managed": False, 6 | "custom_deps": {}, 7 | "custom_vars": { 8 | "use_rust": True, 9 | "checkout_pgo_profiles": True, 10 | "checkout_nacl": False, 11 | } 12 | }, 13 | ] 14 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0001-Add-Carbonyl-library.patch: -------------------------------------------------------------------------------- 1 | From 0ed9a390f25d73492ce1170ce229b95772fd458d Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:20:50 +0100 4 | Subject: [PATCH 01/14] Add Carbonyl library 5 | 6 | --- 7 | carbonyl/build | 1 + 8 | carbonyl/src | 1 + 9 | headless/BUILD.gn | 15 ++++++++++++++- 10 | 3 files changed, 16 insertions(+), 1 deletion(-) 11 | create mode 120000 carbonyl/build 12 | create mode 120000 carbonyl/src 13 | 14 | diff --git a/carbonyl/build b/carbonyl/build 15 | new file mode 120000 16 | index 0000000000000..44735d5866459 17 | --- /dev/null 18 | +++ b/carbonyl/build 19 | @@ -0,0 +1 @@ 20 | +../../../build 21 | \ No newline at end of file 22 | diff --git a/carbonyl/src b/carbonyl/src 23 | new file mode 120000 24 | index 0000000000000..dabb0e15a991e 25 | --- /dev/null 26 | +++ b/carbonyl/src 27 | @@ -0,0 +1 @@ 28 | +../../../src 29 | \ No newline at end of file 30 | diff --git a/headless/BUILD.gn b/headless/BUILD.gn 31 | index bfae1e3290de0..8018111ed9898 100644 32 | --- a/headless/BUILD.gn 33 | +++ b/headless/BUILD.gn 34 | @@ -453,6 +453,7 @@ component("headless_non_renderer") { 35 | "//build:branding_buildflags", 36 | "//build:branding_buildflags", 37 | "//build:chromeos_buildflags", 38 | + "//carbonyl/src/browser:carbonyl", 39 | "//components/cookie_config", 40 | "//components/crash/core/common:common", 41 | "//components/embedder_support", 42 | @@ -993,13 +994,25 @@ static_library("headless_shell_lib") { 43 | } 44 | 45 | executable("headless_shell") { 46 | + if (is_mac && !use_lld) { 47 | + ldflags = [ "-Wl,-no_compact_unwind" ] 48 | + } else if (is_linux) { 49 | + ldflags = [ 50 | + "-Wl,-rpath=\$ORIGIN/.", 51 | + "-Wl,-rpath-link=.", 52 | + ] 53 | + } 54 | + 55 | configs -= [ "//build/config/compiler:thinlto_optimize_default" ] 56 | configs += [ "//build/config/compiler:thinlto_optimize_max" ] 57 | 58 | sources = [ "app/headless_shell_main.cc" ] 59 | defines = [] 60 | 61 | - deps = [ ":headless_shell_lib" ] 62 | + deps = [ 63 | + ":headless_shell_lib", 64 | + "//carbonyl/src/browser:carbonyl", 65 | + ] 66 | 67 | if (!headless_use_embedded_resources) { 68 | data = [ 69 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0003-Setup-shared-software-rendering-surface.patch: -------------------------------------------------------------------------------- 1 | From eea9662f4ba08a655057143d320e4e692fd92469 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:24:29 +0100 4 | Subject: [PATCH 03/14] Setup shared software rendering surface 5 | 6 | --- 7 | components/viz/host/host_display_client.cc | 4 +++- 8 | components/viz/host/host_display_client.h | 6 ++++-- 9 | .../viz/host/layered_window_updater_impl.cc | 2 +- 10 | .../viz/host/layered_window_updater_impl.h | 2 +- 11 | .../output_surface_provider_impl.cc | 16 ++++++++++++++++ 12 | .../compositor/viz_process_transport_factory.cc | 4 +++- 13 | .../mojom/compositing/display_private.mojom | 1 - 14 | .../compositing/layered_window_updater.mojom | 2 +- 15 | ui/compositor/compositor.h | 16 ++++++++++++++++ 16 | 9 files changed, 45 insertions(+), 8 deletions(-) 17 | 18 | diff --git a/components/viz/host/host_display_client.cc b/components/viz/host/host_display_client.cc 19 | index 6d905b62e6258..bfb803829c73b 100644 20 | --- a/components/viz/host/host_display_client.cc 21 | +++ b/components/viz/host/host_display_client.cc 22 | @@ -47,9 +47,9 @@ void HostDisplayClient::OnDisplayReceivedCALayerParams( 23 | } 24 | #endif 25 | 26 | -#if BUILDFLAG(IS_WIN) 27 | void HostDisplayClient::CreateLayeredWindowUpdater( 28 | mojo::PendingReceiver receiver) { 29 | +#if BUILDFLAG(IS_WIN) 30 | if (!NeedsToUseLayerWindow(widget_)) { 31 | DLOG(ERROR) << "HWND shouldn't be using a layered window"; 32 | return; 33 | @@ -57,7 +57,9 @@ void HostDisplayClient::CreateLayeredWindowUpdater( 34 | 35 | layered_window_updater_ = 36 | std::make_unique(widget_, std::move(receiver)); 37 | +#endif 38 | } 39 | +#if BUILDFLAG(IS_WIN) 40 | void HostDisplayClient::AddChildWindowToBrowser( 41 | gpu::SurfaceHandle child_window) { 42 | NOTREACHED(); 43 | diff --git a/components/viz/host/host_display_client.h b/components/viz/host/host_display_client.h 44 | index 5eeaadec9773f..f3409f0eeda03 100644 45 | --- a/components/viz/host/host_display_client.h 46 | +++ b/components/viz/host/host_display_client.h 47 | @@ -47,11 +47,13 @@ class VIZ_HOST_EXPORT HostDisplayClient : public mojom::DisplayClient { 48 | #endif 49 | 50 | #if BUILDFLAG(IS_WIN) 51 | - void CreateLayeredWindowUpdater( 52 | - mojo::PendingReceiver receiver) override; 53 | void AddChildWindowToBrowser(gpu::SurfaceHandle child_window) override; 54 | #endif 55 | 56 | + protected: 57 | + void CreateLayeredWindowUpdater( 58 | + mojo::PendingReceiver receiver) override; 59 | + 60 | // TODO(crbug.com/1052397): Revisit the macro expression once build flag switch 61 | // of lacros-chrome is complete. 62 | #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) 63 | diff --git a/components/viz/host/layered_window_updater_impl.cc b/components/viz/host/layered_window_updater_impl.cc 64 | index 271486b45dcc8..a62210d8ca3c8 100644 65 | --- a/components/viz/host/layered_window_updater_impl.cc 66 | +++ b/components/viz/host/layered_window_updater_impl.cc 67 | @@ -44,7 +44,7 @@ void LayeredWindowUpdaterImpl::OnAllocatedSharedMemory( 68 | // |region|'s handle will close when it goes out of scope. 69 | } 70 | 71 | -void LayeredWindowUpdaterImpl::Draw(DrawCallback draw_callback) { 72 | +void LayeredWindowUpdaterImpl::Draw(const gfx::Rect& damage_rect, DrawCallback draw_callback) { 73 | TRACE_EVENT0("viz", "LayeredWindowUpdaterImpl::Draw"); 74 | 75 | if (!canvas_) { 76 | diff --git a/components/viz/host/layered_window_updater_impl.h b/components/viz/host/layered_window_updater_impl.h 77 | index 8af69cac78b74..9f74e511c263d 100644 78 | --- a/components/viz/host/layered_window_updater_impl.h 79 | +++ b/components/viz/host/layered_window_updater_impl.h 80 | @@ -38,7 +38,7 @@ class VIZ_HOST_EXPORT LayeredWindowUpdaterImpl 81 | // mojom::LayeredWindowUpdater implementation. 82 | void OnAllocatedSharedMemory(const gfx::Size& pixel_size, 83 | base::UnsafeSharedMemoryRegion region) override; 84 | - void Draw(DrawCallback draw_callback) override; 85 | + void Draw(const gfx::Rect& damage_rect, DrawCallback draw_callback) override; 86 | 87 | private: 88 | const HWND hwnd_; 89 | diff --git a/components/viz/service/display_embedder/output_surface_provider_impl.cc b/components/viz/service/display_embedder/output_surface_provider_impl.cc 90 | index d8f25c1435d4b..2929ebd3887c2 100644 91 | --- a/components/viz/service/display_embedder/output_surface_provider_impl.cc 92 | +++ b/components/viz/service/display_embedder/output_surface_provider_impl.cc 93 | @@ -16,6 +16,7 @@ 94 | #include "build/build_config.h" 95 | #include "build/chromecast_buildflags.h" 96 | #include "build/chromeos_buildflags.h" 97 | +#include "carbonyl/src/browser/software_output_device_proxy.h" 98 | #include "cc/base/switches.h" 99 | #include "components/viz/common/display/renderer_settings.h" 100 | #include "components/viz/common/frame_sinks/begin_frame_source.h" 101 | @@ -29,6 +30,7 @@ 102 | #include "gpu/command_buffer/service/scheduler_sequence.h" 103 | #include "gpu/config/gpu_finch_features.h" 104 | #include "gpu/ipc/common/surface_handle.h" 105 | +#include "services/viz/privileged/mojom/compositing/layered_window_updater.mojom.h" 106 | #include "ui/base/ui_base_switches.h" 107 | 108 | #if BUILDFLAG(IS_WIN) 109 | @@ -134,10 +136,24 @@ std::unique_ptr OutputSurfaceProviderImpl::CreateOutputSurface( 110 | } 111 | } 112 | 113 | +namespace { 114 | + static const bool use_layered_window = true; 115 | +} 116 | + 117 | std::unique_ptr 118 | OutputSurfaceProviderImpl::CreateSoftwareOutputDeviceForPlatform( 119 | gpu::SurfaceHandle surface_handle, 120 | mojom::DisplayClient* display_client) { 121 | +// #if !BUILDFLAG(IS_APPLE) 122 | + if (use_layered_window) { 123 | + DCHECK(display_client); 124 | + mojo::PendingRemote layered_window_updater; 125 | + display_client->CreateLayeredWindowUpdater( 126 | + layered_window_updater.InitWithNewPipeAndPassReceiver()); 127 | + return std::make_unique( 128 | + std::move(layered_window_updater)); 129 | + } 130 | +// #endif 131 | if (headless_) 132 | return std::make_unique(); 133 | 134 | diff --git a/content/browser/compositor/viz_process_transport_factory.cc b/content/browser/compositor/viz_process_transport_factory.cc 135 | index 3b44531f2618f..fae71c1c19a4f 100644 136 | --- a/content/browser/compositor/viz_process_transport_factory.cc 137 | +++ b/content/browser/compositor/viz_process_transport_factory.cc 138 | @@ -53,6 +53,8 @@ 139 | #include "ui/gfx/win/rendering_window_manager.h" 140 | #endif 141 | 142 | +#include "carbonyl/src/browser/host_display_client.h" 143 | + 144 | namespace content { 145 | namespace { 146 | 147 | @@ -400,7 +402,7 @@ void VizProcessTransportFactory::OnEstablishedGpuChannel( 148 | root_params->display_private = 149 | display_private.BindNewEndpointAndPassReceiver(); 150 | compositor_data.display_client = 151 | - std::make_unique(compositor); 152 | + std::make_unique(); 153 | root_params->display_client = 154 | compositor_data.display_client->GetBoundRemote(resize_task_runner_); 155 | mojo::AssociatedRemote 156 | diff --git a/services/viz/privileged/mojom/compositing/display_private.mojom b/services/viz/privileged/mojom/compositing/display_private.mojom 157 | index 52f44a31de4a8..65b938d76e430 100644 158 | --- a/services/viz/privileged/mojom/compositing/display_private.mojom 159 | +++ b/services/viz/privileged/mojom/compositing/display_private.mojom 160 | @@ -103,7 +103,6 @@ interface DisplayClient { 161 | 162 | // Creates a LayeredWindowUpdater implementation to draw into a layered 163 | // window. 164 | - [EnableIf=is_win] 165 | CreateLayeredWindowUpdater(pending_receiver receiver); 166 | 167 | // Sends the created child window to the browser process so that it can be 168 | diff --git a/services/viz/privileged/mojom/compositing/layered_window_updater.mojom b/services/viz/privileged/mojom/compositing/layered_window_updater.mojom 169 | index 2f462f0deb5fc..695869b83cefa 100644 170 | --- a/services/viz/privileged/mojom/compositing/layered_window_updater.mojom 171 | +++ b/services/viz/privileged/mojom/compositing/layered_window_updater.mojom 172 | @@ -26,5 +26,5 @@ interface LayeredWindowUpdater { 173 | // Draws to the HWND by copying pixels from shared memory. Callback must be 174 | // called after draw operation is complete to signal shared memory can be 175 | // modified. 176 | - Draw() => (); 177 | + Draw(gfx.mojom.Rect damage_rect) => (); 178 | }; 179 | diff --git a/ui/compositor/compositor.h b/ui/compositor/compositor.h 180 | index 50cea82c6b477..f024e6013bfb9 100644 181 | --- a/ui/compositor/compositor.h 182 | +++ b/ui/compositor/compositor.h 183 | @@ -87,6 +87,7 @@ class DisplayPrivate; 184 | class ExternalBeginFrameController; 185 | } // namespace mojom 186 | class ContextProvider; 187 | +class HostDisplayClient; 188 | class HostFrameSinkManager; 189 | class LocalSurfaceId; 190 | class RasterContextProvider; 191 | @@ -143,6 +144,16 @@ class COMPOSITOR_EXPORT ContextFactory { 192 | virtual viz::HostFrameSinkManager* GetHostFrameSinkManager() = 0; 193 | }; 194 | 195 | +class COMPOSITOR_EXPORT CompositorDelegate { 196 | + public: 197 | + virtual bool IsOffscreen() const = 0; 198 | + virtual std::unique_ptr CreateHostDisplayClient( 199 | + ui::Compositor* compositor) = 0; 200 | + 201 | + protected: 202 | + virtual ~CompositorDelegate() {} 203 | +}; 204 | + 205 | // Compositor object to take care of GPU painting. 206 | // A Browser compositor object is responsible for generating the final 207 | // displayable form of pixels comprising a single widget's contents. It draws an 208 | @@ -186,6 +197,9 @@ class COMPOSITOR_EXPORT Compositor : public base::PowerSuspendObserver, 209 | // Schedules a redraw of the layer tree associated with this compositor. 210 | void ScheduleDraw(); 211 | 212 | + CompositorDelegate* delegate() const { return delegate_; } 213 | + void SetDelegate(CompositorDelegate* delegate) { delegate_ = delegate; } 214 | + 215 | // Sets the root of the layer tree drawn by this Compositor. The root layer 216 | // must have no parent. The compositor's root layer is reset if the root layer 217 | // is destroyed. NULL can be passed to reset the root layer, in which case the 218 | @@ -503,6 +517,8 @@ class COMPOSITOR_EXPORT Compositor : public base::PowerSuspendObserver, 219 | 220 | std::unique_ptr pending_begin_frame_args_; 221 | 222 | + CompositorDelegate* delegate_ = nullptr; 223 | + 224 | // The root of the Layer tree drawn by this compositor. 225 | raw_ptr root_layer_ = nullptr; 226 | 227 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0004-Setup-browser-default-settings.patch: -------------------------------------------------------------------------------- 1 | From c960c9b1f7ef3f16b27e4eaa4896e3563c88ea91 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:27:27 +0100 4 | Subject: [PATCH 04/14] Setup browser default settings 5 | 6 | --- 7 | headless/public/headless_browser.cc | 4 ++-- 8 | headless/public/headless_browser.h | 4 ++-- 9 | 2 files changed, 4 insertions(+), 4 deletions(-) 10 | 11 | diff --git a/headless/public/headless_browser.cc b/headless/public/headless_browser.cc 12 | index b6c70ecb0fc23..c836a082d2e68 100644 13 | --- a/headless/public/headless_browser.cc 14 | +++ b/headless/public/headless_browser.cc 15 | @@ -22,14 +22,14 @@ namespace headless { 16 | 17 | namespace { 18 | // Product name for building the default user agent string. 19 | -const char kHeadlessProductName[] = "HeadlessChrome"; 20 | +const char kHeadlessProductName[] = "Google Chrome"; 21 | constexpr gfx::Size kDefaultWindowSize(800, 600); 22 | 23 | constexpr gfx::FontRenderParams::Hinting kDefaultFontRenderHinting = 24 | gfx::FontRenderParams::Hinting::HINTING_FULL; 25 | 26 | std::string GetProductNameAndVersion() { 27 | - return std::string(kHeadlessProductName) + "/" + PRODUCT_VERSION; 28 | + return std::string(kHeadlessProductName) + "/" + PRODUCT_VERSION + " (Carbonyl)"; 29 | } 30 | } // namespace 31 | 32 | diff --git a/headless/public/headless_browser.h b/headless/public/headless_browser.h 33 | index 48efaa7d57ca2..afc0236147519 100644 34 | --- a/headless/public/headless_browser.h 35 | +++ b/headless/public/headless_browser.h 36 | @@ -176,10 +176,10 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options { 37 | base::FilePath user_data_dir; 38 | 39 | // Run a browser context in an incognito mode. Enabled by default. 40 | - bool incognito_mode = true; 41 | + bool incognito_mode = false; 42 | 43 | // If true, then all pop-ups and calls to window.open will fail. 44 | - bool block_new_web_contents = false; 45 | + bool block_new_web_contents = true; 46 | 47 | // Whether or not BeginFrames will be issued over DevTools protocol 48 | // (experimental). 49 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0005-Remove-some-debug-assertions.patch: -------------------------------------------------------------------------------- 1 | From 481ff19118891fe65e80b8be0e1f4498874d3b56 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:28:35 +0100 4 | Subject: [PATCH 05/14] Remove some debug assertions 5 | 6 | --- 7 | .../browser/web_contents/web_contents_impl.cc | 1 - 8 | .../core/v8/script_promise_resolver.cc | 44 +++++++++---------- 9 | .../compositing/paint_artifact_compositor.cc | 2 - 10 | .../platform/graphics/graphics_context.cc | 16 +++---- 11 | 4 files changed, 30 insertions(+), 33 deletions(-) 12 | 13 | diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc 14 | index 74749758894a2..4eb891c32b474 100644 15 | --- a/content/browser/web_contents/web_contents_impl.cc 16 | +++ b/content/browser/web_contents/web_contents_impl.cc 17 | @@ -5988,7 +5988,6 @@ void WebContentsImpl::DidNavigateMainFramePreCommit( 18 | 19 | if (IsFullscreen()) 20 | ExitFullscreen(false); 21 | - DCHECK(!IsFullscreen()); 22 | 23 | // Clean up keyboard lock state when navigating. 24 | CancelKeyboardLock(keyboard_lock_widget_); 25 | diff --git a/third_party/blink/renderer/bindings/core/v8/script_promise_resolver.cc b/third_party/blink/renderer/bindings/core/v8/script_promise_resolver.cc 26 | index c3176f4937c21..56d34529dedfa 100644 27 | --- a/third_party/blink/renderer/bindings/core/v8/script_promise_resolver.cc 28 | +++ b/third_party/blink/renderer/bindings/core/v8/script_promise_resolver.cc 29 | @@ -58,28 +58,28 @@ ScriptPromiseResolver::ScriptPromiseResolver( 30 | ScriptPromiseResolver::~ScriptPromiseResolver() = default; 31 | 32 | void ScriptPromiseResolver::Dispose() { 33 | -#if DCHECK_IS_ON() 34 | - // This assertion fails if: 35 | - // - promise() is called at least once and 36 | - // - this resolver is destructed before it is resolved, rejected, 37 | - // detached, the V8 isolate is terminated or the associated 38 | - // ExecutionContext is stopped. 39 | - const bool is_properly_detached = 40 | - state_ == kDetached || !is_promise_called_ || 41 | - !GetScriptState()->ContextIsValid() || !GetExecutionContext() || 42 | - GetExecutionContext()->IsContextDestroyed(); 43 | - if (!is_properly_detached && !suppress_detach_check_) { 44 | - // This is here to make it easier to track down which promise resolvers are 45 | - // being abandoned. See https://crbug.com/873980. 46 | - static crash_reporter::CrashKeyString<1024> trace_key( 47 | - "scriptpromiseresolver-trace"); 48 | - crash_reporter::SetCrashKeyStringToStackTrace(&trace_key, 49 | - create_stack_trace_); 50 | - DCHECK(false) 51 | - << "ScriptPromiseResolver was not properly detached; created at\n" 52 | - << create_stack_trace_.ToString(); 53 | - } 54 | -#endif 55 | +// #if DCHECK_IS_ON() 56 | +// // This assertion fails if: 57 | +// // - promise() is called at least once and 58 | +// // - this resolver is destructed before it is resolved, rejected, 59 | +// // detached, the V8 isolate is terminated or the associated 60 | +// // ExecutionContext is stopped. 61 | +// const bool is_properly_detached = 62 | +// state_ == kDetached || !is_promise_called_ || 63 | +// !GetScriptState()->ContextIsValid() || !GetExecutionContext() || 64 | +// GetExecutionContext()->IsContextDestroyed(); 65 | +// if (!is_properly_detached && !suppress_detach_check_) { 66 | +// // This is here to make it easier to track down which promise resolvers are 67 | +// // being abandoned. See https://crbug.com/873980. 68 | +// static crash_reporter::CrashKeyString<1024> trace_key( 69 | +// "scriptpromiseresolver-trace"); 70 | +// crash_reporter::SetCrashKeyStringToStackTrace(&trace_key, 71 | +// create_stack_trace_); 72 | +// DCHECK(false) 73 | +// << "ScriptPromiseResolver was not properly detached; created at\n" 74 | +// << create_stack_trace_.ToString(); 75 | +// } 76 | +// #endif 77 | deferred_resolve_task_.Cancel(); 78 | } 79 | 80 | diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc 81 | index d3131a4e07ece..a9464abd86a69 100644 82 | --- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc 83 | +++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc 84 | @@ -196,7 +196,6 @@ bool NeedsFullUpdateAfterPaintingChunk( 85 | // properties are changed, which would indicate a missing call to 86 | // SetNeedsUpdate. 87 | if (previous.properties != repainted.properties) { 88 | - NOTREACHED(); 89 | return true; 90 | } 91 | 92 | @@ -253,7 +252,6 @@ bool NeedsFullUpdateAfterPaintingChunk( 93 | // properties are changed, which would indicate a missing call to 94 | // SetNeedsUpdate. 95 | if (previous.properties != repainted.properties) { 96 | - NOTREACHED(); 97 | return true; 98 | } 99 | 100 | diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.cc b/third_party/blink/renderer/platform/graphics/graphics_context.cc 101 | index 2518b71275670..3a1b8e6646c43 100644 102 | --- a/third_party/blink/renderer/platform/graphics/graphics_context.cc 103 | +++ b/third_party/blink/renderer/platform/graphics/graphics_context.cc 104 | @@ -146,14 +146,14 @@ GraphicsContext::GraphicsContext(PaintController& paint_controller) 105 | } 106 | 107 | GraphicsContext::~GraphicsContext() { 108 | -#if DCHECK_IS_ON() 109 | - if (!disable_destruction_checks_) { 110 | - DCHECK(!paint_state_index_); 111 | - DCHECK(!paint_state_->SaveCount()); 112 | - DCHECK(!layer_count_); 113 | - DCHECK(!SaveCount()); 114 | - } 115 | -#endif 116 | +// #if DCHECK_IS_ON() 117 | +// if (!disable_destruction_checks_) { 118 | +// DCHECK(!paint_state_index_); 119 | +// DCHECK(!paint_state_->SaveCount()); 120 | +// DCHECK(!layer_count_); 121 | +// DCHECK(!SaveCount()); 122 | +// } 123 | +// #endif 124 | } 125 | 126 | void GraphicsContext::CopyConfigFrom(GraphicsContext& other) { 127 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0006-Setup-display-DPI.patch: -------------------------------------------------------------------------------- 1 | From cc9c37adb3ad2613a114bd37e1fde43f83951d88 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Sun, 12 Feb 2023 01:00:43 +0100 4 | Subject: [PATCH 06/14] Setup display DPI 5 | 6 | --- 7 | .../lib/browser/headless_browser_impl_aura.cc | 11 ++-- 8 | headless/lib/browser/headless_screen.cc | 5 +- 9 | ui/display/display.cc | 52 ++++++++++--------- 10 | 3 files changed, 35 insertions(+), 33 deletions(-) 11 | 12 | diff --git a/headless/lib/browser/headless_browser_impl_aura.cc b/headless/lib/browser/headless_browser_impl_aura.cc 13 | index 81261215c702f..508660db32151 100644 14 | --- a/headless/lib/browser/headless_browser_impl_aura.cc 15 | +++ b/headless/lib/browser/headless_browser_impl_aura.cc 16 | @@ -19,6 +19,8 @@ 17 | #include "ui/events/devices/device_data_manager.h" 18 | #include "ui/gfx/geometry/rect.h" 19 | 20 | +#include "carbonyl/src/browser/bridge.h" 21 | + 22 | namespace headless { 23 | 24 | void HeadlessBrowserImpl::PlatformInitialize() { 25 | @@ -57,13 +59,8 @@ void HeadlessBrowserImpl::PlatformSetWebContentsBounds( 26 | const gfx::Rect& bounds) { 27 | // Browser's window bounds should contain all web contents, so that we're sure 28 | // that we will actually produce visible damage when taking a screenshot. 29 | - gfx::Rect old_host_bounds = 30 | - web_contents->window_tree_host()->GetBoundsInPixels(); 31 | - gfx::Rect new_host_bounds( 32 | - 0, 0, std::max(old_host_bounds.width(), bounds.x() + bounds.width()), 33 | - std::max(old_host_bounds.height(), bounds.y() + bounds.height())); 34 | - web_contents->window_tree_host()->SetBoundsInPixels(new_host_bounds); 35 | - web_contents->window_tree_host()->window()->SetBounds(new_host_bounds); 36 | + web_contents->window_tree_host()->SetBoundsInPixels(ScaleToEnclosedRect(bounds, carbonyl::Renderer::GetDPI())); 37 | + web_contents->window_tree_host()->window()->SetBounds(bounds); 38 | 39 | gfx::NativeView native_view = web_contents->web_contents()->GetNativeView(); 40 | native_view->SetBounds(bounds); 41 | diff --git a/headless/lib/browser/headless_screen.cc b/headless/lib/browser/headless_screen.cc 42 | index 28f1a65f6dce5..8bf00ef5e036a 100644 43 | --- a/headless/lib/browser/headless_screen.cc 44 | +++ b/headless/lib/browser/headless_screen.cc 45 | @@ -13,6 +13,8 @@ 46 | #include "ui/gfx/geometry/size_conversions.h" 47 | #include "ui/gfx/native_widget_types.h" 48 | 49 | +#include "carbonyl/src/browser/bridge.h" 50 | + 51 | namespace headless { 52 | 53 | // static 54 | @@ -49,7 +51,8 @@ display::Display HeadlessScreen::GetDisplayNearestWindow( 55 | HeadlessScreen::HeadlessScreen(const gfx::Rect& screen_bounds) { 56 | static int64_t synthesized_display_id = 2000; 57 | display::Display display(synthesized_display_id++); 58 | - display.SetScaleAndBounds(1.0f, screen_bounds); 59 | + float dpi = carbonyl::Renderer::GetDPI(); 60 | + display.SetScaleAndBounds(dpi, ScaleToEnclosedRect(screen_bounds, dpi)); 61 | ProcessDisplayChanged(display, true /* is_primary */); 62 | } 63 | 64 | diff --git a/ui/display/display.cc b/ui/display/display.cc 65 | index 466ef1fd1fe6e..1d71f3b4c9857 100644 66 | --- a/ui/display/display.cc 67 | +++ b/ui/display/display.cc 68 | @@ -21,6 +21,8 @@ 69 | #include "ui/gfx/geometry/transform.h" 70 | #include "ui/gfx/icc_profile.h" 71 | 72 | +#include "carbonyl/src/browser/bridge.h" 73 | + 74 | namespace display { 75 | namespace { 76 | 77 | @@ -39,22 +41,22 @@ float g_forced_device_scale_factor = -1.0; 78 | constexpr float kDisplaySizeAllowanceEpsilon = 0.01f; 79 | 80 | bool HasForceDeviceScaleFactorImpl() { 81 | - return base::CommandLine::ForCurrentProcess()->HasSwitch( 82 | - switches::kForceDeviceScaleFactor); 83 | + // return base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kForceDeviceScaleFactor); 84 | + return true; 85 | } 86 | 87 | float GetForcedDeviceScaleFactorImpl() { 88 | - double scale_in_double = 1.0; 89 | - if (HasForceDeviceScaleFactorImpl()) { 90 | - std::string value = 91 | - base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( 92 | - switches::kForceDeviceScaleFactor); 93 | - if (!base::StringToDouble(value, &scale_in_double)) { 94 | - LOG(ERROR) << "Failed to parse the default device scale factor:" << value; 95 | - scale_in_double = 1.0; 96 | - } 97 | - } 98 | - return static_cast(scale_in_double); 99 | + // double scale_in_double = 1.0; 100 | + // if (HasForceDeviceScaleFactorImpl()) { 101 | + // std::string value = 102 | + // base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( 103 | + // switches::kForceDeviceScaleFactor); 104 | + // if (!base::StringToDouble(value, &scale_in_double)) { 105 | + // LOG(ERROR) << "Failed to parse the default device scale factor:" << value; 106 | + // scale_in_double = 1.0; 107 | + // } 108 | + // } 109 | + return carbonyl::Bridge::GetCurrent()->GetDPI(); 110 | } 111 | 112 | const char* ToRotationString(display::Display::Rotation rotation) { 113 | @@ -97,11 +99,11 @@ void Display::ResetForceDeviceScaleFactorForTesting() { 114 | // static 115 | void Display::SetForceDeviceScaleFactor(double dsf) { 116 | // Reset any previously set values and unset the flag. 117 | - g_has_forced_device_scale_factor = -1; 118 | - g_forced_device_scale_factor = -1.0; 119 | + // g_has_forced_device_scale_factor = -1; 120 | + // g_forced_device_scale_factor = -1.0; 121 | 122 | - base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( 123 | - switches::kForceDeviceScaleFactor, base::StringPrintf("%.2f", dsf)); 124 | + // base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( 125 | + // switches::kForceDeviceScaleFactor, base::StringPrintf("%.2f", dsf)); 126 | } 127 | 128 | // static 129 | @@ -273,15 +275,15 @@ void Display::SetScaleAndBounds(float device_scale_factor, 130 | } 131 | 132 | void Display::SetScale(float device_scale_factor) { 133 | - if (!HasForceDeviceScaleFactor()) { 134 | -#if BUILDFLAG(IS_APPLE) 135 | - // Unless an explicit scale factor was provided for testing, ensure the 136 | - // scale is integral. 137 | - device_scale_factor = static_cast(device_scale_factor); 138 | -#endif 139 | +// if (!HasForceDeviceScaleFactor()) { 140 | +// #if BUILDFLAG(IS_APPLE) 141 | +// // Unless an explicit scale factor was provided for testing, ensure the 142 | +// // scale is integral. 143 | +// device_scale_factor = static_cast(device_scale_factor); 144 | +// #endif 145 | device_scale_factor_ = device_scale_factor; 146 | - } 147 | - device_scale_factor_ = std::max(0.5f, device_scale_factor_); 148 | + // } 149 | + // device_scale_factor_ = std::max(0.5f, device_scale_factor_); 150 | } 151 | 152 | void Display::SetSize(const gfx::Size& size_in_pixel) { 153 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0007-Disable-text-effects.patch: -------------------------------------------------------------------------------- 1 | From 022ed4d808369659eab4e83cd677eb974215c58c Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:31:17 +0100 4 | Subject: [PATCH 07/14] Disable text effects 5 | 6 | --- 7 | .../core/paint/ng/ng_text_painter_base.cc | 30 ++++++++-------- 8 | ui/gfx/render_text.cc | 34 +++++++++---------- 9 | 2 files changed, 32 insertions(+), 32 deletions(-) 10 | 11 | diff --git a/third_party/blink/renderer/core/paint/ng/ng_text_painter_base.cc b/third_party/blink/renderer/core/paint/ng/ng_text_painter_base.cc 12 | index 008d80040e719..c11da51d0e906 100644 13 | --- a/third_party/blink/renderer/core/paint/ng/ng_text_painter_base.cc 14 | +++ b/third_party/blink/renderer/core/paint/ng/ng_text_painter_base.cc 15 | @@ -123,21 +123,21 @@ void NGTextPainterBase::PaintUnderOrOverLineDecorations( 16 | continue; 17 | } 18 | 19 | - if (decoration_info.HasUnderline() && decoration_info.FontData() && 20 | - EnumHasFlags(lines_to_paint, TextDecorationLine::kUnderline)) { 21 | - decoration_info.SetUnderlineLineData(decoration_offset); 22 | - PaintDecorationUnderOrOverLine(fragment_paint_info, context, 23 | - decoration_info, 24 | - TextDecorationLine::kUnderline, flags); 25 | - } 26 | - 27 | - if (decoration_info.HasOverline() && decoration_info.FontData() && 28 | - EnumHasFlags(lines_to_paint, TextDecorationLine::kOverline)) { 29 | - decoration_info.SetOverlineLineData(decoration_offset); 30 | - PaintDecorationUnderOrOverLine(fragment_paint_info, context, 31 | - decoration_info, 32 | - TextDecorationLine::kOverline, flags); 33 | - } 34 | + // if (decoration_info.HasUnderline() && decoration_info.FontData() && 35 | + // EnumHasFlags(lines_to_paint, TextDecorationLine::kUnderline)) { 36 | + // decoration_info.SetUnderlineLineData(decoration_offset); 37 | + // PaintDecorationUnderOrOverLine(fragment_paint_info, context, 38 | + // decoration_info, 39 | + // TextDecorationLine::kUnderline, flags); 40 | + // } 41 | + 42 | + // if (decoration_info.HasOverline() && decoration_info.FontData() && 43 | + // EnumHasFlags(lines_to_paint, TextDecorationLine::kOverline)) { 44 | + // decoration_info.SetOverlineLineData(decoration_offset); 45 | + // PaintDecorationUnderOrOverLine(fragment_paint_info, context, 46 | + // decoration_info, 47 | + // TextDecorationLine::kOverline, flags); 48 | + // } 49 | } 50 | } 51 | 52 | diff --git a/ui/gfx/render_text.cc b/ui/gfx/render_text.cc 53 | index 67fbf128ea158..a645ba61c8597 100644 54 | --- a/ui/gfx/render_text.cc 55 | +++ b/ui/gfx/render_text.cc 56 | @@ -55,9 +55,9 @@ constexpr char16_t kEllipsisCodepoint = 0x2026; 57 | 58 | // Fraction of the text size to raise the center of a strike-through line above 59 | // the baseline. 60 | -const SkScalar kStrikeThroughOffset = (SK_Scalar1 * 65 / 252); 61 | +// const SkScalar kStrikeThroughOffset = (SK_Scalar1 * 65 / 252); 62 | // Fraction of the text size to lower an underline below the baseline. 63 | -const SkScalar kUnderlineOffset = (SK_Scalar1 / 9); 64 | +// const SkScalar kUnderlineOffset = (SK_Scalar1 / 9); 65 | 66 | // Float comparison needs epsilon to consider rounding errors in float 67 | // arithmetic. Epsilon should be dependent on the context and here, we are 68 | @@ -374,27 +374,27 @@ void SkiaTextRenderer::DrawUnderline(int x, 69 | int y, 70 | int width, 71 | SkScalar thickness_factor) { 72 | - SkScalar x_scalar = SkIntToScalar(x); 73 | - const SkScalar text_size = font_.getSize(); 74 | - SkRect r = SkRect::MakeLTRB( 75 | - x_scalar, y + text_size * kUnderlineOffset, x_scalar + width, 76 | - y + (text_size * 77 | - (kUnderlineOffset + 78 | - (thickness_factor * RenderText::kLineThicknessFactor)))); 79 | - canvas_skia_->drawRect(r, flags_); 80 | + // SkScalar x_scalar = SkIntToScalar(x); 81 | + // const SkScalar text_size = font_.getSize(); 82 | + // SkRect r = SkRect::MakeLTRB( 83 | + // x_scalar, y + text_size * kUnderlineOffset, x_scalar + width, 84 | + // y + (text_size * 85 | + // (kUnderlineOffset + 86 | + // (thickness_factor * RenderText::kLineThicknessFactor)))); 87 | + // canvas_skia_->drawRect(r, flags_); 88 | } 89 | 90 | void SkiaTextRenderer::DrawStrike(int x, 91 | int y, 92 | int width, 93 | SkScalar thickness_factor) { 94 | - const SkScalar text_size = font_.getSize(); 95 | - const SkScalar height = text_size * thickness_factor; 96 | - const SkScalar top = y - text_size * kStrikeThroughOffset - height / 2; 97 | - SkScalar x_scalar = SkIntToScalar(x); 98 | - const SkRect r = 99 | - SkRect::MakeLTRB(x_scalar, top, x_scalar + width, top + height); 100 | - canvas_skia_->drawRect(r, flags_); 101 | + // const SkScalar text_size = font_.getSize(); 102 | + // const SkScalar height = text_size * thickness_factor; 103 | + // const SkScalar top = y - text_size * kStrikeThroughOffset - height / 2; 104 | + // SkScalar x_scalar = SkIntToScalar(x); 105 | + // const SkRect r = 106 | + // SkRect::MakeLTRB(x_scalar, top, x_scalar + width, top + height); 107 | + // canvas_skia_->drawRect(r, flags_); 108 | } 109 | 110 | StyleIterator::StyleIterator(const BreakList* colors, 111 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0008-Fix-text-layout.patch: -------------------------------------------------------------------------------- 1 | From 7b1f72900f704ffecc48c66da7ccd6de205b88f7 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:32:14 +0100 4 | Subject: [PATCH 08/14] Fix text layout 5 | 6 | --- 7 | .../core/css/resolver/style_resolver.cc | 17 ++++++++++++++++- 8 | 1 file changed, 16 insertions(+), 1 deletion(-) 9 | 10 | diff --git a/third_party/blink/renderer/core/css/resolver/style_resolver.cc b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 11 | index 6207b72d17cb9..79cb8c85b697f 100644 12 | --- a/third_party/blink/renderer/core/css/resolver/style_resolver.cc 13 | +++ b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 14 | @@ -281,7 +281,9 @@ String ComputeBaseComputedStyleDiff(const ComputedStyle* base_computed_style, 15 | return g_null_atom; 16 | } 17 | 18 | - return String("Field diff: ") + builder.ReleaseString(); 19 | + // TODO(fathy): Carbonyl should properly set the computed style 20 | + // return String("Field diff: ") + builder.ReleaseString(); 21 | + return g_null_atom; 22 | } 23 | #endif // DCHECK_IS_ON() 24 | 25 | @@ -1039,6 +1041,19 @@ scoped_refptr StyleResolver::ResolveStyle( 26 | UseCounter::Count(GetDocument(), WebFeature::kHasGlyphRelativeUnits); 27 | } 28 | 29 | + auto font = state.StyleBuilder().GetFontDescription(); 30 | + FontFamily family; 31 | + 32 | + family.SetFamily("monospace", FontFamily::Type::kGenericFamily); 33 | + font.SetFamily(family); 34 | + font.SetStretch(ExtraExpandedWidthValue()); 35 | + font.SetKerning(FontDescription::kNoneKerning); 36 | + font.SetComputedSize(11.75 / 7.0); 37 | + font.SetGenericFamily(FontDescription::kMonospaceFamily); 38 | + font.SetIsAbsoluteSize(true); 39 | + state.StyleBuilder().SetFontDescription(font); 40 | + state.StyleBuilder().SetLineHeight(Length::Fixed(14.0 / 7.0)); 41 | + 42 | state.LoadPendingResources(); 43 | 44 | // Now return the style. 45 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0010-Conditionally-enable-text-rendering.patch: -------------------------------------------------------------------------------- 1 | From bdc80f35a7113b7523c4d992edc9170db082deb0 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Sun, 12 Feb 2023 00:55:33 +0100 4 | Subject: [PATCH 10/14] Conditionally enable text rendering 5 | 6 | --- 7 | content/renderer/render_frame_impl.cc | 3 ++- 8 | .../core/css/resolver/style_resolver.cc | 26 +++++++++++-------- 9 | third_party/blink/renderer/platform/BUILD.gn | 1 + 10 | .../blink/renderer/platform/fonts/font.cc | 10 +++---- 11 | 4 files changed, 23 insertions(+), 17 deletions(-) 12 | 13 | diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc 14 | index 97b61ffb954be..891efd6a9d796 100644 15 | --- a/content/renderer/render_frame_impl.cc 16 | +++ b/content/renderer/render_frame_impl.cc 17 | @@ -259,6 +259,7 @@ 18 | // Carbonyl 19 | #include 20 | #include 21 | +#include "carbonyl/src/browser/bridge.h" 22 | #include "cc/paint/paint_recorder.h" 23 | #include "cc/paint/skia_paint_canvas.h" 24 | #include "cc/raster/playback_image_provider.h" 25 | @@ -2221,7 +2222,7 @@ void RenderFrameImpl::Initialize(blink::WebFrame* parent) { 26 | 27 | render_callback_ = std::make_shared>( 28 | [=]() -> bool { 29 | - if (!IsMainFrame() || IsHidden()) { 30 | + if (!IsMainFrame() || IsHidden() || carbonyl::Bridge::BitmapMode()) { 31 | return false; 32 | } 33 | 34 | diff --git a/third_party/blink/renderer/core/css/resolver/style_resolver.cc b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 35 | index 79cb8c85b697f..7129982acf4a6 100644 36 | --- a/third_party/blink/renderer/core/css/resolver/style_resolver.cc 37 | +++ b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 38 | @@ -116,6 +116,8 @@ 39 | #include "third_party/blink/renderer/platform/wtf/text/atomic_string_hash.h" 40 | #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" 41 | 42 | +#include "carbonyl/src/browser/bridge.h" 43 | + 44 | namespace blink { 45 | 46 | namespace { 47 | @@ -1041,18 +1043,20 @@ scoped_refptr StyleResolver::ResolveStyle( 48 | UseCounter::Count(GetDocument(), WebFeature::kHasGlyphRelativeUnits); 49 | } 50 | 51 | - auto font = state.StyleBuilder().GetFontDescription(); 52 | - FontFamily family; 53 | + if (!carbonyl::Bridge::BitmapMode()) { 54 | + auto font = state.StyleBuilder().GetFontDescription(); 55 | + FontFamily family; 56 | 57 | - family.SetFamily("monospace", FontFamily::Type::kGenericFamily); 58 | - font.SetFamily(family); 59 | - font.SetStretch(ExtraExpandedWidthValue()); 60 | - font.SetKerning(FontDescription::kNoneKerning); 61 | - font.SetComputedSize(11.75 / 7.0); 62 | - font.SetGenericFamily(FontDescription::kMonospaceFamily); 63 | - font.SetIsAbsoluteSize(true); 64 | - state.StyleBuilder().SetFontDescription(font); 65 | - state.StyleBuilder().SetLineHeight(Length::Fixed(14.0 / 7.0)); 66 | + family.SetFamily("monospace", FontFamily::Type::kGenericFamily); 67 | + font.SetFamily(family); 68 | + font.SetStretch(ExtraExpandedWidthValue()); 69 | + font.SetKerning(FontDescription::kNoneKerning); 70 | + font.SetComputedSize(13.25 / 4.0); 71 | + font.SetGenericFamily(FontDescription::kMonospaceFamily); 72 | + font.SetIsAbsoluteSize(true); 73 | + state.StyleBuilder().SetFontDescription(font); 74 | + state.StyleBuilder().SetLineHeight(Length::Fixed(16.0 / 4.0)); 75 | + } 76 | 77 | state.LoadPendingResources(); 78 | 79 | diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn 80 | index e7b1c1a52e4c9..63fc13e44b5ae 100644 81 | --- a/third_party/blink/renderer/platform/BUILD.gn 82 | +++ b/third_party/blink/renderer/platform/BUILD.gn 83 | @@ -1678,6 +1678,7 @@ component("platform") { 84 | "//base/allocator:buildflags", 85 | "//build:chromecast_buildflags", 86 | "//build:chromeos_buildflags", 87 | + "//carbonyl/src/browser:carbonyl", 88 | "//cc/ipc", 89 | "//cc/mojo_embedder", 90 | "//components/paint_preview/common", 91 | diff --git a/third_party/blink/renderer/platform/fonts/font.cc b/third_party/blink/renderer/platform/fonts/font.cc 92 | index dfdc79eacce3b..4625300729523 100644 93 | --- a/third_party/blink/renderer/platform/fonts/font.cc 94 | +++ b/third_party/blink/renderer/platform/fonts/font.cc 95 | @@ -49,6 +49,8 @@ 96 | #include "third_party/skia/include/core/SkTextBlob.h" 97 | #include "ui/gfx/geometry/rect_f.h" 98 | 99 | +#include "carbonyl/src/browser/bridge.h" 100 | + 101 | namespace blink { 102 | 103 | namespace { 104 | @@ -151,14 +153,12 @@ bool Font::operator==(const Font& other) const { 105 | 106 | namespace { 107 | 108 | -static const bool carbonyl_b64_text = true; 109 | - 110 | void DrawBlobs(cc::PaintCanvas* canvas, 111 | const cc::PaintFlags& flags, 112 | const ShapeResultBloberizer::BlobBuffer& blobs, 113 | const gfx::PointF& point, 114 | cc::NodeId node_id = cc::kInvalidNodeId) { 115 | - if (carbonyl_b64_text) { 116 | + if (!carbonyl::Bridge::BitmapMode()) { 117 | return; 118 | } 119 | 120 | @@ -237,7 +237,7 @@ void Font::DrawText(cc::PaintCanvas* canvas, 121 | if (ShouldSkipDrawing()) 122 | return; 123 | 124 | - if (carbonyl_b64_text) { 125 | + if (!carbonyl::Bridge::BitmapMode()) { 126 | auto string = StringView( 127 | run_info.run.ToStringView(), 128 | run_info.from, 129 | @@ -285,7 +285,7 @@ void Font::DrawText(cc::PaintCanvas* canvas, 130 | if (ShouldSkipDrawing()) 131 | return; 132 | 133 | - if (carbonyl_b64_text) { 134 | + if (!carbonyl::Bridge::BitmapMode()) { 135 | auto string = StringView( 136 | text_info.text, 137 | text_info.from, 138 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0011-Rename-carbonyl-Renderer-to-carbonyl-Bridge.patch: -------------------------------------------------------------------------------- 1 | From fa52dbb68b7822ee4c01a697197e68ef1ab4a19c Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Sun, 12 Feb 2023 01:29:05 +0100 4 | Subject: [PATCH 11/14] Rename carbonyl::Renderer to carbonyl::Bridge 5 | 6 | --- 7 | headless/app/headless_shell.cc | 5 ++++- 8 | headless/app/headless_shell_main.cc | 2 +- 9 | headless/lib/browser/headless_browser_impl.cc | 8 ++++---- 10 | headless/lib/browser/headless_browser_impl_aura.cc | 2 +- 11 | headless/lib/browser/headless_screen.cc | 2 +- 12 | headless/lib/browser/headless_web_contents_impl.cc | 4 ++-- 13 | 6 files changed, 13 insertions(+), 10 deletions(-) 14 | 15 | diff --git a/headless/app/headless_shell.cc b/headless/app/headless_shell.cc 16 | index 5b51c22ae1da3..b6a52857e8f90 100644 17 | --- a/headless/app/headless_shell.cc 18 | +++ b/headless/app/headless_shell.cc 19 | @@ -12,6 +12,7 @@ 20 | #include "base/bind.h" 21 | #include "base/command_line.h" 22 | #include "base/files/file_util.h" 23 | +#include "base/functional/callback.h" 24 | #include "base/i18n/rtl.h" 25 | #include "base/task/thread_pool.h" 26 | #include "build/branding_buildflags.h" 27 | @@ -92,7 +93,9 @@ void HeadlessShell::OnBrowserStart(HeadlessBrowser* browser) { 28 | HeadlessBrowserContext::Builder context_builder = 29 | browser_->CreateBrowserContextBuilder(); 30 | 31 | - context_builder.SetWindowSize(carbonyl::Renderer::Main()->GetSize()); 32 | + carbonyl::Bridge::GetCurrent()->StartRenderer(); 33 | + 34 | + context_builder.SetWindowSize(carbonyl::Bridge::GetCurrent()->GetSize()); 35 | 36 | // Retrieve the locale set by InitApplicationLocale() in 37 | // headless_content_main_delegate.cc in a way that is free of side-effects. 38 | diff --git a/headless/app/headless_shell_main.cc b/headless/app/headless_shell_main.cc 39 | index f9b8bac5c18a5..739df1ae1bd58 100644 40 | --- a/headless/app/headless_shell_main.cc 41 | +++ b/headless/app/headless_shell_main.cc 42 | @@ -17,7 +17,7 @@ 43 | #include "carbonyl/src/browser/bridge.h" 44 | 45 | int main(int argc, const char** argv) { 46 | - carbonyl_shell_main(); 47 | + carbonyl::Bridge::Main(); 48 | 49 | #if BUILDFLAG(IS_WIN) 50 | sandbox::SandboxInterfaceInfo sandbox_info = {nullptr}; 51 | diff --git a/headless/lib/browser/headless_browser_impl.cc b/headless/lib/browser/headless_browser_impl.cc 52 | index fd45d215479ab..1df3ffe72c93d 100644 53 | --- a/headless/lib/browser/headless_browser_impl.cc 54 | +++ b/headless/lib/browser/headless_browser_impl.cc 55 | @@ -119,7 +119,7 @@ void HeadlessBrowserImpl::set_browser_main_parts( 56 | } 57 | 58 | void HeadlessBrowserImpl::Resize() { 59 | - auto size = carbonyl::Renderer::GetSize(); 60 | + auto size = carbonyl::Bridge::GetCurrent()->Resize(); 61 | auto rect = gfx::Rect(0, 0, size.width(), size.height()); 62 | 63 | for (auto* ctx: GetAllBrowserContexts()) { 64 | @@ -134,7 +134,7 @@ void HeadlessBrowserImpl::Resize() { 65 | } 66 | } 67 | 68 | - carbonyl::Renderer::Main()->Resize(); 69 | + carbonyl::Bridge::GetCurrent()->Resize(); 70 | } 71 | 72 | void HeadlessBrowserImpl::OnShutdownInput() { 73 | @@ -279,7 +279,7 @@ void HeadlessBrowserImpl::OnKeyPressInput(char key) { 74 | blink::WebKeyboardEvent::Type::kRawKeyDown, 75 | blink::WebInputEvent::kNoModifiers, 76 | base::TimeTicks::Now()); 77 | - 78 | + 79 | // TODO(fathy): support IME 80 | switch (key) { 81 | case 0x11: 82 | @@ -500,7 +500,7 @@ void HeadlessBrowserImpl::RunOnStartCallback() { 83 | } 84 | }; 85 | 86 | - carbonyl::Renderer::Main()->Listen(&delegate); 87 | + carbonyl::Bridge::GetCurrent()->Listen(&delegate); 88 | }); 89 | } 90 | 91 | diff --git a/headless/lib/browser/headless_browser_impl_aura.cc b/headless/lib/browser/headless_browser_impl_aura.cc 92 | index 508660db32151..80340d9f1b3b3 100644 93 | --- a/headless/lib/browser/headless_browser_impl_aura.cc 94 | +++ b/headless/lib/browser/headless_browser_impl_aura.cc 95 | @@ -59,7 +59,7 @@ void HeadlessBrowserImpl::PlatformSetWebContentsBounds( 96 | const gfx::Rect& bounds) { 97 | // Browser's window bounds should contain all web contents, so that we're sure 98 | // that we will actually produce visible damage when taking a screenshot. 99 | - web_contents->window_tree_host()->SetBoundsInPixels(ScaleToEnclosedRect(bounds, carbonyl::Renderer::GetDPI())); 100 | + web_contents->window_tree_host()->SetBoundsInPixels(ScaleToEnclosedRect(bounds, carbonyl::Bridge::GetCurrent()->GetDPI())); 101 | web_contents->window_tree_host()->window()->SetBounds(bounds); 102 | 103 | gfx::NativeView native_view = web_contents->web_contents()->GetNativeView(); 104 | diff --git a/headless/lib/browser/headless_screen.cc b/headless/lib/browser/headless_screen.cc 105 | index 8bf00ef5e036a..89c5ccc8d7759 100644 106 | --- a/headless/lib/browser/headless_screen.cc 107 | +++ b/headless/lib/browser/headless_screen.cc 108 | @@ -51,7 +51,7 @@ display::Display HeadlessScreen::GetDisplayNearestWindow( 109 | HeadlessScreen::HeadlessScreen(const gfx::Rect& screen_bounds) { 110 | static int64_t synthesized_display_id = 2000; 111 | display::Display display(synthesized_display_id++); 112 | - float dpi = carbonyl::Renderer::GetDPI(); 113 | + float dpi = carbonyl::Bridge::GetCurrent()->GetDPI(); 114 | display.SetScaleAndBounds(dpi, ScaleToEnclosedRect(screen_bounds, dpi)); 115 | ProcessDisplayChanged(display, true /* is_primary */); 116 | } 117 | diff --git a/headless/lib/browser/headless_web_contents_impl.cc b/headless/lib/browser/headless_web_contents_impl.cc 118 | index fad8c3fdd2bfe..a166a08f6ea15 100644 119 | --- a/headless/lib/browser/headless_web_contents_impl.cc 120 | +++ b/headless/lib/browser/headless_web_contents_impl.cc 121 | @@ -400,7 +400,7 @@ void HeadlessWebContentsImpl::TitleWasSet(content::NavigationEntry* entry) { 122 | if (!web_contents() || !web_contents()->GetPrimaryMainFrame()->IsActive()) 123 | return; 124 | 125 | - carbonyl::Renderer::Main()->SetTitle(base::UTF16ToUTF8(entry->GetTitleForDisplay())); 126 | + carbonyl::Bridge::GetCurrent()->SetTitle(base::UTF16ToUTF8(entry->GetTitleForDisplay())); 127 | } 128 | 129 | void HeadlessWebContentsImpl::DidFinishNavigation(content::NavigationHandle* handle) { 130 | @@ -411,7 +411,7 @@ void HeadlessWebContentsImpl::DidFinishNavigation(content::NavigationHandle* han 131 | 132 | auto& nav = web_contents()->GetController(); 133 | 134 | - carbonyl::Renderer::Main()->PushNav( 135 | + carbonyl::Bridge::GetCurrent()->PushNav( 136 | handle->GetURL().spec(), 137 | nav.CanGoBack(), 138 | nav.CanGoForward() 139 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0012-Create-separate-bridge-for-Blink.patch: -------------------------------------------------------------------------------- 1 | From 6862e372717eff278470453e800dc693f33b873c Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Sun, 12 Feb 2023 02:20:32 +0100 4 | Subject: [PATCH 12/14] Create separate bridge for Blink 5 | 6 | --- 7 | .../blink/renderer/core/css/resolver/style_resolver.cc | 4 ++-- 8 | third_party/blink/renderer/platform/BUILD.gn | 2 +- 9 | third_party/blink/renderer/platform/fonts/font.cc | 8 ++++---- 10 | 3 files changed, 7 insertions(+), 7 deletions(-) 11 | 12 | diff --git a/third_party/blink/renderer/core/css/resolver/style_resolver.cc b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 13 | index 7129982acf4a6..cb116ee07c8f6 100644 14 | --- a/third_party/blink/renderer/core/css/resolver/style_resolver.cc 15 | +++ b/third_party/blink/renderer/core/css/resolver/style_resolver.cc 16 | @@ -116,7 +116,7 @@ 17 | #include "third_party/blink/renderer/platform/wtf/text/atomic_string_hash.h" 18 | #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" 19 | 20 | -#include "carbonyl/src/browser/bridge.h" 21 | +#include "carbonyl/src/browser/blink.h" 22 | 23 | namespace blink { 24 | 25 | @@ -1043,7 +1043,7 @@ scoped_refptr StyleResolver::ResolveStyle( 26 | UseCounter::Count(GetDocument(), WebFeature::kHasGlyphRelativeUnits); 27 | } 28 | 29 | - if (!carbonyl::Bridge::BitmapMode()) { 30 | + if (!carbonyl::blink::BitmapMode()) { 31 | auto font = state.StyleBuilder().GetFontDescription(); 32 | FontFamily family; 33 | 34 | diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn 35 | index 63fc13e44b5ae..ceb41d781acf6 100644 36 | --- a/third_party/blink/renderer/platform/BUILD.gn 37 | +++ b/third_party/blink/renderer/platform/BUILD.gn 38 | @@ -1678,7 +1678,7 @@ component("platform") { 39 | "//base/allocator:buildflags", 40 | "//build:chromecast_buildflags", 41 | "//build:chromeos_buildflags", 42 | - "//carbonyl/src/browser:carbonyl", 43 | + "//carbonyl/src/browser:blink", 44 | "//cc/ipc", 45 | "//cc/mojo_embedder", 46 | "//components/paint_preview/common", 47 | diff --git a/third_party/blink/renderer/platform/fonts/font.cc b/third_party/blink/renderer/platform/fonts/font.cc 48 | index 4625300729523..3d1b463e9651c 100644 49 | --- a/third_party/blink/renderer/platform/fonts/font.cc 50 | +++ b/third_party/blink/renderer/platform/fonts/font.cc 51 | @@ -49,7 +49,7 @@ 52 | #include "third_party/skia/include/core/SkTextBlob.h" 53 | #include "ui/gfx/geometry/rect_f.h" 54 | 55 | -#include "carbonyl/src/browser/bridge.h" 56 | +#include "carbonyl/src/browser/blink.h" 57 | 58 | namespace blink { 59 | 60 | @@ -158,7 +158,7 @@ void DrawBlobs(cc::PaintCanvas* canvas, 61 | const ShapeResultBloberizer::BlobBuffer& blobs, 62 | const gfx::PointF& point, 63 | cc::NodeId node_id = cc::kInvalidNodeId) { 64 | - if (!carbonyl::Bridge::BitmapMode()) { 65 | + if (!carbonyl::blink::BitmapMode()) { 66 | return; 67 | } 68 | 69 | @@ -237,7 +237,7 @@ void Font::DrawText(cc::PaintCanvas* canvas, 70 | if (ShouldSkipDrawing()) 71 | return; 72 | 73 | - if (!carbonyl::Bridge::BitmapMode()) { 74 | + if (!carbonyl::blink::BitmapMode()) { 75 | auto string = StringView( 76 | run_info.run.ToStringView(), 77 | run_info.from, 78 | @@ -285,7 +285,7 @@ void Font::DrawText(cc::PaintCanvas* canvas, 79 | if (ShouldSkipDrawing()) 80 | return; 81 | 82 | - if (!carbonyl::Bridge::BitmapMode()) { 83 | + if (!carbonyl::blink::BitmapMode()) { 84 | auto string = StringView( 85 | text_info.text, 86 | text_info.from, 87 | -------------------------------------------------------------------------------- /chromium/patches/chromium/0014-Move-Skia-text-rendering-control-to-bridge.patch: -------------------------------------------------------------------------------- 1 | From 2275364ee7e16ba6b46f0f339e34326d4a8c7584 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Mon, 13 Feb 2023 17:13:38 +0100 4 | Subject: [PATCH 14/14] Move Skia text rendering control to bridge 5 | 6 | --- 7 | content/renderer/render_frame_impl.cc | 5 ----- 8 | skia/BUILD.gn | 2 +- 9 | 2 files changed, 1 insertion(+), 6 deletions(-) 10 | 11 | diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc 12 | index 379cf6c58b2b0..891efd6a9d796 100644 13 | --- a/content/renderer/render_frame_impl.cc 14 | +++ b/content/renderer/render_frame_impl.cc 15 | @@ -285,7 +285,6 @@ 16 | #include "third_party/skia/include/svg/SkSVGCanvas.h" 17 | #include "third_party/skia/include/utils/SkBase64.h" 18 | #include "third_party/skia/src/text/GlyphRun.h" 19 | -#include "third_party/skia/src/core/SkBitmapDevice.h" 20 | #include "third_party/skia/src/core/SkClipStackDevice.h" 21 | #include "third_party/skia/src/core/SkDevice.h" 22 | #include "third_party/skia/src/core/SkFontPriv.h" 23 | @@ -2244,10 +2243,6 @@ void RenderFrameImpl::Initialize(blink::WebFrame* parent) { 24 | ); 25 | 26 | host->ObserveTerminalRender(render_callback_); 27 | - 28 | - if (!carbonyl::Bridge::BitmapMode()) { 29 | - SkBitmapDevice::DisableTextRendering(); 30 | - } 31 | } 32 | 33 | void RenderFrameImpl::GetInterface( 34 | diff --git a/skia/BUILD.gn b/skia/BUILD.gn 35 | index b330273c16db3..297ffacf073fa 100644 36 | --- a/skia/BUILD.gn 37 | +++ b/skia/BUILD.gn 38 | @@ -203,7 +203,7 @@ source_set("skcms") { 39 | } 40 | 41 | component("skia") { 42 | - deps = [] 43 | + deps = [ "//carbonyl/src/browser:bridge" ] 44 | sources = [ 45 | # Chrome sources. 46 | "config/SkUserConfig.h", 47 | -------------------------------------------------------------------------------- /chromium/patches/skia/0001-Disable-text-rendering.patch: -------------------------------------------------------------------------------- 1 | From 218fbf4bba772c465712c4ea442adb57968e9c22 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Mon, 13 Feb 2023 17:18:18 +0100 4 | Subject: [PATCH 1/2] Disable text rendering 5 | 6 | --- 7 | src/core/SkBitmapDevice.cpp | 8 ++++++-- 8 | 1 file changed, 6 insertions(+), 2 deletions(-) 9 | 10 | diff --git a/src/core/SkBitmapDevice.cpp b/src/core/SkBitmapDevice.cpp 11 | index b497d690f7..9631f47967 100644 12 | --- a/src/core/SkBitmapDevice.cpp 13 | +++ b/src/core/SkBitmapDevice.cpp 14 | @@ -28,6 +28,8 @@ 15 | #include "src/image/SkImage_Base.h" 16 | #include "src/text/GlyphRun.h" 17 | 18 | +#include "carbonyl/src/browser/bridge.h" 19 | + 20 | struct Bounder { 21 | SkRect fBounds; 22 | bool fHasBounds; 23 | @@ -522,8 +524,10 @@ void SkBitmapDevice::onDrawGlyphRunList(SkCanvas* canvas, 24 | const sktext::GlyphRunList& glyphRunList, 25 | const SkPaint& initialPaint, 26 | const SkPaint& drawingPaint) { 27 | - SkASSERT(!glyphRunList.hasRSXForm()); 28 | - LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr ) 29 | + if (carbonyl::Bridge::BitmapMode()) { 30 | + SkASSERT(!glyphRunList.hasRSXForm()); 31 | + LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr ) 32 | + } 33 | } 34 | 35 | void SkBitmapDevice::drawVertices(const SkVertices* vertices, 36 | -------------------------------------------------------------------------------- /chromium/patches/skia/0002-Export-some-private-APIs.patch: -------------------------------------------------------------------------------- 1 | From a271b203a2b60f0cd450bda0fa2cc14885f1d9a8 Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Thu, 9 Feb 2023 03:38:05 +0100 4 | Subject: [PATCH 2/2] Export some private APIs 5 | 6 | Temporary until TextCaptureDevice moves here 7 | --- 8 | include/utils/SkBase64.h | 2 +- 9 | src/core/SkClipStack.h | 2 +- 10 | src/core/SkClipStackDevice.h | 2 +- 11 | src/core/SkDevice.h | 2 +- 12 | src/core/SkFontPriv.h | 2 +- 13 | 5 files changed, 5 insertions(+), 5 deletions(-) 14 | 15 | diff --git a/include/utils/SkBase64.h b/include/utils/SkBase64.h 16 | index e01028543a..beddbd2c95 100644 17 | --- a/include/utils/SkBase64.h 18 | +++ b/include/utils/SkBase64.h 19 | @@ -12,7 +12,7 @@ 20 | 21 | #include 22 | 23 | -struct SkBase64 { 24 | +struct SK_API SkBase64 { 25 | public: 26 | enum Error { 27 | kNoError, 28 | diff --git a/src/core/SkClipStack.h b/src/core/SkClipStack.h 29 | index c325d2c619..d93b9cf37f 100644 30 | --- a/src/core/SkClipStack.h 31 | +++ b/src/core/SkClipStack.h 32 | @@ -30,7 +30,7 @@ class GrProxyProvider; 33 | // (i.e., the fSaveCount in force when it was added). Restores are thus 34 | // implemented by removing clips from fDeque that have an fSaveCount larger 35 | // then the freshly decremented count. 36 | -class SkClipStack { 37 | +class SK_API SkClipStack { 38 | public: 39 | enum BoundsType { 40 | // The bounding box contains all the pixels that can be written to 41 | diff --git a/src/core/SkClipStackDevice.h b/src/core/SkClipStackDevice.h 42 | index eff1f1a440..a8d6b4fe07 100644 43 | --- a/src/core/SkClipStackDevice.h 44 | +++ b/src/core/SkClipStackDevice.h 45 | @@ -11,7 +11,7 @@ 46 | #include "src/core/SkClipStack.h" 47 | #include "src/core/SkDevice.h" 48 | 49 | -class SkClipStackDevice : public SkBaseDevice { 50 | +class SK_API SkClipStackDevice : public SkBaseDevice { 51 | public: 52 | SkClipStackDevice(const SkImageInfo& info, const SkSurfaceProps& props) 53 | : SkBaseDevice(info, props) 54 | diff --git a/src/core/SkDevice.h b/src/core/SkDevice.h 55 | index e0fed94b9b..c7194f9c1c 100644 56 | --- a/src/core/SkDevice.h 57 | +++ b/src/core/SkDevice.h 58 | @@ -54,7 +54,7 @@ struct SkStrikeDeviceInfo { 59 | const sktext::gpu::SDFTControl* const fSDFTControl; 60 | }; 61 | 62 | -class SkBaseDevice : public SkRefCnt, public SkMatrixProvider { 63 | +class SK_API SkBaseDevice : public SkRefCnt, public SkMatrixProvider { 64 | public: 65 | SkBaseDevice(const SkImageInfo&, const SkSurfaceProps&); 66 | 67 | diff --git a/src/core/SkFontPriv.h b/src/core/SkFontPriv.h 68 | index 95ca905bf1..a31aba8e2b 100644 69 | --- a/src/core/SkFontPriv.h 70 | +++ b/src/core/SkFontPriv.h 71 | @@ -16,7 +16,7 @@ 72 | class SkReadBuffer; 73 | class SkWriteBuffer; 74 | 75 | -class SkFontPriv { 76 | +class SK_API SkFontPriv { 77 | public: 78 | /* This is the size we use when we ask for a glyph's path. We then 79 | * post-transform it as we draw to match the request. 80 | -------------------------------------------------------------------------------- /chromium/patches/webrtc/0001-Disable-GIO-on-Linux.patch: -------------------------------------------------------------------------------- 1 | From 5ad57c96f23739717bcea018baf2bc8f4157b01d Mon Sep 17 00:00:00 2001 2 | From: Fathy Boundjadj 3 | Date: Mon, 13 Feb 2023 16:37:40 +0100 4 | Subject: [PATCH] Disable GIO on Linux 5 | 6 | --- 7 | modules/portal/BUILD.gn | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/modules/portal/BUILD.gn b/modules/portal/BUILD.gn 11 | index 36bcb53e8e..822688b1dc 100644 12 | --- a/modules/portal/BUILD.gn 13 | +++ b/modules/portal/BUILD.gn 14 | @@ -85,7 +85,7 @@ if ((is_linux || is_chromeos) && rtc_use_pipewire) { 15 | # `rtc_use_pipewire` is not set, which causes pipewire_config to not be 16 | # included in targets. More details in: webrtc:13898 17 | if (is_linux && !is_castos) { 18 | - defines += [ "WEBRTC_USE_GIO" ] 19 | + $ defines += [ "WEBRTC_USE_GIO" ] 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | header = """ 3 | # Changelog\n 4 | All notable changes to this project will be documented in this file.\n 5 | """ 6 | body = """ 7 | {% if version %}\ 8 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 9 | {% else %}\ 10 | ## [unreleased] 11 | {% endif %}\ 12 | {% for group, commits in commits | group_by(attribute="group") %} 13 | ### {{ group | striptags | trim | upper_first }} 14 | {% for commit in commits %} 15 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 16 | {%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}){% endfor -%}){% endif %}\ 17 | {% endfor %} 18 | {% endfor %}\n 19 | """ 20 | trim = true 21 | footer = "" 22 | 23 | [git] 24 | conventional_commits = true 25 | filter_unconventional = true 26 | split_commits = false 27 | commit_preprocessors = [ 28 | { pattern = "#([0-9]+)", replace = "[#${1}](https://github.com/fathyb/carbonyl/issues/${1})" } 29 | ] 30 | commit_parsers = [ 31 | { message = "^feat", group = "🚀 Features" }, 32 | { message = "^fix", group = "🐛 Bug Fixes" }, 33 | { message = "^doc", group = "📖 Documentation" }, 34 | { message = "^perf", group = "⚡ Performance"}, 35 | { message = "^chore", skip = true }, 36 | { body = ".*security", group = "🔐 Security"}, 37 | ] 38 | protect_breaking_commits = false 39 | filter_commits = false 40 | tag_pattern = "v[0-9]*" 41 | skip_tags = "" 42 | ignore_tags = "" 43 | topo_order = true 44 | sort_commits = "oldest" 45 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | _Copyright © 2023, Fathy Boundjadj_ 2 | _All rights reserved._ 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the name of the copyright holder nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carbonyl", 3 | "version": "0.0.3", 4 | "license": "BSD-3-Clause" 5 | } 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 |
5 |

6 |
  7 |    O    O
  8 |     \  /
  9 | O —— Cr —— O
 10 |     /  \
 11 |    O    O
12 |

Carbonyl

17 | 18 | Carbonyl is a Chromium based browser built to run in a terminal. [Read the blog post](https://fathy.fr/carbonyl). 19 | 20 | It supports pretty much all Web APIs including WebGL, WebGPU, audio and video playback, animations, etc.. 21 | 22 | It's snappy, starts in less than a second, runs at 60 FPS, and idles at 0% CPU usage. It does not require a window server (i.e. works in a safe-mode console), and even runs through SSH. 23 | 24 | Carbonyl originally started as [`html2svg`](https://github.com/fathyb/html2svg) and is now the runtime behind it. 25 | 26 | ## Usage 27 | 28 | > Carbonyl on Linux without Docker requires the same dependencies as Chromium. 29 | 30 | ### Docker 31 | 32 | ```shell 33 | $ docker run --rm -ti fathyb/carbonyl https://youtube.com 34 | ``` 35 | 36 | ### npm 37 | 38 | ```console 39 | $ npm install --global carbonyl 40 | $ carbonyl https://github.com 41 | ``` 42 | 43 | ### Binaries 44 | 45 | - [macOS amd64](https://github.com/fathyb/carbonyl/releases/download/v0.0.3/carbonyl.macos-amd64.zip) 46 | - [macOS arm64](https://github.com/fathyb/carbonyl/releases/download/v0.0.3/carbonyl.macos-arm64.zip) 47 | - [Linux amd64](https://github.com/fathyb/carbonyl/releases/download/v0.0.3/carbonyl.linux-amd64.zip) 48 | - [Linux arm64](https://github.com/fathyb/carbonyl/releases/download/v0.0.3/carbonyl.linux-arm64.zip) 49 | 50 | ## Demo 51 | 52 | 53 | 54 | 55 | 58 | 61 | 62 | 63 | 66 | 67 | 68 |
56 | 59 |
64 |
69 | 70 | ## Known issues 71 | 72 | - Fullscreen mode not supported yet 73 | 74 | ## Comparisons 75 | 76 | ### Lynx 77 | 78 | Lynx is the original terminal web browser, and the oldest one still maintained. 79 | 80 | #### Pros 81 | 82 | - When it understands a page, Lynx has the best layout, fully optimized for the terminal 83 | 84 | #### Cons 85 | 86 | > Some might sound like pluses, but Browsh and Carbonyl let you disable most of those if you'd like 87 | 88 | - Does not support a lot of modern web standards 89 | - Cannot run JavaScript/WebAssembly 90 | - Cannot view or play media (audio, video, DOOM) 91 | 92 | ### Browsh 93 | 94 | Browsh is the original "normal browser into a terminal" project. It starts Firefox in headless mode and connects to it through an automation protocol. 95 | 96 | #### Pro 97 | 98 | - It's easier to update the underlying browser: just update Firefox 99 | - This makes development easier: just install Firefox and compile the Go code in a few seconds 100 | - As of today, Browsh supports extensions while Carbonyl doesn't, although it's on our roadmap 101 | 102 | #### Cons 103 | 104 | - It runs slower and requires more resources than Carbonyl. 50x more CPU power is needed for the same content in average, that's because Carbonyl does not downscale or copy the window framebuffer, it natively renders to the terminal resolution. 105 | - It uses custom stylesheets to fix the layout, which is less reliable than Carbonyl's changes to its HTML engine (Blink). 106 | 107 | ## Operating System Support 108 | 109 | As far as tested, the operating systems under are supported: 110 | 111 | - Linux (Debian, Ubuntu and Arch tested) 112 | - MacOS 113 | - Windows 11 and WSL 114 | 115 | ## Contributing 116 | 117 | Carbonyl is split in two parts: the "core" which is built into a shared library (`libcarbonyl`), and the "runtime" which dynamically loads the core (`carbonyl` executable). 118 | 119 | The core is written in Rust and takes a few seconds to build from scratch. The runtime is a modified version of the Chromium headless shell and takes more than an hour to build from scratch. 120 | 121 | If you're just making changes to the Rust code, build `libcarbonyl` and replace it in a release version of Carbonyl. 122 | 123 | ### Core 124 | 125 | ```console 126 | $ cargo build 127 | ``` 128 | 129 | ### Runtime 130 | 131 | Few notes: 132 | 133 | - Building the runtime is almost the same as building Chromium with extra steps to patch and bundle the Rust library. Scripts in the `scripts/` directory are simple wrappers around `gn`, `ninja`, etc.. 134 | - Building Chromium for arm64 on Linux requires an amd64 processor 135 | - Carbonyl is only tested on Linux and macOS, other platforms likely require code changes to Chromium 136 | - Chromium is huge and takes a long time to build, making your computer mostly unresponsive. An 8-core CPU such as an M1 Max or an i9 9900k with 10 Gbps fiber takes around ~1 hour to fetch and build. It requires around 100 GB of disk space. 137 | 138 | #### Fetch 139 | 140 | > Fetch Chromium's code. 141 | 142 | ```console 143 | $ ./scripts/gclient.sh sync 144 | ``` 145 | 146 | #### Apply patches 147 | 148 | > Any changes made to Chromium will be reverted, make sure to save any changes you made. 149 | 150 | ```console 151 | $ ./scripts/patches.sh apply 152 | ``` 153 | 154 | #### Configure 155 | 156 | ```console 157 | $ ./scripts/gn.sh args out/Default 158 | ``` 159 | 160 | > `Default` is the target name, you can use multiple ones and pick any name you'd like, i.e.: 161 | > 162 | > ```console 163 | > $ ./scripts/gn.sh args out/release 164 | > $ ./scripts/gn.sh args out/debug 165 | > # or if you'd like to build a multi-platform image 166 | > $ ./scripts/gn.sh args out/arm64 167 | > $ ./scripts/gn.sh args out/amd64 168 | > ``` 169 | 170 | When prompted, enter the following arguments: 171 | 172 | ```gn 173 | import("//carbonyl/src/browser/args.gn") 174 | 175 | # uncomment this to build for arm64 176 | # target_cpu = "arm64" 177 | 178 | # comment this to disable ccache 179 | cc_wrapper = "env CCACHE_SLOPPINESS=time_macros ccache" 180 | 181 | # comment this for a debug build 182 | is_debug = false 183 | symbol_level = 0 184 | is_official_build = true 185 | ``` 186 | 187 | #### Build binaries 188 | 189 | ```console 190 | $ ./scripts/build.sh Default 191 | ``` 192 | 193 | This should produce the following outputs: 194 | 195 | - `out/Default/headless_shell`: browser binary 196 | - `out/Default/icudtl.dat` 197 | - `out/Default/libEGL.so` 198 | - `out/Default/libGLESv2.so` 199 | - `out/Default/v8_context_snapshot.bin` 200 | 201 | #### Build Docker image 202 | 203 | ```console 204 | # Build arm64 Docker image using binaries from the Default target 205 | $ ./scripts/docker-build.sh Default arm64 206 | # Build amd64 Docker image using binaries from the Default target 207 | $ ./scripts/docker-build.sh Default amd64 208 | ``` 209 | 210 | #### Run 211 | 212 | ``` 213 | $ ./scripts/run.sh Default https://wikipedia.org 214 | ``` 215 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | export INSTALL_DEPOT_TOOLS="true" 5 | 6 | cd "$CARBONYL_ROOT" 7 | source scripts/env.sh 8 | 9 | target="$1" 10 | cpu="$2" 11 | 12 | if [ ! -z "$target" ]; then 13 | shift 14 | fi 15 | if [ ! -z "$cpu" ]; then 16 | shift 17 | fi 18 | 19 | triple=$(scripts/platform-triple.sh "$cpu") 20 | 21 | if [ -z "$CARBONYL_SKIP_CARGO_BUILD" ]; then 22 | if [ -z "$MACOSX_DEPLOYMENT_TARGET" ]; then 23 | export MACOSX_DEPLOYMENT_TARGET=10.13 24 | fi 25 | 26 | cargo build --target "$triple" --release 27 | fi 28 | 29 | if [ -f "build/$triple/release/libcarbonyl.dylib" ]; then 30 | cp "build/$triple/release/libcarbonyl.dylib" "$CHROMIUM_SRC/out/$target" 31 | install_name_tool \ 32 | -id @executable_path/libcarbonyl.dylib \ 33 | "build/$triple/release/libcarbonyl.dylib" 34 | else 35 | cp "build/$triple/release/libcarbonyl.so" "$CHROMIUM_SRC/out/$target" 36 | fi 37 | 38 | cd "$CHROMIUM_SRC/out/$target" 39 | 40 | ninja headless:headless_shell "$@" 41 | -------------------------------------------------------------------------------- /scripts/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | git cliff a69e8b609625b67a3e52e18f73ba5d0f49ceb7c3..HEAD "$@" > changelog.md 9 | -------------------------------------------------------------------------------- /scripts/copy-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | target="$1" 9 | cpu="$2" 10 | 11 | triple=$(scripts/platform-triple.sh "$cpu") 12 | dest="build/pre-built/$triple" 13 | src="$CHROMIUM_SRC/out/$target" 14 | 15 | lib_ext="so" 16 | if [ -f "$src"/libEGL.dylib ]; then 17 | lib_ext="dylib" 18 | fi 19 | 20 | rm -rf "$dest" 21 | mkdir -p "$dest" 22 | cd "$dest" 23 | 24 | cp "$src/headless_shell" carbonyl 25 | cp "$src/icudtl.dat" . 26 | cp "$src/libEGL.$lib_ext" . 27 | cp "$src/libGLESv2.$lib_ext" . 28 | cp "$src"/v8_context_snapshot*.bin . 29 | cp "$CARBONYL_ROOT/build/$triple/release/libcarbonyl.$lib_ext" . 30 | 31 | files="carbonyl " 32 | 33 | if [ "$lib_ext" == "so" ]; then 34 | cp "$src/libvk_swiftshader.so" . 35 | cp "$src/libvulkan.so.1" . 36 | cp "$src/vk_swiftshader_icd.json" . 37 | 38 | files+=$(echo *.so *.so.1) 39 | fi 40 | 41 | if [[ "$cpu" == "arm64" ]] && command -v aarch64-linux-gnu-strip; then 42 | aarch64-linux-gnu-strip $files 43 | else 44 | strip $files 45 | fi 46 | 47 | echo "Binaries copied to $dest" 48 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | cpu="$1" 9 | 10 | triple=$(scripts/platform-triple.sh "$cpu" linux) 11 | build_dir="build/docker/$triple" 12 | 13 | rm -rf "$build_dir" 14 | mkdir -p "build/docker" 15 | cp -r "$CARBONYL_ROOT/build/pre-built/$triple" "$build_dir" 16 | cp "$CARBONYL_ROOT/Dockerfile" "$build_dir" 17 | 18 | tag="fathyb/carbonyl:$cpu" 19 | 20 | docker buildx build "$build_dir" --load --platform "linux/$cpu" --tag "$tag" 21 | 22 | echo "Image tag: $tag" 23 | -------------------------------------------------------------------------------- /scripts/docker-push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | 5 | source "$CARBONYL_ROOT/scripts/env.sh" 6 | 7 | tag="fathyb/carbonyl" 8 | version="$1" 9 | 10 | echo "Pushing arm64 image as $tag:$version-arm64" 11 | docker tag "$tag:arm64" "$tag:$version-arm64" 12 | docker push "$tag:$version-arm64" 13 | 14 | echo "Pushing amd64 image as $tag:$version-amd64" 15 | docker tag "$tag:amd64" "$tag:$version-amd64" 16 | docker push "$tag:$version-amd64" 17 | 18 | docker manifest create "$tag:$version" \ 19 | --amend "$tag:$version-arm64" \ 20 | --amend "$tag:$version-amd64" 21 | 22 | docker manifest push "$tag:$version" --purge 23 | -------------------------------------------------------------------------------- /scripts/env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "$CARBONYL_ROOT" ]; then 6 | echo "CARBONYL_ROOT should be defined" 7 | 8 | exit 2 9 | fi 10 | 11 | if [ -z "$CHROMIUM_ROOT" ]; then 12 | export CHROMIUM_ROOT="$CARBONYL_ROOT/chromium" 13 | fi 14 | if [ -z "$CHROMIUM_SRC" ]; then 15 | export CHROMIUM_SRC="$CHROMIUM_ROOT/src" 16 | fi 17 | if [ -z "$DEPOT_TOOLS_ROOT" ]; then 18 | export DEPOT_TOOLS_ROOT="$CHROMIUM_ROOT/depot_tools" 19 | fi 20 | 21 | export PATH="$PATH:$DEPOT_TOOLS_ROOT" 22 | 23 | if [ "$INSTALL_DEPOT_TOOLS" = "true" ] && [ ! -f "$DEPOT_TOOLS_ROOT/README.md" ]; then 24 | echo "depot_tools not found, fetching submodule.." 25 | 26 | git -C "$CARBONYL_ROOT" submodule update --init --recursive 27 | fi 28 | -------------------------------------------------------------------------------- /scripts/gclient.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | export INSTALL_DEPOT_TOOLS="true" 5 | 6 | source "$CARBONYL_ROOT/scripts/env.sh" 7 | 8 | ( 9 | cd "$CHROMIUM_ROOT" && 10 | gclient "$@" 11 | ) 12 | -------------------------------------------------------------------------------- /scripts/gn.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | export INSTALL_DEPOT_TOOLS="true" 5 | 6 | source "$CARBONYL_ROOT/scripts/env.sh" 7 | 8 | ( 9 | cd "$CHROMIUM_SRC" && 10 | gn "$@" 11 | ) 12 | -------------------------------------------------------------------------------- /scripts/npm-package.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | const pkg = JSON.parse( 7 | await fs.readFile(path.resolve(dirname, "../package.json"), "utf-8") 8 | ); 9 | const version = process.env.RELEASE_MODE 10 | ? pkg.version 11 | : `${pkg.version}-next.${process.env.VERSION_ID}`; 12 | const manifest = { 13 | version, 14 | license: "BSD-3-Clause", 15 | description: "Chromium running in your terminal", 16 | homepage: "https://github.com/fathyb/carbonyl", 17 | repository: "fathyb/carbonyl", 18 | bugs: "https://github.com/fathyb/carbonyl/issues", 19 | author: { 20 | name: "Fathy Boundjadj", 21 | email: "hey@fathy.fr", 22 | url: "https://fathy.fr", 23 | }, 24 | }; 25 | 26 | async function buildMain() { 27 | const root = path.resolve(dirname, "../build/packages/carbonyl"); 28 | 29 | await fs.rm(root, { recursive: true, force: true }); 30 | await fs.mkdir(root, { recursive: true }); 31 | await Promise.all([ 32 | Promise.all( 33 | ["readme.md", "license.md"].map((file) => 34 | fs.cp(path.join(dirname, "..", file), path.join(root, file)) 35 | ) 36 | ), 37 | fs.writeFile( 38 | path.join(root, "package.json"), 39 | JSON.stringify( 40 | { 41 | name: "carbonyl", 42 | ...manifest, 43 | files: ["index.sh", "index.sh.js", "index.js"], 44 | bin: { carbonyl: "index.sh" }, 45 | optionalDependencies: { 46 | "@fathyb/carbonyl-linux-amd64": version, 47 | "@fathyb/carbonyl-linux-arm64": version, 48 | "@fathyb/carbonyl-macos-amd64": version, 49 | "@fathyb/carbonyl-macos-arm64": version, 50 | }, 51 | }, 52 | null, 53 | 4 54 | ) 55 | ), 56 | fs.writeFile( 57 | path.join(root, "index.sh"), 58 | ["#!/usr/bin/env bash", `"$(node "$(realpath "$0")".js)" "$@"`].join( 59 | "\n" 60 | ), 61 | { mode: "755" } 62 | ), 63 | fs.writeFile( 64 | path.join(root, "index.sh.js"), 65 | `process.stdout.write(require('.'))` 66 | ), 67 | fs.writeFile( 68 | path.join(root, "index.js"), 69 | ` 70 | const archs = { 71 | x64: 'amd64', 72 | arm64: 'arm64', 73 | } 74 | const platforms = { 75 | linux: 'linux', 76 | darwin: 'macos', 77 | } 78 | 79 | const arch = archs[process.arch] 80 | const platform = platforms[process.platform] 81 | 82 | if (!arch) { 83 | throw new Error('Processor architecture not supported: ' + process.arch) 84 | } 85 | if (!platform) { 86 | throw new Error('Platform not supported: ' + process.platform) 87 | } 88 | 89 | module.exports = require('@fathyb/carbonyl-' + platform + '-' + arch) 90 | ` 91 | ), 92 | ]); 93 | 94 | return root; 95 | } 96 | async function buildPlatform([os, npmOs, llvmOs], [cpu, npmCpu, llvmCpu]) { 97 | const pkg = `carbonyl-${os}-${cpu}`; 98 | const root = path.resolve(dirname, `../build/packages/${pkg}`); 99 | 100 | await fs.rm(root, { recursive: true, force: true }); 101 | await fs.mkdir(root, { recursive: true }); 102 | await Promise.all([ 103 | Promise.all( 104 | ["readme.md", "license.md"].map((file) => 105 | fs.cp(path.join(dirname, "..", file), path.join(root, file)) 106 | ) 107 | ), 108 | fs.cp( 109 | path.join(dirname, `../build/pre-built/${llvmCpu}-${llvmOs}`), 110 | path.join(root, "build"), 111 | { recursive: true } 112 | ), 113 | fs.writeFile( 114 | path.join(root, "package.json"), 115 | JSON.stringify( 116 | { 117 | name: `@fathyb/${pkg}`, 118 | ...manifest, 119 | files: ["build", "index.js"], 120 | os: [npmOs], 121 | cpu: [npmCpu], 122 | }, 123 | null, 124 | 4 125 | ) 126 | ), 127 | fs.writeFile( 128 | path.join(root, "index.js"), 129 | `module.exports = __dirname + '/build/carbonyl'` 130 | ), 131 | ]); 132 | 133 | return root; 134 | } 135 | 136 | const [root, platforms] = await Promise.all([ 137 | buildMain(), 138 | 139 | Promise.all( 140 | [ 141 | ["macos", "darwin", "apple-darwin"], 142 | ["linux", "linux", "unknown-linux-gnu"], 143 | ].map( 144 | async (os) => 145 | await Promise.all( 146 | [ 147 | ["arm64", "arm64", "aarch64"], 148 | ["amd64", "x64", "x86_64"], 149 | ].map(async (cpu) => await buildPlatform(os, cpu)) 150 | ) 151 | ) 152 | ), 153 | ]); 154 | -------------------------------------------------------------------------------- /scripts/npm-package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | VERSION_ID="$(git rev-parse --short HEAD)" \ 9 | node "$CARBONYL_ROOT/scripts/npm-package.mjs" 10 | -------------------------------------------------------------------------------- /scripts/npm-publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | cd "build/packages" 9 | 10 | if [ -z "$CARBONYL_PUBLISH_PLATFORM" ] && [ -z "$CARBONYL_PUBLISH_ARCH" ]; then 11 | cd "carbonyl" 12 | else 13 | cd "carbonyl-$CARBONYL_PUBLISH_PLATFORM-$CARBONYL_PUBLISH_ARCH" 14 | fi 15 | 16 | yarn publish --non-interactive --access public "$@" 17 | -------------------------------------------------------------------------------- /scripts/patches.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | 5 | source "$CARBONYL_ROOT/scripts/env.sh" 6 | 7 | cd "$CHROMIUM_SRC" 8 | 9 | chromium_upstream="92da8189788b1b373cbd3348f73d695dfdc521b6" 10 | skia_upstream="486deb23bc2a4d3d09c66fef52c2ad64d8b4f761" 11 | webrtc_upstream="727080cbacd58a2f303ed8a03f0264fe1493e47a" 12 | 13 | if [[ "$1" == "apply" ]]; then 14 | echo "Stashing Chromium changes.." 15 | git add -A . 16 | git stash 17 | 18 | echo "Applying Chromium patches.." 19 | git checkout "$chromium_upstream" 20 | git am --committer-date-is-author-date "$CARBONYL_ROOT/chromium/patches/chromium"/* 21 | "$CARBONYL_ROOT/scripts/restore-mtime.sh" "$chromium_upstream" 22 | 23 | echo "Stashing Skia changes.." 24 | cd "$CHROMIUM_SRC/third_party/skia" 25 | git add -A . 26 | git stash 27 | 28 | echo "Applying Skia patches.." 29 | git checkout "$skia_upstream" 30 | git am --committer-date-is-author-date "$CARBONYL_ROOT/chromium/patches/skia"/* 31 | "$CARBONYL_ROOT/scripts/restore-mtime.sh" "$skia_upstream" 32 | 33 | echo "Stashing WebRTC changes.." 34 | cd "$CHROMIUM_SRC/third_party/webrtc" 35 | git add -A . 36 | git stash 37 | 38 | echo "Applying WebRTC patches.." 39 | git checkout "$webrtc_upstream" 40 | git am --committer-date-is-author-date "$CARBONYL_ROOT/chromium/patches/webrtc"/* 41 | "$CARBONYL_ROOT/scripts/restore-mtime.sh" "$webrtc_upstream" 42 | 43 | echo "Patches successfully applied" 44 | elif [[ "$1" == "save" ]]; then 45 | if [[ -d carbonyl ]]; then 46 | git add -A carbonyl 47 | fi 48 | 49 | echo "Updating Chromium patches.." 50 | rm -rf "$CARBONYL_ROOT/chromium/patches/chromium" 51 | git format-patch --no-signature --output-directory "$CARBONYL_ROOT/chromium/patches/chromium" "$chromium_upstream" 52 | 53 | echo "Updating Skia patches.." 54 | cd "$CHROMIUM_SRC/third_party/skia" 55 | rm -rf "$CARBONYL_ROOT/chromium/patches/skia" 56 | git format-patch --no-signature --output-directory "$CARBONYL_ROOT/chromium/patches/skia" "$skia_upstream" 57 | 58 | echo "Updating WebRTC patches.." 59 | cd "$CHROMIUM_SRC/third_party/webrtc" 60 | rm -rf "$CARBONYL_ROOT/chromium/patches/webrtc" 61 | git format-patch --no-signature --output-directory "$CARBONYL_ROOT/chromium/patches/webrtc" "$webrtc_upstream" 62 | 63 | echo "Patches successfully updated" 64 | else 65 | echo "Unknown argument: $1" 66 | 67 | exit 2 68 | fi 69 | -------------------------------------------------------------------------------- /scripts/platform-triple.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | source "$CARBONYL_ROOT/scripts/env.sh" 6 | 7 | cpu="$1" 8 | platform="$2" 9 | 10 | if [ -z "$platform" ]; then 11 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 12 | platform="linux" 13 | elif [[ "$OSTYPE" == "darwin"* ]]; then 14 | platform="macos" 15 | else 16 | echo "Unsupported platform: $OSTYPE" 17 | 18 | exit 2 19 | fi 20 | fi 21 | 22 | if [ "$platform" == "linux" ]; then 23 | platform="unknown-linux-gnu" 24 | elif [ "$platform" == "macos" ]; then 25 | platform="apple-darwin" 26 | fi 27 | 28 | if [ -z "$cpu" ]; then 29 | cpu="$(uname -m)" 30 | fi 31 | 32 | if [[ "$cpu" == "arm64" ]]; then 33 | cpu="aarch64" 34 | elif [[ "$cpu" == "amd64" ]]; then 35 | cpu="x86_64" 36 | fi 37 | 38 | echo -n "$cpu-$platform" 39 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | npm version "$1" --no-git-tag-version 9 | "$CARBONYL_ROOT/scripts/changelog.sh" --tag "$1" 10 | git add -A . 11 | git commit -m "chore(release): $1" 12 | git tag -a "v$1" -m "chore(release): $1" 13 | -------------------------------------------------------------------------------- /scripts/restore-mtime.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | source "$CARBONYL_ROOT/scripts/env.sh" 6 | 7 | for file in $(git diff --name-only HEAD "$1"); do 8 | mtime="$(git log --pretty=format:%cd -n 1 --date=format:%Y%m%d%H%M.%S "$file")" 9 | 10 | touch -t "$mtime" "$file" 11 | done 12 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- "$(pwd)") 4 | 5 | source "$CARBONYL_ROOT/scripts/env.sh" 6 | 7 | target="$1" 8 | shift 9 | 10 | "$CHROMIUM_SRC/out/$target/headless_shell" "$@" 11 | -------------------------------------------------------------------------------- /scripts/runtime-hash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | for file in chromium/.gclient chromium/patches/*/*.patch src/browser/*.{cc,h,gn,mojom}; do 9 | file_sha=$(cat "$file" | openssl sha256) 10 | result=$(echo -n "$sha/${file_sha: -64}" | openssl sha256) 11 | sha+="${file_sha: -64} ${file}"$'\n' 12 | done 13 | 14 | hash=$(echo "$sha" | sort | openssl sha256) 15 | 16 | echo -n "${hash: -16}" 17 | -------------------------------------------------------------------------------- /scripts/runtime-pull.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | echo "Computing Chromium patches sha.." 9 | 10 | sha="$(scripts/runtime-hash.sh)" 11 | triple="$(scripts/platform-triple.sh "$@")" 12 | 13 | if [ ! -f "build/pre-built/$triple.tgz" ]; then 14 | url="https://carbonyl.fathy.fr/runtime/$sha/$triple.tgz" 15 | 16 | echo "Downloading pre-built binaries from $url" 17 | 18 | mkdir -p build/pre-built 19 | 20 | if ! curl --silent --fail --output "build/pre-built/$triple.tgz" "$url"; then 21 | echo "Pre-built binaries not available" 22 | 23 | exit 1 24 | fi 25 | fi 26 | 27 | echo "Pre-build binaries available, extracting.." 28 | 29 | cd build/pre-built 30 | rm -rf "$triple" 31 | tar -xvzf "$triple.tgz" 32 | -------------------------------------------------------------------------------- /scripts/runtime-push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CARBONYL_ROOT=$(cd $(dirname -- "$0") && dirname -- $(pwd)) 4 | 5 | cd "$CARBONYL_ROOT" 6 | source "scripts/env.sh" 7 | 8 | echo "Computing Chromium patches hash.." 9 | 10 | hash=$(scripts/runtime-hash.sh) 11 | triple=$(scripts/platform-triple.sh "$1") 12 | 13 | echo "Archiving binaries.." 14 | 15 | cd build/pre-built 16 | tar cvzf "$triple.tgz" "$triple" 17 | 18 | echo "Binaries archived to build/pre-built/$triple.tgz" 19 | 20 | echo "Pushing $triple.tgz to object storage.." 21 | 22 | AWS_PAGER="" \ 23 | AWS_ACCESS_KEY_ID="$CDN_ACCESS_KEY_ID" \ 24 | AWS_SECRET_ACCESS_KEY="$CDN_SECRET_ACCESS_KEY" \ 25 | aws s3api put-object \ 26 | --endpoint-url "https://7985f304d3a79d71fb63aeb17a31fe30.r2.cloudflarestorage.com" \ 27 | --bucket "carbonyl-runtime" \ 28 | --key "runtime/$hash/$triple.tgz" \ 29 | --body "$triple.tgz" 30 | -------------------------------------------------------------------------------- /src/browser.rs: -------------------------------------------------------------------------------- 1 | mod bridge; 2 | 3 | pub use bridge::*; 4 | -------------------------------------------------------------------------------- /src/browser/BUILD.gn: -------------------------------------------------------------------------------- 1 | import("//build/config/compiler/compiler.gni") 2 | import("//mojo/public/tools/bindings/mojom.gni") 3 | 4 | mojom("mojom") { 5 | sources = [ "carbonyl.mojom" ] 6 | 7 | deps = [ 8 | "//ui/gfx/geometry/mojom", 9 | "//skia/public/mojom", 10 | ] 11 | } 12 | 13 | component("bridge") { 14 | output_name = "carbonyl_bridge" 15 | defines = [ "CARBONYL_BRIDGE_IMPLEMENTATION" ] 16 | sources = [ 17 | "bridge.cc", 18 | "bridge.h", 19 | ] 20 | } 21 | 22 | component("viz") { 23 | output_name = "carbonyl_viz" 24 | defines = [ "CARBONYL_VIZ_IMPLEMENTATION" ] 25 | sources = [ 26 | "host_display_client.cc", 27 | "host_display_client.h", 28 | ] 29 | 30 | deps = [ 31 | ":renderer", 32 | "//base", 33 | "//components/viz/host", 34 | "//services/viz/privileged/mojom", 35 | ] 36 | } 37 | 38 | config("lib") { 39 | target = "" 40 | 41 | if (current_cpu == "x64") { 42 | target += "x86_64-" 43 | } else if (current_cpu == "arm64") { 44 | target += "aarch64-" 45 | } 46 | 47 | if (is_mac) { 48 | target += "apple-darwin" 49 | } else if (is_linux) { 50 | target += "unknown-linux-gnu" 51 | } 52 | 53 | libs = ["carbonyl"] 54 | lib_dirs = ["//carbonyl/build/$target/release"] 55 | } 56 | 57 | component("renderer") { 58 | output_name = "carbonyl_renderer" 59 | defines = [ "CARBONYL_RENDERER_IMPLEMENTATION" ] 60 | sources = [ 61 | "render_service_impl.cc", 62 | "render_service_impl.h", 63 | "renderer.cc", 64 | "renderer.h", 65 | ] 66 | 67 | configs += [ ":lib" ] 68 | deps = [ 69 | ":mojom", 70 | ":bridge", 71 | "//base", 72 | "//skia" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/browser/args.gn: -------------------------------------------------------------------------------- 1 | headless_enable_commands = false 2 | headless_use_embedded_resources = true 3 | 4 | # Enable proprietary codecs such as H.264 5 | ffmpeg_branding = "Chrome" 6 | proprietary_codecs = true 7 | 8 | # Disable unused dependencies 9 | ozone_platform = "headless" 10 | ozone_platform_x11 = false 11 | 12 | use_static_angle = true 13 | 14 | use_qt = false 15 | use_gio = false 16 | use_gtk = false 17 | use_cups = false 18 | use_dbus = false 19 | use_glib = false 20 | use_libpci = false 21 | use_kerberos = false 22 | use_vaapi_x11 = false 23 | use_xkbcommon = false 24 | 25 | angle_use_x11 = false 26 | angle_use_wayland = false 27 | 28 | rtc_use_x11 = false 29 | rtc_use_pipewire = false 30 | rtc_use_x11_extensions = false 31 | 32 | # Linux only 33 | # use_wayland_gbm = false 34 | # use_system_libdrm = false 35 | # use_system_minigbm = false 36 | 37 | # Disable unused features 38 | enable_pdf = false 39 | enable_nacl = false 40 | enable_ppapi = false 41 | enable_printing = false 42 | enable_print_content_analysis = false 43 | enable_plugins = false 44 | enable_rust_json = false 45 | enable_tagged_pdf = false 46 | enable_media_remoting = false 47 | enable_speech_service = false 48 | enable_component_updater = false 49 | enable_screen_ai_service = false 50 | enable_system_notifications = false 51 | enable_browser_speech_service = false 52 | enable_webui_certificate_viewer = false 53 | -------------------------------------------------------------------------------- /src/browser/bridge.cc: -------------------------------------------------------------------------------- 1 | #include "carbonyl/src/browser/bridge.h" 2 | 3 | namespace { 4 | 5 | float dpi_ = 0.0; 6 | bool bitmap_mode_ = false; 7 | 8 | } 9 | 10 | namespace carbonyl { 11 | 12 | void Bridge::Resize() {} 13 | 14 | float Bridge::GetDPI() { 15 | return dpi_; 16 | } 17 | 18 | bool Bridge::BitmapMode() { 19 | return bitmap_mode_; 20 | } 21 | 22 | void Bridge::Configure(float dpi, bool bitmap_mode) { 23 | dpi_ = dpi; 24 | bitmap_mode_ = bitmap_mode; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/browser/bridge.h: -------------------------------------------------------------------------------- 1 | #ifndef CARBONYL_SRC_BROWSER_BRIDGE_H_ 2 | #define CARBONYL_SRC_BROWSER_BRIDGE_H_ 3 | 4 | #include "carbonyl/src/browser/export.h" 5 | 6 | namespace carbonyl { 7 | 8 | class Renderer; 9 | 10 | class CARBONYL_BRIDGE_EXPORT Bridge { 11 | public: 12 | static float GetDPI(); 13 | static bool BitmapMode(); 14 | 15 | private: 16 | friend class Renderer; 17 | 18 | static void Resize(); 19 | static void Configure(float dpi, bool bitmap_mode); 20 | }; 21 | 22 | } 23 | 24 | #endif // CARBONYL_SRC_BROWSER_BRIDGE_H_ 25 | -------------------------------------------------------------------------------- /src/browser/carbonyl.mojom: -------------------------------------------------------------------------------- 1 | // Our C++ bindings will be in the carbonyl::mojom namespace 2 | module carbonyl.mojom; 3 | 4 | // Import existing bindings to common structures 5 | import "ui/gfx/geometry/mojom/geometry.mojom"; 6 | import "skia/public/mojom/skcolor.mojom"; 7 | 8 | // Define a structure to hold text to render 9 | struct TextData { 10 | // An UTF-8 string with the contents 11 | string contents; 12 | // Bounds, size only defined for clearing 13 | gfx.mojom.RectF bounds; 14 | // Color of the text 15 | skia.mojom.SkColor color; 16 | }; 17 | 18 | // The browser process runs this service 19 | interface CarbonylRenderService { 20 | // The renderer process calls this method 21 | DrawText(array data); 22 | }; 23 | -------------------------------------------------------------------------------- /src/browser/export.h: -------------------------------------------------------------------------------- 1 | #ifndef CARBONYL_SRC_BROWSER_BRIDGE_EXPORT_H_ 2 | #define CARBONYL_SRC_BROWSER_BRIDGE_EXPORT_H_ 3 | 4 | // CARBONYL_BRIDGE_EXPORT 5 | #if defined(COMPONENT_BUILD) 6 | 7 | #if defined(WIN32) 8 | 9 | #if defined(CARBONYL_BRIDGE_IMPLEMENTATION) 10 | #define CARBONYL_BRIDGE_EXPORT __declspec(dllexport) 11 | #else 12 | #define CARBONYL_BRIDGE_EXPORT __declspec(dllimport) 13 | #endif 14 | 15 | #else // !defined(WIN32) 16 | 17 | #if defined(CARBONYL_BRIDGE_IMPLEMENTATION) 18 | #define CARBONYL_BRIDGE_EXPORT __attribute__((visibility("default"))) 19 | #else 20 | #define CARBONYL_BRIDGE_EXPORT 21 | #endif 22 | 23 | #endif 24 | 25 | #else // !defined(COMPONENT_BUILD) 26 | 27 | #define CARBONYL_BRIDGE_EXPORT 28 | 29 | #endif 30 | 31 | // CARBONYL_RENDERER_EXPORT 32 | #if defined(COMPONENT_BUILD) 33 | 34 | #if defined(WIN32) 35 | 36 | #if defined(CARBONYL_RENDERER_IMPLEMENTATION) 37 | #define CARBONYL_RENDERER_EXPORT __declspec(dllexport) 38 | #else 39 | #define CARBONYL_RENDERER_EXPORT __declspec(dllimport) 40 | #endif 41 | 42 | #else // !defined(WIN32) 43 | 44 | #if defined(CARBONYL_RENDERER_IMPLEMENTATION) 45 | #define CARBONYL_RENDERER_EXPORT __attribute__((visibility("default"))) 46 | #else 47 | #define CARBONYL_RENDERER_EXPORT 48 | #endif 49 | 50 | #endif 51 | 52 | #else // !defined(COMPONENT_BUILD) 53 | 54 | #define CARBONYL_RENDERER_EXPORT 55 | 56 | #endif 57 | 58 | // CARBONYL_VIZ_EXPORT 59 | #if defined(COMPONENT_BUILD) 60 | 61 | #if defined(WIN32) 62 | 63 | #if defined(CARBONYL_VIZ_IMPLEMENTATION) 64 | #define CARBONYL_VIZ_EXPORT __declspec(dllexport) 65 | #else 66 | #define CARBONYL_VIZ_EXPORT __declspec(dllimport) 67 | #endif 68 | 69 | #else // !defined(WIN32) 70 | 71 | #if defined(CARBONYL_VIZ_IMPLEMENTATION) 72 | #define CARBONYL_VIZ_EXPORT __attribute__((visibility("default"))) 73 | #else 74 | #define CARBONYL_VIZ_EXPORT 75 | #endif 76 | 77 | #endif 78 | 79 | #else // !defined(COMPONENT_BUILD) 80 | 81 | #define CARBONYL_VIZ_EXPORT 82 | 83 | #endif 84 | 85 | #endif // CARBONYL_SRC_BROWSER_BRIDGE_EXPORT_H_ 86 | -------------------------------------------------------------------------------- /src/browser/host_display_client.cc: -------------------------------------------------------------------------------- 1 | #include "carbonyl/src/browser/host_display_client.h" 2 | 3 | #include 4 | 5 | #include "components/viz/common/resources/resource_format.h" 6 | #include "components/viz/common/resources/resource_sizes.h" 7 | #include "mojo/public/cpp/system/platform_handle.h" 8 | #include "skia/ext/platform_canvas.h" 9 | #include "third_party/skia/include/core/SkColor.h" 10 | #include "third_party/skia/include/core/SkRect.h" 11 | #include "third_party/skia/src/core/SkDevice.h" 12 | #include "ui/gfx/skia_util.h" 13 | 14 | #if BUILDFLAG(IS_WIN) 15 | #include "skia/ext/skia_utils_win.h" 16 | #endif 17 | 18 | #include "carbonyl/src/browser/renderer.h" 19 | 20 | namespace carbonyl { 21 | 22 | LayeredWindowUpdater::LayeredWindowUpdater( 23 | mojo::PendingReceiver receiver 24 | ) 25 | : 26 | receiver_(this, std::move(receiver)), 27 | task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) 28 | {} 29 | 30 | LayeredWindowUpdater::~LayeredWindowUpdater() = default; 31 | 32 | void LayeredWindowUpdater::OnAllocatedSharedMemory( 33 | const gfx::Size& pixel_size, 34 | base::UnsafeSharedMemoryRegion region) { 35 | if (region.IsValid()) 36 | shm_mapping_ = region.Map(); 37 | 38 | pixel_size_ = pixel_size; 39 | } 40 | 41 | void LayeredWindowUpdater::Draw(const gfx::Rect& damage_rect, 42 | DrawCallback callback) { 43 | Renderer::GetCurrent()->DrawBitmap( 44 | shm_mapping_.GetMemoryAs(), 45 | pixel_size_, 46 | damage_rect, 47 | base::BindOnce( 48 | []( 49 | scoped_refptr task_runner, 50 | DrawCallback callback 51 | ) { 52 | task_runner->PostTask(FROM_HERE, std::move(callback)); 53 | }, 54 | task_runner_, 55 | std::move(callback) 56 | ) 57 | ); 58 | } 59 | 60 | HostDisplayClient::HostDisplayClient() 61 | : viz::HostDisplayClient(gfx::kNullAcceleratedWidget) {} 62 | HostDisplayClient::~HostDisplayClient() = default; 63 | 64 | void HostDisplayClient::CreateLayeredWindowUpdater( 65 | mojo::PendingReceiver receiver) { 66 | layered_window_updater_ = 67 | std::make_unique(std::move(receiver)); 68 | } 69 | 70 | #if BUILDFLAG(IS_MAC) 71 | void HostDisplayClient::OnDisplayReceivedCALayerParams( 72 | const gfx::CALayerParams& ca_layer_params) {} 73 | #endif 74 | 75 | #if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS) 76 | void HostDisplayClient::DidCompleteSwapWithNewSize( 77 | const gfx::Size& size) {} 78 | #endif 79 | 80 | } // namespace carbonyl 81 | -------------------------------------------------------------------------------- /src/browser/host_display_client.h: -------------------------------------------------------------------------------- 1 | #ifndef CARBONYL_SRC_BROWSER_HOST_DISPLAY_CLIENT_H_ 2 | #define CARBONYL_SRC_BROWSER_HOST_DISPLAY_CLIENT_H_ 3 | 4 | #include 5 | 6 | #include "base/callback.h" 7 | #include "base/memory/shared_memory_mapping.h" 8 | #include "carbonyl/src/browser/export.h" 9 | #include "components/viz/host/host_display_client.h" 10 | #include "services/viz/privileged/mojom/compositing/layered_window_updater.mojom.h" 11 | #include "ui/gfx/native_widget_types.h" 12 | 13 | namespace carbonyl { 14 | 15 | typedef base::RepeatingCallback 16 | OnPaintCallback; 17 | 18 | class CARBONYL_VIZ_EXPORT LayeredWindowUpdater : public viz::mojom::LayeredWindowUpdater { 19 | public: 20 | explicit LayeredWindowUpdater( 21 | mojo::PendingReceiver receiver); 22 | ~LayeredWindowUpdater() override; 23 | 24 | // disable copy 25 | LayeredWindowUpdater(const LayeredWindowUpdater&) = delete; 26 | LayeredWindowUpdater& operator=(const LayeredWindowUpdater&) = delete; 27 | 28 | // viz::mojom::LayeredWindowUpdater implementation. 29 | void OnAllocatedSharedMemory(const gfx::Size& pixel_size, 30 | base::UnsafeSharedMemoryRegion region) override; 31 | void Draw(const gfx::Rect& damage_rect, DrawCallback draw_callback) override; 32 | 33 | private: 34 | mojo::Receiver receiver_; 35 | base::WritableSharedMemoryMapping shm_mapping_; 36 | gfx::Size pixel_size_; 37 | DrawCallback callback_; 38 | scoped_refptr task_runner_; 39 | base::WeakPtrFactory weak_ptr_factory_ { this }; 40 | }; 41 | 42 | class CARBONYL_VIZ_EXPORT HostDisplayClient : public viz::HostDisplayClient { 43 | public: 44 | explicit HostDisplayClient(); 45 | ~HostDisplayClient() override; 46 | 47 | // disable copy 48 | HostDisplayClient(const HostDisplayClient&) = delete; 49 | HostDisplayClient& operator=(const HostDisplayClient&) = 50 | delete; 51 | 52 | private: 53 | #if BUILDFLAG(IS_MAC) 54 | void OnDisplayReceivedCALayerParams( 55 | const gfx::CALayerParams& ca_layer_params) override; 56 | #endif 57 | 58 | void CreateLayeredWindowUpdater( 59 | mojo::PendingReceiver receiver) 60 | override; 61 | 62 | #if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS) 63 | void DidCompleteSwapWithNewSize(const gfx::Size& size) override; 64 | #endif 65 | 66 | std::unique_ptr layered_window_updater_; 67 | OnPaintCallback callback_; 68 | }; 69 | 70 | } // namespace carbonyl 71 | 72 | #endif // CARBONYL_SRC_BROWSER_HOST_DISPLAY_CLIENT_H_ 73 | -------------------------------------------------------------------------------- /src/browser/render_service_impl.cc: -------------------------------------------------------------------------------- 1 | #include "carbonyl/src/browser/render_service_impl.h" 2 | 3 | #include 4 | 5 | #include "carbonyl/src/browser/renderer.h" 6 | 7 | namespace carbonyl { 8 | 9 | CarbonylRenderServiceImpl::CarbonylRenderServiceImpl( 10 | mojo::PendingReceiver receiver): 11 | receiver_(this, std::move(receiver)) 12 | {} 13 | 14 | CarbonylRenderServiceImpl::~CarbonylRenderServiceImpl() = default; 15 | 16 | void CarbonylRenderServiceImpl::DrawText(std::vector data) { 17 | std::vector mapped; 18 | 19 | for (auto& text: data) { 20 | mapped.emplace_back(text->contents, text->bounds, text->color); 21 | } 22 | 23 | Renderer::GetCurrent()->DrawText(mapped); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/browser/render_service_impl.h: -------------------------------------------------------------------------------- 1 | #ifndef CARBONYL_SRC_BROWSER_RENDER_SERVICE_IMPL_H_ 2 | #define CARBONYL_SRC_BROWSER_RENDER_SERVICE_IMPL_H_ 3 | 4 | #include "carbonyl/src/browser/export.h" 5 | #include "carbonyl/src/browser/carbonyl.mojom.h" 6 | #include "mojo/public/cpp/bindings/pending_receiver.h" 7 | #include "mojo/public/cpp/bindings/receiver.h" 8 | 9 | namespace carbonyl { 10 | 11 | class CARBONYL_RENDERER_EXPORT CarbonylRenderServiceImpl: public mojom::CarbonylRenderService { 12 | public: 13 | explicit CarbonylRenderServiceImpl(mojo::PendingReceiver receiver); 14 | CarbonylRenderServiceImpl(const CarbonylRenderServiceImpl&) = delete; 15 | CarbonylRenderServiceImpl& operator=(const CarbonylRenderServiceImpl&) = delete; 16 | 17 | ~CarbonylRenderServiceImpl() override; 18 | 19 | // carbonyl::mojom::CarbonylRenderService: 20 | void DrawText(std::vector data) override; 21 | 22 | private: 23 | mojo::Receiver receiver_; 24 | }; 25 | 26 | } // namespace carbonyl 27 | 28 | #endif // CARBONYL_SRC_BROWSER_RENDER_SERVICE_IMPL_H_ -------------------------------------------------------------------------------- /src/browser/renderer.cc: -------------------------------------------------------------------------------- 1 | #include "carbonyl/src/browser/renderer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "base/functional/callback.h" 8 | #include "carbonyl/src/browser/bridge.h" 9 | #include "ui/gfx/geometry/rect_f.h" 10 | #include "third_party/skia/include/core/SkColor.h" 11 | 12 | extern "C" { 13 | 14 | struct carbonyl_renderer_size { 15 | unsigned int width; 16 | unsigned int height; 17 | }; 18 | struct carbonyl_renderer_point { 19 | unsigned int x; 20 | unsigned int y; 21 | }; 22 | struct carbonyl_renderer_rect { 23 | struct carbonyl_renderer_point origin; 24 | struct carbonyl_renderer_size size; 25 | }; 26 | struct carbonyl_renderer_color { 27 | uint8_t r; 28 | uint8_t g; 29 | uint8_t b; 30 | }; 31 | struct carbonyl_renderer_text { 32 | const char* text; 33 | carbonyl_renderer_rect rect; 34 | carbonyl_renderer_color color; 35 | }; 36 | 37 | void carbonyl_bridge_main(); 38 | bool carbonyl_bridge_bitmap_mode(); 39 | float carbonyl_bridge_get_dpi(); 40 | 41 | struct carbonyl_renderer* carbonyl_renderer_create(); 42 | void carbonyl_renderer_start(struct carbonyl_renderer* renderer); 43 | void carbonyl_renderer_resize(struct carbonyl_renderer* renderer); 44 | struct carbonyl_renderer_size carbonyl_renderer_get_size(struct carbonyl_renderer* renderer); 45 | void carbonyl_renderer_push_nav(struct carbonyl_renderer* renderer, const char* url, bool can_go_back, bool can_go_forward); 46 | void carbonyl_renderer_set_title(struct carbonyl_renderer* renderer, const char* title); 47 | void carbonyl_renderer_clear_text(struct carbonyl_renderer* renderer); 48 | void carbonyl_renderer_listen(struct carbonyl_renderer* renderer, const struct carbonyl_renderer_browser_delegate* delegate); 49 | void carbonyl_renderer_draw_text( 50 | struct carbonyl_renderer* renderer, 51 | const struct carbonyl_renderer_text* text, 52 | size_t text_size 53 | ); 54 | void carbonyl_renderer_draw_bitmap( 55 | struct carbonyl_renderer* renderer, 56 | const unsigned char* pixels, 57 | const struct carbonyl_renderer_size size, 58 | const struct carbonyl_renderer_rect rect, 59 | void (*callback) (void*), 60 | void* callback_data 61 | ); 62 | 63 | } 64 | 65 | namespace carbonyl { 66 | 67 | namespace { 68 | static std::unique_ptr globalInstance; 69 | } 70 | 71 | Renderer::Renderer(struct carbonyl_renderer* ptr): ptr_(ptr) {} 72 | 73 | void Renderer::Main() { 74 | carbonyl_bridge_main(); 75 | 76 | Bridge::Configure( 77 | carbonyl_bridge_get_dpi(), 78 | carbonyl_bridge_bitmap_mode() 79 | ); 80 | } 81 | 82 | Renderer* Renderer::GetCurrent() { 83 | if (!globalInstance) { 84 | globalInstance = std::unique_ptr( 85 | new Renderer(carbonyl_renderer_create()) 86 | ); 87 | } 88 | 89 | return globalInstance.get(); 90 | } 91 | 92 | void Renderer::StartRenderer() { 93 | carbonyl_renderer_start(ptr_); 94 | } 95 | 96 | gfx::Size Renderer::GetSize() { 97 | auto size = carbonyl_renderer_get_size(ptr_); 98 | 99 | return gfx::Size(size.width, size.height); 100 | } 101 | 102 | gfx::Size Renderer::Resize() { 103 | carbonyl_renderer_resize(ptr_); 104 | Bridge::Resize(); 105 | 106 | return GetSize(); 107 | } 108 | 109 | void Renderer::Listen(const struct carbonyl_renderer_browser_delegate* delegate) { 110 | carbonyl_renderer_listen(ptr_, delegate); 111 | } 112 | 113 | void Renderer::PushNav(const std::string& url, bool can_go_back, bool can_go_forward) { 114 | if (!url.size()) { 115 | return; 116 | } 117 | 118 | carbonyl_renderer_push_nav(ptr_, url.c_str(), can_go_back, can_go_forward); 119 | } 120 | 121 | void Renderer::SetTitle(const std::string& title) { 122 | if (!title.size()) { 123 | return; 124 | } 125 | 126 | carbonyl_renderer_set_title(ptr_, title.c_str()); 127 | } 128 | 129 | void Renderer::DrawText(const std::vector& text) { 130 | struct carbonyl_renderer_text data[text.size()]; 131 | 132 | for (size_t i = 0; i < text.size(); i++) { 133 | data[i].text = text[i].text.c_str(); 134 | data[i].color.r = SkColorGetR(text[i].color); 135 | data[i].color.g = SkColorGetG(text[i].color); 136 | data[i].color.b = SkColorGetB(text[i].color); 137 | data[i].rect.origin.x = text[i].rect.x(); 138 | data[i].rect.origin.y = text[i].rect.y(); 139 | data[i].rect.size.width = std::ceil(text[i].rect.width()); 140 | data[i].rect.size.height = std::ceil(text[i].rect.height()); 141 | } 142 | 143 | carbonyl_renderer_draw_text(ptr_, data, text.size()); 144 | } 145 | 146 | void Renderer::DrawBitmap( 147 | const unsigned char* pixels, 148 | const gfx::Size& pixels_size, 149 | const gfx::Rect& damage, 150 | base::OnceCallback callback 151 | ) { 152 | auto* box = new base::OnceCallback(std::move(callback)); 153 | 154 | carbonyl_renderer_draw_bitmap( 155 | ptr_, 156 | pixels, 157 | { 158 | .width = (unsigned int)pixels_size.width(), 159 | .height = (unsigned int)pixels_size.height(), 160 | }, 161 | { 162 | .origin = { 163 | .x = (unsigned int)damage.x(), 164 | .y = (unsigned int)damage.y(), 165 | }, 166 | .size = { 167 | .width = (unsigned int)damage.width(), 168 | .height = (unsigned int)damage.height(), 169 | }, 170 | }, 171 | [](void* box) { 172 | auto* ptr = static_cast*>(box); 173 | 174 | std::move(*ptr).Run(); 175 | delete ptr; 176 | }, 177 | box 178 | ); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/browser/renderer.h: -------------------------------------------------------------------------------- 1 | #ifndef CARBONYL_SRC_BROWSER_RENDERER_H_ 2 | #define CARBONYL_SRC_BROWSER_RENDERER_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "base/functional/callback.h" 8 | #include "carbonyl/src/browser/export.h" 9 | #include "ui/gfx/geometry/rect_f.h" 10 | 11 | extern "C" { 12 | 13 | struct carbonyl_renderer; 14 | struct carbonyl_renderer_browser_delegate { 15 | void (*shutdown) (); 16 | void (*refresh) (); 17 | void (*go_to) (const char* url); 18 | void (*go_back) (); 19 | void (*go_forward) (); 20 | void (*scroll) (int); 21 | void (*key_press) (char); 22 | void (*mouse_up) (unsigned int, unsigned int); 23 | void (*mouse_down) (unsigned int, unsigned int); 24 | void (*mouse_move) (unsigned int, unsigned int); 25 | void (*post_task) (void (*)(void*), void*); 26 | }; 27 | 28 | } /* end extern "C" */ 29 | 30 | namespace carbonyl { 31 | 32 | struct CARBONYL_RENDERER_EXPORT Text { 33 | Text( 34 | std::string text, 35 | gfx::RectF rect, 36 | uint32_t color 37 | ): 38 | text(text), 39 | rect(rect), 40 | color(color) 41 | {} 42 | 43 | std::string text; 44 | gfx::RectF rect; 45 | uint32_t color; 46 | }; 47 | 48 | class CARBONYL_RENDERER_EXPORT Renderer { 49 | public: 50 | static void Main(); 51 | static Renderer* GetCurrent(); 52 | 53 | gfx::Size GetSize(); 54 | 55 | gfx::Size Resize(); 56 | void StartRenderer(); 57 | void Listen(const struct carbonyl_renderer_browser_delegate* delegate); 58 | void PushNav(const std::string& url, bool can_go_back, bool can_go_forward); 59 | void SetTitle(const std::string& title); 60 | void DrawText(const std::vector& text); 61 | void DrawBitmap( 62 | const unsigned char* pixels, 63 | const gfx::Size& size, 64 | const gfx::Rect& damage, 65 | base::OnceCallback callback 66 | ); 67 | 68 | private: 69 | Renderer(struct carbonyl_renderer* ptr); 70 | 71 | struct carbonyl_renderer* ptr_; 72 | }; 73 | 74 | } 75 | 76 | #endif // CARBONYL_SRC_BROWSER_RENDERER_H_ 77 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod program; 3 | 4 | pub use cli::*; 5 | pub use program::*; 6 | -------------------------------------------------------------------------------- /src/cli/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{env, ffi::OsStr}; 2 | 3 | use super::CommandLineProgram; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct CommandLine { 7 | pub args: Vec, 8 | pub fps: f32, 9 | pub zoom: f32, 10 | pub debug: bool, 11 | pub bitmap: bool, 12 | pub program: CommandLineProgram, 13 | pub shell_mode: bool, 14 | } 15 | 16 | pub enum EnvVar { 17 | Debug, 18 | Bitmap, 19 | ShellMode, 20 | } 21 | 22 | impl EnvVar { 23 | pub fn as_str(&self) -> &'static str { 24 | match self { 25 | EnvVar::Debug => "CARBONYL_ENV_DEBUG", 26 | EnvVar::Bitmap => "CARBONYL_ENV_BITMAP", 27 | EnvVar::ShellMode => "CARBONYL_ENV_SHELL_MODE", 28 | } 29 | } 30 | } 31 | 32 | impl AsRef for EnvVar { 33 | fn as_ref(&self) -> &OsStr { 34 | self.as_str().as_ref() 35 | } 36 | } 37 | 38 | impl CommandLine { 39 | pub fn parse() -> CommandLine { 40 | let mut fps = 60.0; 41 | let mut zoom = 1.0; 42 | let mut debug = false; 43 | let mut bitmap = false; 44 | let mut shell_mode = false; 45 | let mut program = CommandLineProgram::Main; 46 | let args = env::args().skip(1).collect::>(); 47 | 48 | for arg in &args { 49 | let split: Vec<&str> = arg.split("=").collect(); 50 | let default = arg.as_str(); 51 | let (key, value) = (split.get(0).unwrap_or(&default), split.get(1)); 52 | 53 | macro_rules! set { 54 | ($var:ident, $enum:ident) => {{ 55 | $var = true; 56 | 57 | env::set_var(EnvVar::$enum, "1"); 58 | }}; 59 | } 60 | 61 | macro_rules! set_f32 { 62 | ($var:ident = $expr:expr) => {{ 63 | if let Some(value) = value { 64 | if let Some(value) = value.parse::().ok() { 65 | $var = { 66 | let $var = value; 67 | 68 | $expr 69 | }; 70 | } 71 | } 72 | }}; 73 | } 74 | 75 | match *key { 76 | "-f" | "--fps" => set_f32!(fps = fps), 77 | "-z" | "--zoom" => set_f32!(zoom = zoom / 100.0), 78 | "-d" | "--debug" => set!(debug, Debug), 79 | "-b" | "--bitmap" => set!(bitmap, Bitmap), 80 | 81 | "-h" | "--help" => program = CommandLineProgram::Help, 82 | "-v" | "--version" => program = CommandLineProgram::Version, 83 | _ => (), 84 | } 85 | } 86 | 87 | if env::var(EnvVar::Debug).is_ok() { 88 | debug = true; 89 | } 90 | 91 | if env::var(EnvVar::Bitmap).is_ok() { 92 | bitmap = true; 93 | } 94 | 95 | if env::var(EnvVar::ShellMode).is_ok() { 96 | shell_mode = true; 97 | } 98 | 99 | CommandLine { 100 | args, 101 | fps, 102 | zoom, 103 | debug, 104 | bitmap, 105 | program, 106 | shell_mode, 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cli/program.rs: -------------------------------------------------------------------------------- 1 | use super::CommandLine; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum CommandLineProgram { 5 | Main, 6 | Help, 7 | Version, 8 | } 9 | 10 | impl CommandLineProgram { 11 | pub fn parse_or_run() -> Option { 12 | let cmd = CommandLine::parse(); 13 | 14 | match cmd.program { 15 | CommandLineProgram::Main => return Some(cmd), 16 | CommandLineProgram::Help => { 17 | println!("{}", include_str!("usage.txt")) 18 | } 19 | CommandLineProgram::Version => { 20 | println!("Carbonyl {}", env!("CARGO_PKG_VERSION")) 21 | } 22 | } 23 | 24 | None 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/usage.txt: -------------------------------------------------------------------------------- 1 | O O Carbonyl is a Chromium based browser for the terminal. 2 | \ / 3 | O —— Cr —— O In addition to the following options, 4 | / \ Carbonyl also supports most Chromium options. 5 | O O 6 | 7 | Usage: carbonyl [options] [url] 8 | 9 | Options: 10 | -f, --fps= set the maximum number of frames per second (default: 60) 11 | -z, --zoom= set the zoom level in percent (default: 100) 12 | -b, --bitmap render text as bitmaps 13 | -d, --debug enable debug logs 14 | -h, --help display this help message 15 | -v, --version output the version number 16 | -------------------------------------------------------------------------------- /src/gfx.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | mod point; 3 | mod rect; 4 | mod size; 5 | mod vector; 6 | 7 | pub use color::*; 8 | pub use point::*; 9 | pub use rect::*; 10 | pub use size::*; 11 | pub use vector::*; 12 | -------------------------------------------------------------------------------- /src/gfx/color.rs: -------------------------------------------------------------------------------- 1 | use super::Vector3; 2 | use crate::impl_vector_overload; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq)] 5 | pub struct Color { 6 | pub r: T, 7 | pub g: T, 8 | pub b: T, 9 | } 10 | 11 | impl Color { 12 | pub fn from_iter<'a, T>(iter: &mut T) -> Option 13 | where 14 | T: Iterator, 15 | { 16 | let (b, g, r, _) = (iter.next(), iter.next(), iter.next(), iter.next()); 17 | 18 | Some(Color::::new(*r?, *g?, *b?)) 19 | } 20 | 21 | pub fn black() -> Color { 22 | Color::::new(0, 0, 0) 23 | } 24 | } 25 | 26 | impl_vector_overload!(Color r g b); 27 | -------------------------------------------------------------------------------- /src/gfx/point.rs: -------------------------------------------------------------------------------- 1 | use super::{Rect, Vector2}; 2 | use crate::impl_vector_overload; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 5 | pub struct Point { 6 | pub x: T, 7 | pub y: T, 8 | } 9 | 10 | impl Point { 11 | pub fn inside(&self, rect: Rect) -> bool { 12 | self.x >= rect.origin.x 13 | && self.y >= rect.origin.y 14 | && self.x < rect.origin.x + rect.size.width as i32 15 | && self.y < rect.origin.y + rect.size.height as i32 16 | } 17 | } 18 | 19 | impl_vector_overload!(Point x y); 20 | -------------------------------------------------------------------------------- /src/gfx/rect.rs: -------------------------------------------------------------------------------- 1 | use super::{Point, Size}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq)] 4 | pub struct Rect { 5 | pub origin: Point

, 6 | pub size: Size, 7 | } 8 | 9 | impl Rect { 10 | pub fn new(x: P, y: P, width: S, height: S) -> Self { 11 | Self { 12 | origin: Point::new(x, y), 13 | size: Size::new(width, height), 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/gfx/size.rs: -------------------------------------------------------------------------------- 1 | use super::Vector2; 2 | use crate::impl_vector_overload; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq, Default)] 5 | pub struct Size { 6 | pub width: T, 7 | pub height: T, 8 | } 9 | 10 | impl_vector_overload!(Size width height); 11 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | mod dcs; 2 | mod keyboard; 3 | mod listen; 4 | mod mouse; 5 | mod parser; 6 | mod tty; 7 | 8 | pub use dcs::*; 9 | pub use keyboard::*; 10 | pub use listen::*; 11 | pub use mouse::*; 12 | pub use parser::*; 13 | pub use tty::*; 14 | -------------------------------------------------------------------------------- /src/input/dcs.rs: -------------------------------------------------------------------------------- 1 | mod control_flow; 2 | mod parser; 3 | mod resource; 4 | mod status; 5 | 6 | pub use parser::*; 7 | -------------------------------------------------------------------------------- /src/input/dcs/control_flow.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! control_flow { 3 | (break) => { 4 | std::ops::ControlFlow::Break(None) 5 | }; 6 | ($expr:expr; break) => {{ 7 | $expr; 8 | 9 | std::ops::ControlFlow::Break(None) 10 | }}; 11 | (break $expr:expr) => { 12 | std::ops::ControlFlow::Break($expr.into()) 13 | }; 14 | 15 | (continue) => { 16 | std::ops::ControlFlow::Continue(None) 17 | }; 18 | ($expr:expr; continue) => {{ 19 | $expr; 20 | 21 | std::ops::ControlFlow::Continue(None) 22 | }}; 23 | (continue $expr:expr) => { 24 | std::ops::ControlFlow::Continue($expr.into()) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/input/dcs/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::{control_flow, input::ParseControlFlow}; 2 | 3 | use super::{resource::*, status::*}; 4 | 5 | #[derive(Default, Clone)] 6 | enum Sequence { 7 | #[default] 8 | Code, 9 | Type(u8), 10 | Status(StatusParser), 11 | Resource(ResourceParser), 12 | } 13 | 14 | #[derive(Default, Clone)] 15 | pub struct DeviceControl { 16 | sequence: Sequence, 17 | } 18 | 19 | impl DeviceControl { 20 | pub fn new() -> Self { 21 | Self::default() 22 | } 23 | 24 | pub fn parse(&mut self, key: u8) -> ParseControlFlow { 25 | use Sequence::*; 26 | 27 | self.sequence = match self.sequence { 28 | Code => match key { 29 | b'0' | b'1' => Type(key), 30 | _ => control_flow!(break)?, 31 | }, 32 | Type(code) => match key { 33 | b'$' => Status(StatusParser::new(code)), 34 | b'+' => Resource(ResourceParser::new(code)), 35 | _ => control_flow!(break)?, 36 | }, 37 | Status(ref mut status) => return status.parse(key), 38 | Resource(ref mut resource) => return resource.parse(key), 39 | }; 40 | 41 | control_flow!(continue) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/input/dcs/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | control_flow, 3 | input::{Event, ParseControlFlow, TerminalEvent}, 4 | }; 5 | 6 | #[derive(Copy, Clone)] 7 | enum Sequence { 8 | Start, 9 | Name, 10 | Value, 11 | Terminator, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub struct ResourceParser { 16 | code: u8, 17 | sequence: Sequence, 18 | name: Vec, 19 | value: Vec, 20 | } 21 | 22 | impl ResourceParser { 23 | pub fn new(code: u8) -> Self { 24 | Self { 25 | code, 26 | sequence: Sequence::Start, 27 | name: Vec::new(), 28 | value: Vec::new(), 29 | } 30 | } 31 | 32 | pub fn parse(&mut self, key: u8) -> ParseControlFlow { 33 | use Sequence::*; 34 | 35 | self.sequence = match self.sequence { 36 | Start => match key { 37 | b'r' => Name, 38 | _ => control_flow!(break)?, 39 | }, 40 | Name => match key { 41 | 0x1b => Terminator, 42 | b'=' => Value, 43 | key => self.push_char(key), 44 | }, 45 | Value => match key { 46 | 0x1b => Terminator, 47 | key => self.push_char(key), 48 | }, 49 | Terminator => control_flow!(break self.parse_event(key))?, 50 | }; 51 | 52 | control_flow!(continue) 53 | } 54 | 55 | fn push_char(&mut self, key: u8) -> Sequence { 56 | match self.sequence { 57 | Sequence::Name => self.name.push(key), 58 | Sequence::Value => self.value.push(key), 59 | _ => (), 60 | } 61 | 62 | self.sequence 63 | } 64 | 65 | fn parse_event(&self, key: u8) -> Option { 66 | if key == b'\\' && self.code == b'1' { 67 | let name = read_hex_string(self.name.as_slice()); 68 | let value = read_hex_string(self.value.as_slice()); 69 | 70 | if let (Some(name), Some(value)) = (name, value) { 71 | if name == "TN" { 72 | return Some(Event::Terminal(TerminalEvent::Name(value))); 73 | } 74 | } 75 | } 76 | 77 | None 78 | } 79 | } 80 | 81 | fn read_hex_string(str: &[u8]) -> Option { 82 | let mut iter = str.into_iter(); 83 | let mut vec = Vec::with_capacity(str.len() / 2); 84 | 85 | loop { 86 | match (iter.next(), iter.next()) { 87 | (Some(left), Some(right)) => { 88 | let chunk = [*left, *right]; 89 | let hex = std::str::from_utf8(&chunk).ok()?; 90 | 91 | vec.push(u8::from_str_radix(hex, 16).ok()?) 92 | } 93 | _ => break, 94 | } 95 | } 96 | 97 | Some(std::str::from_utf8(&vec).ok()?.to_owned()) 98 | } 99 | -------------------------------------------------------------------------------- /src/input/dcs/status.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | control_flow, 3 | input::{Event, ParseControlFlow, TerminalEvent}, 4 | }; 5 | 6 | #[derive(Default, Clone)] 7 | enum Sequence { 8 | #[default] 9 | Start, 10 | Value, 11 | Terminator, 12 | } 13 | 14 | #[derive(Default, Clone)] 15 | pub struct StatusParser { 16 | code: u8, 17 | op: Option, 18 | sequence: Sequence, 19 | buffer: Vec, 20 | values: Vec, 21 | } 22 | 23 | impl StatusParser { 24 | pub fn new(code: u8) -> Self { 25 | let mut parser = Self::default(); 26 | 27 | parser.code = code; 28 | 29 | parser 30 | } 31 | 32 | pub fn parse(&mut self, key: u8) -> ParseControlFlow { 33 | use Sequence::*; 34 | 35 | self.sequence = match self.sequence { 36 | Start => match key { 37 | b'r' => Value, 38 | _ => control_flow!(break)?, 39 | }, 40 | Value => match key { 41 | 0x1b => self.terminate(), 42 | b';' => self.push_value(), 43 | char => self.push_char(char), 44 | }, 45 | Terminator => control_flow!(break self.parse_event(key))?, 46 | }; 47 | 48 | control_flow!(continue) 49 | } 50 | 51 | fn terminate(&mut self) -> Sequence { 52 | self.op = self.buffer.pop(); 53 | 54 | self.push_value(); 55 | 56 | Sequence::Terminator 57 | } 58 | 59 | fn push_char(&mut self, key: u8) -> Sequence { 60 | self.buffer.push(key); 61 | 62 | Sequence::Value 63 | } 64 | 65 | fn push_value(&mut self) -> Sequence { 66 | if let Ok(str) = String::from_utf8(std::mem::take(&mut self.buffer)) { 67 | self.values.push(str); 68 | } 69 | 70 | Sequence::Value 71 | } 72 | 73 | fn parse_event(&self, key: u8) -> Option { 74 | if key == b'\\' && self.code == b'1' && self.op == Some(b'm') { 75 | for value in &self.values { 76 | let mut val = 0; 77 | let mut set = Vec::new(); 78 | 79 | for &char in value.as_bytes() { 80 | match char { 81 | b'0'..=b'9' => val = val * 10 + char - b'0', 82 | b':' => set.push(std::mem::take(&mut val)), 83 | _ => break, 84 | } 85 | } 86 | 87 | set.push(val); 88 | 89 | if set.len() > 4 && set[1] == 2 && (set[0] == 38 || set[0] == 48) { 90 | return Some(Event::Terminal(TerminalEvent::TrueColorSupported)); 91 | } 92 | } 93 | } 94 | 95 | None 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/input/keyboard.rs: -------------------------------------------------------------------------------- 1 | use crate::control_flow; 2 | 3 | use super::{Event, ParseControlFlow}; 4 | 5 | pub struct Keyboard { 6 | state: State, 7 | } 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Key { 11 | pub char: u8, 12 | pub modifiers: KeyModifiers, 13 | } 14 | 15 | #[derive(Clone, Debug, Default)] 16 | pub struct KeyModifiers { 17 | pub alt: bool, 18 | pub meta: bool, 19 | pub shift: bool, 20 | pub control: bool, 21 | } 22 | 23 | enum State { 24 | Separator, 25 | Modifier(u8), 26 | } 27 | 28 | impl Keyboard { 29 | pub fn new() -> Self { 30 | Self { 31 | state: State::Separator, 32 | } 33 | } 34 | pub fn key(key: u8, modifiers: u8) -> Option { 35 | let modifiers = KeyModifiers::parse(modifiers); 36 | let char = match key { 37 | // Up 38 | b'A' => 0x11, 39 | // Down 40 | b'B' => 0x12, 41 | // Right 42 | b'C' => 0x13, 43 | // Left 44 | b'D' => 0x14, 45 | _ => return None, 46 | }; 47 | 48 | Some(Event::KeyPress { 49 | key: Key { char, modifiers }, 50 | }) 51 | } 52 | 53 | pub fn parse(&mut self, key: u8) -> ParseControlFlow { 54 | self.state = match self.state { 55 | State::Separator => match key { 56 | b';' => State::Modifier(0), 57 | _ => control_flow!(break)?, 58 | }, 59 | State::Modifier(code) => match key { 60 | b'0'..=b'9' => State::Modifier(code * 10 + key - b'0'), 61 | key => control_flow!(break Self::key(key, code))?, 62 | }, 63 | }; 64 | 65 | control_flow!(continue) 66 | } 67 | } 68 | 69 | impl From for Key { 70 | fn from(char: u8) -> Self { 71 | Self { 72 | char, 73 | modifiers: KeyModifiers::default(), 74 | } 75 | } 76 | } 77 | 78 | impl KeyModifiers { 79 | pub fn parse(key: u8) -> Self { 80 | let (alt, meta, shift, control) = (0b1000, 0b0100, 0b0010, 0b0001); 81 | let mask = match key { 82 | 2 => shift, 83 | 3 => alt, 84 | 4 => shift | alt, 85 | 5 => control, 86 | 6 => shift | control, 87 | 7 => alt | control, 88 | 8 => shift | alt | control, 89 | 9 => meta, 90 | 10 => meta | shift, 91 | 11 => meta | alt, 92 | 12 => meta | alt | shift, 93 | 13 => meta | control, 94 | 14 => meta | control | shift, 95 | 15 => meta | control | alt, 96 | 16 => meta | control | alt | shift, 97 | _ => 0, 98 | }; 99 | 100 | KeyModifiers { 101 | alt: alt & mask != 0, 102 | meta: meta & mask != 0, 103 | shift: shift & mask != 0, 104 | control: control & mask != 0, 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/input/listen.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | 3 | use crate::input::*; 4 | 5 | /// Listen for input events in stdin. 6 | /// This will block, so it should run from a dedicated thread. 7 | pub fn listen(mut callback: F) -> io::Result<()> 8 | where 9 | F: FnMut(Vec), 10 | { 11 | let mut buf = [0u8; 1024]; 12 | let mut stdin = io::stdin(); 13 | let mut parser = Parser::new(); 14 | 15 | loop { 16 | // Wait for some input 17 | let size = stdin.read(&mut buf)?; 18 | let read = parser.parse(&buf[0..size]); 19 | let mut scroll = 0; 20 | let mut events = Vec::with_capacity(read.len()); 21 | 22 | for event in read { 23 | match event { 24 | Event::Exit => return Ok(()), 25 | Event::Scroll { delta } => scroll += delta, 26 | event => events.push(event), 27 | } 28 | } 29 | 30 | if scroll != 0 { 31 | events.push(Event::Scroll { delta: scroll }) 32 | } 33 | 34 | callback(events) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/input/mouse.rs: -------------------------------------------------------------------------------- 1 | use std::ops::BitAnd; 2 | 3 | use crate::{control_flow, utils::log}; 4 | 5 | use super::{Event, ParseControlFlow}; 6 | 7 | #[derive(Default, Clone, Debug)] 8 | pub struct Mouse { 9 | buf: Vec, 10 | btn: Option, 11 | col: Option, 12 | row: Option, 13 | } 14 | 15 | impl Mouse { 16 | pub fn new() -> Self { 17 | Self::default() 18 | } 19 | 20 | pub fn parse(&mut self, key: u8) -> ParseControlFlow { 21 | match key { 22 | b'm' | b'M' => control_flow!(break self.get(key)), 23 | b';' => match self.read() { 24 | None => control_flow!(break), 25 | Some(()) => control_flow!(continue), 26 | }, 27 | key => control_flow!(self.buf.push(key); continue), 28 | } 29 | } 30 | 31 | fn read(&mut self) -> Option<()> { 32 | let buf = std::mem::take(&mut self.buf); 33 | let str = std::str::from_utf8(&buf).ok()?; 34 | let num = Some(str.parse().ok()?); 35 | 36 | match (self.btn, self.col, self.row) { 37 | (None, _, _) => self.btn = num, 38 | (_, None, _) => self.col = num, 39 | (_, _, None) => self.row = num, 40 | _ => { 41 | log::warning!("Malformed mouse sequence"); 42 | 43 | return None; 44 | } 45 | } 46 | 47 | return Some(()); 48 | } 49 | 50 | fn get(&mut self, key: u8) -> Option { 51 | let (btn, col, row) = { 52 | self.read()?; 53 | 54 | (self.btn?, self.col?, self.row?) 55 | }; 56 | 57 | Some({ 58 | if Mask::ScrollDown & btn { 59 | Event::Scroll { delta: -1 } 60 | } else if Mask::ScrollUp & btn { 61 | Event::Scroll { delta: 1 } 62 | } else { 63 | let col = col as usize - 1; 64 | let row = row as usize - 1; 65 | 66 | if key == b'm' { 67 | Event::MouseUp { row, col } 68 | } else if Mask::MouseMove & btn { 69 | Event::MouseMove { row, col } 70 | } else { 71 | Event::MouseDown { row, col } 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | 78 | enum Mask { 79 | MouseMove = 0x20, 80 | ScrollUp = 0x40, 81 | ScrollDown = 0x41, 82 | } 83 | 84 | impl BitAnd for Mask { 85 | type Output = bool; 86 | 87 | fn bitand(self, rhs: u32) -> bool { 88 | let mask = self as u32; 89 | 90 | mask & rhs == mask 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/input/parser.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow; 2 | 3 | use crate::input::*; 4 | 5 | #[derive(Default)] 6 | pub struct Parser { 7 | events: Vec, 8 | sequence: Sequence, 9 | } 10 | 11 | #[derive(Default)] 12 | enum Sequence { 13 | #[default] 14 | Char, 15 | Escape, 16 | Control, 17 | Mouse(Mouse), 18 | Keyboard(Keyboard), 19 | DeviceControl(DeviceControl), 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub enum TerminalEvent { 24 | Name(String), 25 | TrueColorSupported, 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub enum Event { 30 | KeyPress { key: Key }, 31 | MouseUp { row: usize, col: usize }, 32 | MouseDown { row: usize, col: usize }, 33 | MouseMove { row: usize, col: usize }, 34 | Scroll { delta: isize }, 35 | Terminal(TerminalEvent), 36 | Exit, 37 | } 38 | 39 | pub type ParseControlFlow = ControlFlow, Option>; 40 | 41 | impl Parser { 42 | pub fn new() -> Parser { 43 | Self::default() 44 | } 45 | 46 | pub fn parse(&mut self, input: &[u8]) -> Vec { 47 | let mut sequence = std::mem::take(&mut self.sequence); 48 | 49 | macro_rules! emit { 50 | ($event:expr) => {{ 51 | if let Some(event) = $event.into() { 52 | self.events.push(event); 53 | } 54 | 55 | Sequence::Char 56 | }}; 57 | ($event:expr; continue) => {{ 58 | if let Some(event) = $event.into() { 59 | self.events.push(event); 60 | } 61 | 62 | continue; 63 | }}; 64 | } 65 | macro_rules! parse { 66 | ($parser:expr, $key:expr) => ( 67 | match $parser.parse($key) { 68 | ControlFlow::Break(None) => Sequence::Char, 69 | ControlFlow::Break(Some(event)) => emit!(event), 70 | ControlFlow::Continue(None) => continue, 71 | ControlFlow::Continue(Some(event)) => emit!(event; continue), 72 | } 73 | ); 74 | } 75 | 76 | for &key in input { 77 | sequence = match sequence { 78 | Sequence::Char => match key { 79 | 0x1b => Sequence::Escape, 80 | 0x03 => emit!(Event::Exit), 81 | key => emit!(Event::KeyPress { key: key.into() }), 82 | }, 83 | Sequence::Escape => match key { 84 | b'[' => Sequence::Control, 85 | b'P' => Sequence::DeviceControl(DeviceControl::new()), 86 | 0x1b => emit!(Event::KeyPress { key: 0x1b.into() }; continue), 87 | key => { 88 | emit!(Event::KeyPress { key: 0x1b.into() }); 89 | emit!(Event::KeyPress { key: key.into() }) 90 | } 91 | }, 92 | Sequence::Control => match key { 93 | b'<' => Sequence::Mouse(Mouse::new()), 94 | b'1' => Sequence::Keyboard(Keyboard::new()), 95 | key => emit!(Keyboard::key(key, 0)), 96 | }, 97 | Sequence::Mouse(ref mut mouse) => parse!(mouse, key), 98 | Sequence::Keyboard(ref mut keyboard) => parse!(keyboard, key), 99 | Sequence::DeviceControl(ref mut dcs) => parse!(dcs, key), 100 | } 101 | } 102 | 103 | self.sequence = sequence; 104 | 105 | std::mem::take(&mut self.events) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/input/tty.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io; 3 | use std::io::Write; 4 | use std::mem::MaybeUninit; 5 | use std::os::fd::RawFd; 6 | use std::os::unix::prelude::AsRawFd; 7 | 8 | use crate::utils::log; 9 | 10 | pub struct Terminal { 11 | settings: Option, 12 | alt_screen: bool, 13 | } 14 | 15 | impl Drop for Terminal { 16 | fn drop(&mut self) { 17 | self.teardown() 18 | } 19 | } 20 | 21 | impl Terminal { 22 | /// Setup the input stream to operate in raw mode. 23 | /// Returns an object that'll revert terminal settings. 24 | pub fn setup() -> Self { 25 | Self { 26 | settings: match TerminalSettings::open_raw() { 27 | Ok(settings) => Some(settings), 28 | Err(error) => { 29 | log::error!("Failed to setup terminal: {error}"); 30 | 31 | None 32 | } 33 | }, 34 | alt_screen: if let Err(error) = TTY::enter_alt_screen() { 35 | log::error!("Failed to enter alternative screen: {error}"); 36 | 37 | false 38 | } else { 39 | true 40 | }, 41 | } 42 | } 43 | 44 | pub fn teardown(&mut self) { 45 | if let Some(ref settings) = self.settings { 46 | if let Err(error) = settings.apply() { 47 | log::error!("Failed to revert terminal settings: {error}"); 48 | } 49 | 50 | self.settings = None; 51 | } 52 | 53 | if self.alt_screen { 54 | if let Err(error) = TTY::quit_alt_screen() { 55 | log::error!("Failed to quit alternative screen: {error}"); 56 | } 57 | 58 | self.alt_screen = false; 59 | } 60 | } 61 | } 62 | 63 | enum TTY { 64 | Raw(RawFd), 65 | File(File), 66 | } 67 | 68 | const SEQUENCES: [(u32, bool); 4] = [(1049, true), (1003, true), (1006, true), (25, false)]; 69 | 70 | impl TTY { 71 | fn stdin() -> TTY { 72 | let isatty = unsafe { libc::isatty(libc::STDIN_FILENO) }; 73 | 74 | if isatty != 1 { 75 | if let Ok(file) = File::open("/dev/tty") { 76 | return TTY::File(file); 77 | } 78 | } 79 | 80 | TTY::Raw(libc::STDIN_FILENO) 81 | } 82 | 83 | fn enter_alt_screen() -> io::Result<()> { 84 | let mut out = io::stdout(); 85 | 86 | for (sequence, enable) in SEQUENCES { 87 | write!(out, "\x1b[?{}{}", sequence, if enable { "h" } else { "l" })?; 88 | } 89 | 90 | // Set the current foreground color to black 91 | write!(out, "\x1b[48;2;0;0;0m")?; 92 | // Query current foreground color to for true-color support detection 93 | write!(out, "\x1bP$qm\x1b\\")?; 94 | // Query current terminal name 95 | write!(out, "\x1bP+q544e\x1b\\")?; 96 | 97 | out.flush() 98 | } 99 | 100 | fn quit_alt_screen() -> io::Result<()> { 101 | let mut out = io::stdout(); 102 | 103 | for (sequence, enable) in SEQUENCES { 104 | write!(out, "\x1b[?{}{}", sequence, if enable { "l" } else { "h" })?; 105 | } 106 | 107 | out.flush() 108 | } 109 | 110 | fn as_raw_fd(self) -> RawFd { 111 | match self { 112 | TTY::Raw(fd) => fd, 113 | TTY::File(file) => file.as_raw_fd(), 114 | } 115 | } 116 | } 117 | 118 | trait ToErr { 119 | fn to_err(self) -> io::Result<()>; 120 | } 121 | impl ToErr for libc::c_int { 122 | fn to_err(self) -> io::Result<()> { 123 | if self == 0 { 124 | Ok(()) 125 | } else { 126 | Err(io::Error::last_os_error()) 127 | } 128 | } 129 | } 130 | 131 | /// Safe wrapper around libc::termios 132 | #[derive(Clone)] 133 | struct TerminalSettings { 134 | data: libc::termios, 135 | } 136 | 137 | impl TerminalSettings { 138 | /// Fetch settings from the current TTY 139 | fn open() -> io::Result { 140 | let tty = TTY::stdin(); 141 | let mut term = MaybeUninit::uninit(); 142 | let data = unsafe { 143 | libc::tcgetattr(tty.as_raw_fd(), term.as_mut_ptr()).to_err()?; 144 | 145 | term.assume_init() 146 | }; 147 | 148 | Ok(Self { data }) 149 | } 150 | 151 | fn open_raw() -> io::Result { 152 | let mut raw = Self::open()?; 153 | let settings = raw.clone(); 154 | 155 | raw.make_raw(); 156 | raw.apply()?; 157 | 158 | Ok(settings) 159 | } 160 | 161 | /// Enable raw input 162 | fn make_raw(&mut self) { 163 | let c_oflag = self.data.c_oflag; 164 | 165 | // Set the terminal to raw mode 166 | unsafe { libc::cfmakeraw(&mut self.data) } 167 | 168 | // Restore output flags, ensures carriage returns are consistent 169 | self.data.c_oflag = c_oflag; 170 | } 171 | 172 | /// Apply the settings to the current TTY 173 | fn apply(&self) -> io::Result<()> { 174 | let tty = TTY::stdin(); 175 | 176 | unsafe { libc::tcsetattr(tty.as_raw_fd(), libc::TCSANOW, &self.data).to_err() } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod browser; 2 | pub mod cli; 3 | pub mod gfx; 4 | pub mod input; 5 | pub mod output; 6 | pub mod ui; 7 | 8 | mod utils; 9 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | // mod kd_tree; 2 | // mod quantizer; 3 | mod cell; 4 | mod frame_sync; 5 | mod painter; 6 | mod quad; 7 | mod render_thread; 8 | mod renderer; 9 | mod window; 10 | mod xterm; 11 | 12 | pub use cell::*; 13 | pub use frame_sync::*; 14 | pub use painter::*; 15 | pub use quad::*; 16 | pub use render_thread::*; 17 | pub use renderer::*; 18 | pub use window::*; 19 | -------------------------------------------------------------------------------- /src/output/cell.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::gfx::{Color, Point}; 4 | 5 | #[derive(Clone, PartialEq)] 6 | pub struct Grapheme { 7 | /// Unicode character in UTF-8, might contain multiple code points (Emoji, CJK). 8 | pub char: String, 9 | pub index: usize, 10 | pub width: usize, 11 | pub color: Color, 12 | } 13 | 14 | /// Terminal cell with `height = width * 2` 15 | #[derive(PartialEq)] 16 | pub struct Cell { 17 | pub cursor: Point, 18 | /// Text grapheme if any 19 | pub grapheme: Option>, 20 | pub quadrant: (Color, Color, Color, Color), 21 | } 22 | 23 | impl Cell { 24 | pub fn new(x: u32, y: u32) -> Cell { 25 | Cell { 26 | cursor: Point::new(x, y), 27 | grapheme: None, 28 | quadrant: ( 29 | Color::black(), 30 | Color::black(), 31 | Color::black(), 32 | Color::black(), 33 | ), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/output/frame_sync.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | /// A utility to synchronize rendering with a given FPS 4 | pub struct FrameSync { 5 | render_start: Option, 6 | frame_duration: Duration, 7 | } 8 | 9 | impl FrameSync { 10 | pub fn new(fps: f32) -> Self { 11 | Self { 12 | render_start: None, 13 | frame_duration: Duration::from_micros((1_000_000.0 / fps) as u64), 14 | } 15 | } 16 | 17 | /// Mark the beginning of the render 18 | pub fn start(&mut self) { 19 | self.render_start = Some(Instant::now()); 20 | } 21 | 22 | /// Get a deadline until the next frame 23 | pub fn deadline(&self) -> Instant { 24 | match self.render_start { 25 | // We never rendered yet, render now! 26 | None => Instant::now(), 27 | // Else we should render `frame_duration` after the last render start. 28 | // If we render at 60 FPS, this should be 16ms after the render start. 29 | // If the render takes more than the frame duration, this will always 30 | // return a deadline in a the past, making render happen immediately. 31 | Some(render_start) => render_start + self.frame_duration, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/output/kd_tree.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | 3 | use crate::gfx::Color; 4 | 5 | struct KDNode { 6 | left: Option>, 7 | right: Option>, 8 | normal: Color, 9 | middle: (usize, Color), 10 | } 11 | 12 | impl KDNode { 13 | fn new(colors: &[Color]) { 14 | let (sum, sum_squared) = colors.iter().fold( 15 | (Color::black(), Color::black()), 16 | |(sum, sum_squared), color| (sum + color, sum_squared + color * color), 17 | ); 18 | } 19 | 20 | fn nearest(&self, color: Color, mut limit: f64) -> Option<(usize, f64)> { 21 | let diff = color - self.middle.1; 22 | let distance = diff.mul(&diff).sum().sqrt(); 23 | let mut result = None; 24 | 25 | if distance < limit { 26 | limit = distance; 27 | } 28 | 29 | let dot = diff.mul(self.normal).sum(); 30 | 31 | if dot <= 0.0 { 32 | if let Some(ref left) = self.left { 33 | if let Some(nearest) = left.nearest(color, limit) { 34 | limit = nearest.1; 35 | result = Some(nearest); 36 | } 37 | } 38 | 39 | if -dot < limit { 40 | if let Some(ref right) = self.right { 41 | if let Some(nearest) = right.nearest(color, limit) { 42 | result = Some(nearest); 43 | } 44 | } 45 | } 46 | } else { 47 | if let Some(ref right) = self.right { 48 | if let Some(nearest) = right.nearest(color, limit) { 49 | limit = nearest.1; 50 | result = Some(nearest); 51 | } 52 | } 53 | 54 | if dot < limit { 55 | if let Some(ref left) = self.left { 56 | if let Some(nearest) = left.nearest(color, limit) { 57 | result = Some(nearest); 58 | } 59 | } 60 | } 61 | } 62 | 63 | result 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/output/painter.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Stdout, Write}; 2 | 3 | use crate::gfx::{Color, Point}; 4 | 5 | use super::{binarize_quandrant, Cell}; 6 | 7 | pub struct Painter { 8 | output: Stdout, 9 | buffer: Vec, 10 | cursor: Option>, 11 | true_color: bool, 12 | background: Option, 13 | foreground: Option, 14 | background_code: Option, 15 | foreground_code: Option, 16 | } 17 | 18 | impl Painter { 19 | pub fn new() -> Painter { 20 | Painter { 21 | buffer: Vec::new(), 22 | cursor: None, 23 | output: io::stdout(), 24 | background: None, 25 | foreground: None, 26 | background_code: None, 27 | foreground_code: None, 28 | true_color: match std::env::var("COLORTERM").unwrap_or_default().as_str() { 29 | "truecolor" | "24bit" => true, 30 | _ => false, 31 | }, 32 | } 33 | } 34 | 35 | pub fn true_color(&self) -> bool { 36 | self.true_color 37 | } 38 | 39 | pub fn set_true_color(&mut self, true_color: bool) { 40 | self.true_color = true_color 41 | } 42 | 43 | pub fn begin(&mut self) -> io::Result<()> { 44 | write!(self.buffer, "\x1b[?25l\x1b[?12l") 45 | } 46 | 47 | pub fn end(&mut self, cursor: Option) -> io::Result<()> { 48 | if let Some(cursor) = cursor { 49 | write!( 50 | self.buffer, 51 | "\x1b[{};{}H\x1b[?25h\x1b[?12h", 52 | cursor.y + 1, 53 | cursor.x + 1 54 | )?; 55 | } 56 | 57 | self.output.write(self.buffer.as_slice())?; 58 | self.output.flush()?; 59 | self.buffer.clear(); 60 | self.cursor = None; 61 | 62 | Ok(()) 63 | } 64 | 65 | pub fn paint(&mut self, cell: &Cell) -> io::Result<()> { 66 | let &Cell { 67 | cursor, 68 | quadrant, 69 | ref grapheme, 70 | } = cell; 71 | 72 | let (char, background, foreground, width) = if let Some(grapheme) = grapheme { 73 | if grapheme.index > 0 { 74 | return Ok(()); 75 | } 76 | 77 | ( 78 | grapheme.char.as_str(), 79 | quadrant 80 | .0 81 | .avg_with(quadrant.1) 82 | .avg_with(quadrant.2) 83 | .avg_with(quadrant.3), 84 | grapheme.color, 85 | grapheme.width as u32, 86 | ) 87 | } else { 88 | let (char, background, foreground) = binarize_quandrant(quadrant); 89 | 90 | (char, background, foreground, 1) 91 | }; 92 | 93 | if self.cursor != Some(cursor) { 94 | write!(self.buffer, "\x1b[{};{}H", cursor.y + 1, cursor.x + 1)?; 95 | }; 96 | 97 | self.cursor = Some(cursor + Point::new(width, 0)); 98 | 99 | if self.background != Some(background) { 100 | self.background = Some(background); 101 | 102 | if self.true_color { 103 | write!( 104 | self.buffer, 105 | "\x1b[48;2;{};{};{}m", 106 | background.r, background.g, background.b, 107 | )? 108 | } else { 109 | let code = background.to_xterm(); 110 | 111 | if self.background_code != Some(code) { 112 | self.background_code = Some(code); 113 | 114 | write!(self.buffer, "\x1b[48;5;{code}m")? 115 | } 116 | } 117 | } 118 | 119 | if self.foreground != Some(foreground) { 120 | self.foreground = Some(foreground); 121 | 122 | if self.true_color { 123 | write!( 124 | self.buffer, 125 | "\x1b[38;2;{};{};{}m", 126 | foreground.r, foreground.g, foreground.b, 127 | )? 128 | } else { 129 | let code = foreground.to_xterm(); 130 | 131 | if self.foreground_code != Some(code) { 132 | self.foreground_code = Some(code); 133 | 134 | write!(self.buffer, "\x1b[38;5;{code}m")? 135 | } 136 | } 137 | } 138 | 139 | self.buffer.write_all(char.as_bytes())?; 140 | 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/output/quad.rs: -------------------------------------------------------------------------------- 1 | use crate::gfx::Color; 2 | use crate::utils::FourBits::{self, *}; 3 | 4 | /// Turn a quadrant of four colors into two colors and a quadrant unicode character. 5 | pub fn binarize_quandrant( 6 | (x, y, z, w): (Color, Color, Color, Color), 7 | ) -> (&'static str, Color, Color) { 8 | // Step 1: grayscale 9 | const LUMA: Color = Color::new(0.299, 0.587, 0.114); 10 | let (a, b, c, d) = ( 11 | LUMA.dot(x.cast()), 12 | LUMA.dot(y.cast()), 13 | LUMA.dot(z.cast()), 14 | LUMA.dot(w.cast()), 15 | ); 16 | // Step 2: luminance middlepoint 17 | let min = a.min(b).min(c).min(d); 18 | let max = a.max(b).max(c).max(d); 19 | let mid = min + (max - min) / 2.0; 20 | 21 | // Step 3: average colors based on binary mask 22 | match FourBits::new(a > mid, b > mid, c > mid, d > mid) { 23 | B0000 => ("▄", x.avg_with(y), z.avg_with(w)), 24 | B0001 => ("▖", x.avg_with(y).avg_with(z), w), 25 | B0010 => ("▗", x.avg_with(y).avg_with(w), z), 26 | B0011 => ("▄", x.avg_with(y), z.avg_with(w)), 27 | B0100 => ("▝", x.avg_with(z).avg_with(w), y), 28 | B0101 => ("▞", x.avg_with(z), y.avg_with(w)), 29 | B0110 => ("▐", x.avg_with(w), y.avg_with(z)), 30 | B0111 => ("▘", y.avg_with(z).avg_with(w), x), 31 | B1000 => ("▘", y.avg_with(z).avg_with(w), x), 32 | B1001 => ("▌", y.avg_with(z), x.avg_with(w)), 33 | B1010 => ("▚", y.avg_with(w), x.avg_with(z)), 34 | B1011 => ("▝", x.avg_with(z).avg_with(w), y), 35 | B1100 => ("▄", x.avg_with(y), z.avg_with(w)), 36 | B1101 => ("▗", x.avg_with(y).avg_with(w), z), 37 | B1110 => ("▖", x.avg_with(y).avg_with(z), w), 38 | B1111 => ("▄", x.avg_with(y), z.avg_with(w)), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/output/quantizer.rs: -------------------------------------------------------------------------------- 1 | use crate::gfx::Color; 2 | 3 | #[derive(Clone, Copy)] 4 | enum Channel { 5 | R, 6 | G, 7 | B, 8 | } 9 | 10 | const COLOR_BUCKETS: usize = 8; 11 | const COLORS: usize = 2_usize.pow(COLOR_BUCKETS as u32); 12 | 13 | /// Find the closest color to `color` on `palette` using a binary search 14 | pub fn palette_color(palette: &[Color; COLORS], color: Color) { 15 | let mut size = palette.len() / 2; 16 | let mut iter = palette.iter(); 17 | let mut prev = iter.next(); 18 | } 19 | 20 | fn distance(a: Color, b: Color) {} 21 | 22 | pub fn quantize(pixels: &[u8]) -> [Color; COLORS] { 23 | let mut min = Color::black(); 24 | let mut max = Color::black(); 25 | let mut bucket = Vec::::new(); 26 | let mut pixels_iter = pixels.iter(); 27 | let mut bucket_iter = bucket.iter_mut(); 28 | 29 | // Step 1: find the dominant channel 30 | loop { 31 | match ( 32 | bucket_iter.next(), 33 | pixels_iter.next(), 34 | pixels_iter.next(), 35 | pixels_iter.next(), 36 | pixels_iter.next(), 37 | ) { 38 | (Some(color), Some(r), Some(g), Some(b), Some(_)) => { 39 | // Save the color in a bucket 40 | color.set_r(*r); 41 | color.set_g(*g); 42 | color.set_b(*b); 43 | 44 | min.set_r(min.r().min(color.r())); 45 | min.set_g(min.g().min(color.g())); 46 | min.set_b(min.b().min(color.b())); 47 | 48 | max.set_r(max.r().max(color.r())); 49 | max.set_g(max.g().max(color.g())); 50 | max.set_b(max.b().max(color.b())); 51 | } 52 | _ => break, 53 | } 54 | } 55 | 56 | let ranges = [ 57 | (Channel::R, max.r() - min.r()), 58 | (Channel::G, max.g() - min.g()), 59 | (Channel::B, max.b() - min.b()), 60 | ]; 61 | let (channel, _) = ranges 62 | .iter() 63 | .reduce(|a, b| if a.1 > b.1 { a } else { b }) 64 | .unwrap(); 65 | 66 | // Step 2: perform median-cut 67 | for i in 1..=COLOR_BUCKETS { 68 | let buckets = 2_usize.pow(i as u32); 69 | let size = bucket.len() / buckets; 70 | 71 | for j in 0..buckets { 72 | let start = j * size; 73 | let end = start + size; 74 | let slice = &mut bucket[start..end]; 75 | 76 | slice.sort_unstable_by(match channel { 77 | Channel::R => |a: &Color, b: &Color| a.r().cmp(&b.r()), 78 | Channel::G => |a: &Color, b: &Color| a.g().cmp(&b.g()), 79 | Channel::B => |a: &Color, b: &Color| a.b().cmp(&b.b()), 80 | }); 81 | } 82 | } 83 | 84 | // Step 3: get the average color in each bucket 85 | let mut palette = [Color::black(); COLORS]; 86 | let size = bucket.len() / palette.len(); 87 | 88 | for (i, color) in palette.iter_mut().enumerate() { 89 | let start = i * size; 90 | let end = start + size; 91 | let slice = &bucket[start..end]; 92 | let mut sum = None; 93 | 94 | for color in slice.into_iter() { 95 | sum = Some(match sum { 96 | None => color.cast(), 97 | Some(sum) => color.cast() + sum, 98 | }) 99 | } 100 | 101 | if let Some(sum) = sum { 102 | let avg = sum / size as u32; 103 | 104 | color.set_r(avg.r() as u8); 105 | color.set_g(avg.g() as u8); 106 | color.set_b(avg.b() as u8); 107 | } 108 | } 109 | 110 | palette 111 | } 112 | -------------------------------------------------------------------------------- /src/output/render_thread.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::mpsc::{self, Receiver, Sender}, 3 | thread::{self, JoinHandle}, 4 | time::Instant, 5 | }; 6 | 7 | use crate::cli::CommandLine; 8 | 9 | use super::{FrameSync, Renderer}; 10 | 11 | /// Control a rendering thread that lazily starts. 12 | /// This allows the `Bridge` struct to be used in places 13 | /// where we do not expected the rendering thread to start. 14 | pub struct RenderThread { 15 | thread: Option<(Sender, JoinHandle<()>)>, 16 | enabled: bool, 17 | } 18 | 19 | type RenderClosure = Box; 20 | enum Message { 21 | Run(RenderClosure), 22 | Shutdown, 23 | } 24 | 25 | impl RenderThread { 26 | pub fn new() -> Self { 27 | Self { 28 | thread: None, 29 | enabled: false, 30 | } 31 | } 32 | 33 | /// Enable the rendering thread. 34 | /// Allows the thread to be lazily initiated. 35 | pub fn enable(&mut self) { 36 | self.enabled = true 37 | } 38 | 39 | /// Stop the rendering thread. 40 | /// Returns a `JoinHandle` if a thread was started. 41 | pub fn stop(&mut self) -> Option> { 42 | self.enabled = false; 43 | self.send(Message::Shutdown); 44 | 45 | let (_, handle) = self.thread.take()?; 46 | 47 | Some(handle) 48 | } 49 | 50 | /// Run a closure on the rendering thread. 51 | pub fn render(&mut self, run: F) 52 | where 53 | F: FnMut(&mut Renderer) + Send + 'static, 54 | { 55 | self.send(Message::Run(Box::new(run))) 56 | } 57 | 58 | /// Boot the rendering thread, contains a simple event loop. 59 | fn boot(rx: Receiver) { 60 | let cmd = CommandLine::parse(); 61 | let mut sync = FrameSync::new(cmd.fps); 62 | let mut renderer = Renderer::new(); 63 | let mut needs_render = false; 64 | 65 | loop { 66 | // Get a deadline for the next frame 67 | let deadline = sync.deadline(); 68 | let mut wait = true; 69 | 70 | loop { 71 | let message = if wait { 72 | // On the first iteration, we want to block indefinitely 73 | // until we get a message, after which we schedule a render. 74 | wait = false; 75 | 76 | rx.recv().ok() 77 | } else { 78 | // On subsequence iterations, we want to process a maximum 79 | // number of events until the deadline for the next frame. 80 | rx.recv_timeout(deadline - Instant::now()).ok() 81 | }; 82 | 83 | match message { 84 | // Timeout and no message, render if needed 85 | None => break, 86 | // Shutdown the thread 87 | Some(Message::Shutdown) => return, 88 | // Run a closure and schedule a render 89 | Some(Message::Run(mut closure)) => { 90 | closure(&mut renderer); 91 | 92 | needs_render = true; 93 | } 94 | } 95 | } 96 | 97 | // Render if needed 98 | if needs_render { 99 | needs_render = false; 100 | 101 | // Update the frame sync timings 102 | sync.start(); 103 | renderer.render().unwrap(); 104 | } 105 | } 106 | } 107 | 108 | /// Send a message to the rendering thread. 109 | /// Creates a new thread if enabled and needed. 110 | fn send(&mut self, message: Message) { 111 | if let Some((tx, _)) = &self.thread { 112 | tx.send(message).unwrap() 113 | } else if self.enabled { 114 | let (tx, rx) = mpsc::channel(); 115 | 116 | tx.send(message).unwrap(); 117 | 118 | self.thread = Some((tx.clone(), thread::spawn(move || Self::boot(rx)))); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/output/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Write}, 3 | rc::Rc, 4 | }; 5 | 6 | use unicode_segmentation::UnicodeSegmentation; 7 | use unicode_width::UnicodeWidthStr; 8 | 9 | use crate::{ 10 | gfx::{Color, Point, Rect, Size}, 11 | input::Key, 12 | ui::navigation::{Navigation, NavigationAction}, 13 | utils::log, 14 | }; 15 | 16 | use super::{Cell, Grapheme, Painter}; 17 | 18 | pub struct Renderer { 19 | nav: Navigation, 20 | cells: Vec<(Cell, Cell)>, 21 | painter: Painter, 22 | size: Size, 23 | } 24 | 25 | impl Renderer { 26 | pub fn new() -> Renderer { 27 | Renderer { 28 | nav: Navigation::new(), 29 | cells: Vec::with_capacity(0), 30 | painter: Painter::new(), 31 | size: Size::new(0, 0), 32 | } 33 | } 34 | 35 | pub fn enable_true_color(&mut self) { 36 | self.painter.set_true_color(true) 37 | } 38 | 39 | pub fn keypress(&mut self, key: &Key) -> io::Result { 40 | let action = self.nav.keypress(key); 41 | 42 | Ok(action) 43 | } 44 | pub fn mouse_up(&mut self, origin: Point) -> io::Result { 45 | let action = self.nav.mouse_up(origin); 46 | 47 | Ok(action) 48 | } 49 | pub fn mouse_down(&mut self, origin: Point) -> io::Result { 50 | let action = self.nav.mouse_down(origin); 51 | 52 | Ok(action) 53 | } 54 | pub fn mouse_move(&mut self, origin: Point) -> io::Result { 55 | let action = self.nav.mouse_move(origin); 56 | 57 | Ok(action) 58 | } 59 | 60 | pub fn push_nav(&mut self, url: &str, can_go_back: bool, can_go_forward: bool) { 61 | self.nav.push(url, can_go_back, can_go_forward) 62 | } 63 | 64 | pub fn get_size(&self) -> Size { 65 | self.size 66 | } 67 | 68 | pub fn set_size(&mut self, size: Size) { 69 | self.nav.set_size(size); 70 | self.size = size; 71 | 72 | let mut x = 0; 73 | let mut y = 0; 74 | let bound = size.width - 1; 75 | let cells = (size.width + size.width * size.height) as usize; 76 | 77 | self.cells.clear(); 78 | self.cells.resize_with(cells, || { 79 | let cell = (Cell::new(x, y), Cell::new(x, y)); 80 | 81 | if x < bound { 82 | x += 1; 83 | } else { 84 | x = 0; 85 | y += 1; 86 | } 87 | 88 | cell 89 | }); 90 | } 91 | 92 | pub fn render(&mut self) -> io::Result<()> { 93 | let size = self.size; 94 | 95 | for (origin, element) in self.nav.render(size) { 96 | self.fill_rect( 97 | Rect::new(origin.x, origin.y, element.text.width() as u32, 1), 98 | element.background, 99 | ); 100 | self.draw_text( 101 | &element.text, 102 | origin * (2, 1), 103 | Size::splat(0), 104 | element.foreground, 105 | ); 106 | } 107 | 108 | self.painter.begin()?; 109 | 110 | for (previous, current) in self.cells.iter_mut() { 111 | if current == previous { 112 | continue; 113 | } 114 | 115 | previous.quadrant = current.quadrant; 116 | previous.grapheme = current.grapheme.clone(); 117 | 118 | self.painter.paint(current)?; 119 | } 120 | 121 | self.painter.end(self.nav.cursor())?; 122 | 123 | Ok(()) 124 | } 125 | 126 | /// Draw the background from a pixel array encoded in RGBA8888 127 | pub fn draw_background(&mut self, pixels: &[u8], pixels_size: Size, rect: Rect) { 128 | let viewport = self.size.cast::(); 129 | 130 | if pixels.len() < viewport.width * viewport.height * 8 * 4 { 131 | log::debug!( 132 | "unexpected size, actual: {}, expected: {}", 133 | pixels.len(), 134 | viewport.width * viewport.height * 8 * 4 135 | ); 136 | return; 137 | } 138 | 139 | let origin = rect.origin.cast::().max(0.0) / (2.0, 4.0); 140 | let size = rect.size.cast::().max(0.0) / (2.0, 4.0); 141 | let top = (origin.y.floor() as usize).min(viewport.height); 142 | let left = (origin.x.floor() as usize).min(viewport.width); 143 | let right = ((origin.x + size.width).ceil() as usize) 144 | .min(viewport.width) 145 | .max(left); 146 | let bottom = ((origin.y + size.height).ceil() as usize) 147 | .min(viewport.height) 148 | .max(top); 149 | let row_length = pixels_size.width as usize; 150 | let pixel = |x, y| { 151 | Color::new( 152 | pixels[((x + y * row_length) * 4 + 2) as usize], 153 | pixels[((x + y * row_length) * 4 + 1) as usize], 154 | pixels[((x + y * row_length) * 4 + 0) as usize], 155 | ) 156 | }; 157 | let pair = |x, y| pixel(x, y).avg_with(pixel(x, y + 1)); 158 | 159 | for y in top..bottom { 160 | let index = (y + 1) * viewport.width; 161 | let start = index + left; 162 | let end = index + right; 163 | let (mut x, y) = (left * 2, y * 4); 164 | 165 | for (_, cell) in &mut self.cells[start..end] { 166 | cell.quadrant = ( 167 | pair(x + 0, y + 0), 168 | pair(x + 1, y + 0), 169 | pair(x + 1, y + 2), 170 | pair(x + 0, y + 2), 171 | ); 172 | 173 | x += 2; 174 | } 175 | } 176 | } 177 | 178 | pub fn clear_text(&mut self) { 179 | for (_, cell) in self.cells.iter_mut() { 180 | cell.grapheme = None 181 | } 182 | } 183 | 184 | pub fn set_title(&self, title: &str) -> io::Result<()> { 185 | let mut stdout = io::stdout(); 186 | 187 | write!(stdout, "\x1b]0;{title}\x07")?; 188 | write!(stdout, "\x1b]1;{title}\x07")?; 189 | write!(stdout, "\x1b]2;{title}\x07")?; 190 | 191 | stdout.flush() 192 | } 193 | 194 | pub fn fill_rect(&mut self, rect: Rect, color: Color) { 195 | self.draw(rect, |cell| { 196 | cell.grapheme = None; 197 | cell.quadrant = (color, color, color, color); 198 | }) 199 | } 200 | 201 | pub fn draw(&mut self, bounds: Rect, mut draw: F) 202 | where 203 | F: FnMut(&mut Cell), 204 | { 205 | let origin = bounds.origin.cast::(); 206 | let size = bounds.size.cast::(); 207 | let viewport_width = self.size.width as usize; 208 | let top = origin.y; 209 | let bottom = top + size.height; 210 | 211 | // Iterate over each row 212 | for y in top..bottom { 213 | let left = y * viewport_width + origin.x; 214 | let right = left + size.width; 215 | 216 | for (_, current) in self.cells[left..right].iter_mut() { 217 | draw(current) 218 | } 219 | } 220 | } 221 | 222 | /// Render some text into the terminal output 223 | pub fn draw_text(&mut self, string: &str, origin: Point, size: Size, color: Color) { 224 | // Get an iterator starting at the text origin 225 | let len = self.cells.len(); 226 | let viewport = &self.size.cast::(); 227 | 228 | if size.width > 2 && size.height > 2 { 229 | let origin = (origin.cast::() / (2.0, 4.0) + (0.0, 1.0)).round(); 230 | let size = (size.cast::() / (2.0, 4.0)).round(); 231 | let left = (origin.x.max(0.0) as usize).min(viewport.width); 232 | let right = ((origin.x + size.width).max(0.0) as usize).min(viewport.width); 233 | let top = (origin.y.max(0.0) as usize).min(viewport.height); 234 | let bottom = ((origin.y + size.height).max(0.0) as usize).min(viewport.height); 235 | 236 | for y in top..bottom { 237 | let index = y * viewport.width; 238 | let start = index + left; 239 | let end = index + right; 240 | 241 | for (_, cell) in self.cells[start..end].iter_mut() { 242 | cell.grapheme = None 243 | } 244 | } 245 | } else { 246 | // Compute the buffer index based on the position 247 | let index = origin.x / 2 + (origin.y + 1) / 4 * (viewport.width as i32); 248 | let mut iter = self.cells[len.min(index as usize)..].iter_mut(); 249 | 250 | // Get every Unicode grapheme in the input string 251 | for grapheme in UnicodeSegmentation::graphemes(string, true) { 252 | let width = grapheme.width(); 253 | 254 | for index in 0..width { 255 | // Get the next terminal cell at the given position 256 | match iter.next() { 257 | // Stop if we're at the end of the buffer 258 | None => return, 259 | // Set the cell to the current grapheme 260 | Some((_, cell)) => { 261 | let next = Grapheme { 262 | // Create a new shared reference to the text 263 | color, 264 | index, 265 | width, 266 | // Export the set of unicode code points for this graphene into an UTF-8 string 267 | char: grapheme.to_string(), 268 | }; 269 | 270 | if match cell.grapheme { 271 | None => true, 272 | Some(ref previous) => { 273 | previous.color != next.color || previous.char != next.char 274 | } 275 | } { 276 | cell.grapheme = Some(Rc::new(next)) 277 | } 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/output/window.rs: -------------------------------------------------------------------------------- 1 | use core::mem::MaybeUninit; 2 | use std::str::FromStr; 3 | 4 | use crate::{cli::CommandLine, gfx::Size, utils::log}; 5 | 6 | /// A terminal window. 7 | #[derive(Clone, Debug)] 8 | pub struct Window { 9 | /// Device pixel ratio 10 | pub dpi: f32, 11 | /// Size of a terminal cell in pixels 12 | pub scale: Size, 13 | /// Size of the termina window in cells 14 | pub cells: Size, 15 | /// Size of the browser window in pixels 16 | pub browser: Size, 17 | /// Command line arguments 18 | pub cmd: CommandLine, 19 | } 20 | 21 | impl Window { 22 | /// Read the window 23 | pub fn read() -> Window { 24 | let mut window = Self { 25 | dpi: 1.0, 26 | scale: (0.0, 0.0).into(), 27 | cells: (0, 0).into(), 28 | browser: (0, 0).into(), 29 | cmd: CommandLine::parse(), 30 | }; 31 | 32 | window.update(); 33 | 34 | window 35 | } 36 | 37 | pub fn update(&mut self) -> &Self { 38 | let (mut term, mut cell) = unsafe { 39 | let mut ptr = MaybeUninit::::uninit(); 40 | 41 | if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ptr.as_mut_ptr()) == 0 { 42 | let size = ptr.assume_init(); 43 | 44 | ( 45 | Size::new(size.ws_col, size.ws_row), 46 | Size::new(size.ws_xpixel, size.ws_ypixel), 47 | ) 48 | } else { 49 | (Size::splat(0), Size::splat(0)) 50 | } 51 | }; 52 | 53 | if cell.width == 0 || cell.height == 0 { 54 | cell.width = 8; 55 | cell.height = 16; 56 | } 57 | 58 | if term.width == 0 || term.height == 0 { 59 | let cols = match parse_var("COLUMNS").unwrap_or(0) { 60 | 0 => 80, 61 | x => x, 62 | }; 63 | let rows = match parse_var("LINES").unwrap_or(0) { 64 | 0 => 24, 65 | x => x, 66 | }; 67 | 68 | log::warning!( 69 | "TIOCGWINSZ returned an empty size ({}x{}), defaulting to {}x{}", 70 | term.width, 71 | term.height, 72 | cols, 73 | rows 74 | ); 75 | 76 | term.width = cols; 77 | term.height = rows; 78 | } 79 | 80 | let zoom = 1.5 * self.cmd.zoom; 81 | let cells = Size::new(term.width.max(1), term.height.max(2) - 1); 82 | let auto_scale = false; 83 | let cell_pixels = if auto_scale { 84 | Size::new(cell.width as f32, cell.height as f32) / cells.cast() 85 | } else { 86 | Size::new(8.0, 16.0) 87 | }; 88 | // Normalize the cells dimensions for an aspect ratio of 1:2 89 | let cell_width = (cell_pixels.width + cell_pixels.height / 2.0) / 2.0; 90 | 91 | // Round DPI to 2 decimals for proper viewport computations 92 | self.dpi = (2.0 / cell_width * zoom * 100.0).ceil() / 100.0; 93 | // A virtual cell should contain a 2x4 pixel quadrant 94 | self.scale = Size::new(2.0, 4.0) / self.dpi; 95 | // Keep some space for the UI 96 | self.cells = Size::new(term.width.max(1), term.height.max(2) - 1).cast(); 97 | self.browser = self.cells.cast::().mul(self.scale).ceil().cast(); 98 | 99 | self 100 | } 101 | } 102 | 103 | fn parse_var(var: &str) -> Option { 104 | std::env::var(var).ok()?.parse().ok() 105 | } 106 | -------------------------------------------------------------------------------- /src/output/xterm.rs: -------------------------------------------------------------------------------- 1 | use crate::gfx::Color; 2 | 3 | impl Color { 4 | pub fn to_xterm(&self) -> u8 { 5 | if self.max_val() - self.min_val() < 8 { 6 | match self.r { 7 | 0..=4 => 16, 8 | 5..=8 => 232, 9 | 238..=246 => 255, 10 | 247..=255 => 231, 11 | r => 232 + (r - 8 + 5) / 10, 12 | } 13 | } else { 14 | let scale = 5.0 / 200.0; 15 | 16 | (16.0 17 | + self 18 | .cast::() 19 | .mul_add(scale, scale * -55.0) 20 | .max(0.0) 21 | .round() 22 | .dot((36.0, 6.0, 1.0))) as u8 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | pub mod navigation; 2 | -------------------------------------------------------------------------------- /src/ui/navigation.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use unicode_width::UnicodeWidthStr; 4 | 5 | use crate::{ 6 | gfx::{Color, Point, Size}, 7 | input::Key, 8 | utils::log, 9 | }; 10 | 11 | pub enum NavigationAction { 12 | Ignore, 13 | Forward, 14 | GoTo(String), 15 | GoBack(), 16 | GoForward(), 17 | Refresh(), 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct NavigationElement { 22 | pub text: String, 23 | pub background: Color, 24 | pub foreground: Color, 25 | } 26 | 27 | pub struct Navigation { 28 | url: Option, 29 | size: Size, 30 | cursor: Option, 31 | can_go_back: bool, 32 | can_go_forward: bool, 33 | } 34 | 35 | impl Navigation { 36 | pub fn new() -> Self { 37 | Self { 38 | url: None, 39 | size: (0, 0).into(), 40 | cursor: None, 41 | can_go_back: false, 42 | can_go_forward: false, 43 | } 44 | } 45 | 46 | pub fn cursor(&self) -> Option { 47 | Some((11 + self.cursor? as i32, 0).into()) 48 | } 49 | 50 | pub fn keypress(&mut self, key: &Key) -> NavigationAction { 51 | let modifier_key = match env::consts::OS { 52 | "macos" => key.modifiers.meta, 53 | _ => key.modifiers.alt, 54 | }; 55 | 56 | match self.cursor { 57 | None => match (modifier_key, key.char) { 58 | (true, 0x14) => NavigationAction::GoBack(), 59 | (true, 0x13) => NavigationAction::GoForward(), 60 | _ => NavigationAction::Forward, 61 | }, 62 | Some(cursor) => { 63 | if let Some(url) = &mut self.url { 64 | // TODO: Unicode 65 | match key.char { 66 | // Return 67 | 0x0d => return NavigationAction::GoTo(url.clone()), 68 | // Up 69 | 0x11 => self.cursor = Some(0), 70 | // Down 71 | 0x12 => self.cursor = Some(url.width()), 72 | // Right 73 | 0x13 => self.cursor = Some((cursor + 1).min(url.width())), 74 | // Left 75 | 0x14 => self.cursor = Some(if cursor > 0 { cursor - 1 } else { 0 }), 76 | // Backspace 77 | 0x7f => { 78 | if cursor > 0 { 79 | url.remove(cursor - 1); 80 | 81 | self.cursor = Some(cursor - 1); 82 | } 83 | } 84 | key => { 85 | url.insert(cursor, key as char); 86 | 87 | self.cursor = Some((cursor + 1).min(url.width())) 88 | } 89 | } 90 | 91 | NavigationAction::Ignore 92 | } else { 93 | NavigationAction::Forward 94 | } 95 | } 96 | } 97 | } 98 | 99 | pub fn display_url(&self) -> &str { 100 | match &self.url { 101 | None => "about:blank", 102 | Some(url) => url, 103 | } 104 | } 105 | 106 | pub fn url_size(&self) -> usize { 107 | self.display_url().width() 108 | } 109 | 110 | pub fn mouse_up(&mut self, origin: Point) -> NavigationAction { 111 | if origin.y != 0 { 112 | self.cursor = None; 113 | 114 | NavigationAction::Forward 115 | } else { 116 | NavigationAction::Ignore 117 | } 118 | } 119 | pub fn mouse_down(&mut self, origin: Point) -> NavigationAction { 120 | if origin.y != 0 { 121 | self.cursor = None; 122 | 123 | return NavigationAction::Forward; 124 | } 125 | 126 | self.cursor = None; 127 | 128 | return match origin.x { 129 | 0..=2 => NavigationAction::GoBack(), 130 | 3..=5 => NavigationAction::GoForward(), 131 | 6..=8 => NavigationAction::Refresh(), 132 | 11.. => { 133 | self.cursor = Some(self.url_size().min(origin.x as usize - 11)); 134 | 135 | log::debug!("setting cursor to {:?}", self.cursor); 136 | 137 | NavigationAction::Ignore 138 | } 139 | _ => NavigationAction::Ignore, 140 | }; 141 | } 142 | pub fn mouse_move(&mut self, _origin: Point) -> NavigationAction { 143 | NavigationAction::Forward 144 | } 145 | 146 | pub fn push(&mut self, url: &str, can_go_back: bool, can_go_forward: bool) { 147 | if match (self.cursor, &self.url) { 148 | (None, _) => false, 149 | (_, None) => true, 150 | (_, Some(current)) => current != url, 151 | } { 152 | self.cursor = Some(url.len()) 153 | } 154 | 155 | self.url = Some(url.to_owned()); 156 | self.can_go_back = can_go_back; 157 | self.can_go_forward = can_go_forward; 158 | } 159 | 160 | pub fn set_size(&mut self, size: Size) { 161 | self.size = size 162 | } 163 | 164 | pub fn render_btn(&self, icon: &str, enabled: bool) -> [NavigationElement; 3] { 165 | let background = Color::splat(255); 166 | let foreground = Color::splat(0); 167 | 168 | [ 169 | NavigationElement { 170 | text: "[".to_owned(), 171 | background, 172 | foreground, 173 | }, 174 | NavigationElement { 175 | text: icon.to_owned(), 176 | background, 177 | foreground: if enabled { 178 | foreground 179 | } else { 180 | Color::splat(200) 181 | }, 182 | }, 183 | NavigationElement { 184 | text: "]".to_owned(), 185 | background, 186 | foreground, 187 | }, 188 | ] 189 | } 190 | 191 | pub fn render(&self, size: Size) -> Vec<(Point, NavigationElement)> { 192 | let ui_elements = 13; 193 | let space = if size.width >= ui_elements { 194 | (size.width - ui_elements) as usize 195 | } else { 196 | 0 197 | }; 198 | let url: String = self.display_url().chars().take(space).collect(); 199 | let width = url.width(); 200 | let padded = format!(" {}{} ", url, " ".repeat(space - width)); 201 | let mut elements = Vec::new(); 202 | let mut point = Point::splat(0); 203 | 204 | for list in [ 205 | self.render_btn("\u{276e}", self.can_go_back), 206 | self.render_btn("\u{276f}", self.can_go_forward), 207 | self.render_btn("↻", true), 208 | self.render_btn(&padded, true), 209 | ] { 210 | for element in list { 211 | let width = element.text.width() as i32; 212 | 213 | elements.push((point.clone(), element)); 214 | 215 | point = point + (width, 0); 216 | } 217 | } 218 | 219 | elements 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | mod four_bits; 2 | mod try_block; 3 | 4 | pub mod log; 5 | 6 | use try_block::*; 7 | 8 | pub use four_bits::*; 9 | -------------------------------------------------------------------------------- /src/utils/four_bits.rs: -------------------------------------------------------------------------------- 1 | pub enum FourBits { 2 | B0000 = 0b0000, 3 | B0001 = 0b0001, 4 | B0010 = 0b0010, 5 | B0011 = 0b0011, 6 | B0100 = 0b0100, 7 | B0101 = 0b0101, 8 | B0110 = 0b0110, 9 | B0111 = 0b0111, 10 | B1000 = 0b1000, 11 | B1001 = 0b1001, 12 | B1010 = 0b1010, 13 | B1011 = 0b1011, 14 | B1100 = 0b1100, 15 | B1101 = 0b1101, 16 | B1110 = 0b1110, 17 | B1111 = 0b1111, 18 | } 19 | 20 | impl FourBits { 21 | pub fn new(x: bool, y: bool, z: bool, w: bool) -> Self { 22 | use FourBits::*; 23 | 24 | match (x as u8) << 3 | (y as u8) << 2 | (z as u8) << 1 | (w as u8) << 0 { 25 | 0b0000 => B0000, 26 | 0b0001 => B0001, 27 | 0b0010 => B0010, 28 | 0b0011 => B0011, 29 | 0b0100 => B0100, 30 | 0b0101 => B0101, 31 | 0b0110 => B0110, 32 | 0b0111 => B0111, 33 | 0b1000 => B1000, 34 | 0b1001 => B1001, 35 | 0b1010 => B1010, 36 | 0b1011 => B1011, 37 | 0b1100 => B1100, 38 | 0b1101 => B1101, 39 | 0b1110 => B1110, 40 | 0b1111 => B1111, 41 | _ => panic!("Unexpected mask value"), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/log.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use chrono::prelude::*; 4 | 5 | use crate::utils::try_block; 6 | 7 | macro_rules! debug { 8 | ($($args:expr),+) => { 9 | crate::utils::log::write( 10 | "DEBUG", 11 | file!(), 12 | line!(), 13 | &format!($($args),*) 14 | ) 15 | }; 16 | } 17 | macro_rules! warning { 18 | ($($args:expr),+) => { 19 | crate::utils::log::write( 20 | "WARNING", 21 | file!(), 22 | line!(), 23 | &format!($($args),*) 24 | ) 25 | }; 26 | } 27 | macro_rules! error { 28 | ($($args:expr),+) => { 29 | crate::utils::log::write( 30 | "ERROR", 31 | file!(), 32 | line!(), 33 | &format!($($args),*) 34 | ) 35 | }; 36 | } 37 | 38 | pub(crate) use debug; 39 | pub(crate) use error; 40 | pub(crate) use warning; 41 | 42 | pub fn write(level: &str, file: &str, line: u32, message: &str) { 43 | let date = Utc::now(); 44 | 45 | eprintln!( 46 | "[{:02}{:02}/{:02}{:02}{:02}.{:06}:{}:{}({})] {}", 47 | date.month(), 48 | date.day(), 49 | date.hour(), 50 | date.minute(), 51 | date.second(), 52 | date.nanosecond() / 1000, 53 | level, 54 | try_block!(Path::new(file).file_name()?.to_str()).unwrap_or("default"), 55 | line, 56 | message 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/try_block.rs: -------------------------------------------------------------------------------- 1 | macro_rules! try_block { 2 | ($block:expr) => { 3 | (|| $block)() 4 | }; 5 | } 6 | 7 | pub(crate) use try_block; 8 | --------------------------------------------------------------------------------