mirror of
https://github.com/fathyb/carbonyl.git
synced 2024-09-11 02:45:31 +03:00
Initial commit
This commit is contained in:
parent
761e4e0716
commit
a69e8b6096
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build]
|
||||
rustflags = ["-L", "/Users/fathy/Git/carbonyl/chromium/src/out/Default/obj/headless"]
|
||||
target-dir = "build"
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,5 +5,6 @@ node_modules/
|
||||
/.git_cache
|
||||
/.vscode
|
||||
/build
|
||||
/electron/*
|
||||
!/electron/.gclient
|
||||
/chromium/*
|
||||
!/chromium/.gclient
|
||||
/Cargo.lock
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "libsixel"]
|
||||
path = libsixel
|
||||
url = https://github.com/saitoha/libsixel
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "carbonyl"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
unicode-width = "0.1.10"
|
||||
unicode-segmentation = "1.10.0"
|
||||
|
||||
[lib]
|
||||
name = "carbonyl"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["staticlib"]
|
@ -21,7 +21,7 @@ RUN apt-get update && \
|
||||
|
||||
# Release binaries
|
||||
# ================
|
||||
FROM --platform=$BUILDPLATFORM debian:11 AS html2svg-binaries
|
||||
FROM --platform=$BUILDPLATFORM debian:11 AS carbonyl-binaries
|
||||
|
||||
RUN apt-get update && apt-get install -y unzip
|
||||
|
||||
@ -31,7 +31,7 @@ RUN unzip /runtime.zip -d /runtime
|
||||
|
||||
# TypeScript build
|
||||
# ================
|
||||
FROM --platform=$BUILDPLATFORM node:18 AS html2svg-js
|
||||
FROM --platform=$BUILDPLATFORM node:18 AS carbonyl-js
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock /app/
|
||||
@ -55,8 +55,8 @@ WORKDIR /app
|
||||
COPY package.json yarn.lock /app/
|
||||
RUN yarn --production
|
||||
|
||||
COPY --from=html2svg-js /app/build /app/build
|
||||
COPY --from=html2svg-binaries /runtime /app/build/runtime
|
||||
COPY --from=carbonyl-js /app/build /app/build
|
||||
COPY --from=carbonyl-binaries /runtime /app/build/runtime
|
||||
COPY /scripts/docker-entrypoint.sh /app/scripts/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
|
||||
|
11
chromium/.gclient
Normal file
11
chromium/.gclient
Normal file
@ -0,0 +1,11 @@
|
||||
solutions = [
|
||||
{
|
||||
"name": "src",
|
||||
"url": "https://chromium.googlesource.com/chromium/src.git@111.0.5539.1",
|
||||
"managed": False,
|
||||
"custom_deps": {},
|
||||
"custom_vars": {
|
||||
"use_rust": True,
|
||||
}
|
||||
},
|
||||
]
|
@ -1,10 +0,0 @@
|
||||
solutions = [
|
||||
{ "name" : 'src/electron',
|
||||
"url" : 'https://github.com/electron/electron',
|
||||
"deps_file" : 'DEPS',
|
||||
"managed" : False,
|
||||
"custom_deps" : {
|
||||
},
|
||||
"custom_vars": {},
|
||||
},
|
||||
]
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "html2svg",
|
||||
"name": "carbonyl",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
|
144
readme.md
144
readme.md
@ -1,133 +1,43 @@
|
||||
# `html2svg`
|
||||
# `carbonyl`
|
||||
|
||||
Convert HTML and `<canvas>` to vector (SVG, PDF) or bitmap (PNG, JPEG, WebP) images using Chromium. [Read the blog post](https://fathy.fr/html2svg).
|
||||
Carbonyl is a Chromium based browser built to run in a terminal. [Read the blog post](https://fathy.fr/carbonyl).
|
||||
|
||||
It supports pretty much all Web APIs including WebGL, WebGPU, audio and video playback, animations, etc..
|
||||
|
||||
It's snappy, starts in less than a second, runs at 60 FPS, and idles at 0% CPU usage. It does not requires a window server (ie. works in a safe-mode console), and even runs through SSH.
|
||||
|
||||
Carbonyl originally started as [`html2svg`](https://github.com/fathyb/html2svg) and is now the runtime behind it.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
# Export to SVG
|
||||
$ docker run fathyb/html2svg https://google.com > google.svg
|
||||
$ docker run fathyb/html2svg https://google.com --format svg > google.svg
|
||||
# Export to PDF
|
||||
$ docker run fathyb/html2svg https://google.com --format pdf > google.pdf
|
||||
# Export to PNG
|
||||
$ docker run fathyb/html2svg https://google.com --format png > google.png
|
||||
# Display help
|
||||
$ docker run fathyb/html2svg --help
|
||||
Usage: html2svg [options] [command] <url>
|
||||
|
||||
Arguments:
|
||||
url URL to the web page to render
|
||||
|
||||
Options:
|
||||
-f, --full capture the entire page
|
||||
-W, --wait <seconds> set the amount of seconds to wait between the page loaded event and taking the screenshot (default: 1)
|
||||
-w, --width <width> set the viewport width in pixels (default: 1920)
|
||||
-h, --height <height> set the viewport height in pixels (default: 1080)
|
||||
-f, --format <format> set the output format, should one of these values: svg, pdf, png, jpg, webp (default: "svg")
|
||||
--help display help for command
|
||||
|
||||
Commands:
|
||||
serve [options]
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
An HTTP server is also provided, all CLI options are supported:
|
||||
> Currently building...
|
||||
|
||||
```shell
|
||||
# Start a server on port 8080
|
||||
$ docker run -p 8080:8080 fathyb/html2svg serve
|
||||
# Export to SVG
|
||||
$ curl -d http://google.fr http://localhost:8080 > google.svg
|
||||
$ curl -d '{"url": "http://google.fr", "format": "svg"}' http://localhost:8080 > google.svg
|
||||
# Export to PDF
|
||||
$ curl -d '{"url": "http://google.fr", "format": "pdf"}' http://localhost:8080 > google.pdf
|
||||
# Export to PNG
|
||||
$ curl -d '{"url": "http://google.fr", "format": "png"}' http://localhost:8080 > google.png
|
||||
# Display help
|
||||
$ docker run fathyb/html2svg serve --help
|
||||
Usage: html2svg serve [options]
|
||||
|
||||
Options:
|
||||
-H, --host <hostname> set the hostname to listen on (default: "localhost")
|
||||
-p, --port <hostname> set the port to listen on (default: 8080)
|
||||
-u, --unix <path> set the unix socket to listen on
|
||||
-h, --help display help for command
|
||||
# Watch YouTube inside a Docker container
|
||||
$ docker run fathyb/carbonyl youtube.com
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Building Chromium is only officially supported on AMD64. If you'd like to target ARM64, cross-compile from AMD64 instead.
|
||||
### Fetch
|
||||
|
||||
### Local
|
||||
|
||||
You'll need to install all tools required to build Chromium: https://www.chromium.org/developers/how-tos/get-the-code/
|
||||
|
||||
If you're running Linux, you can use [the Docker build instructions](#docker) to generate binaries.
|
||||
|
||||
1. Fetch dependencies:
|
||||
```shell
|
||||
$ yarn
|
||||
```
|
||||
2. Clone Electron.js and Chromium using `gclient`:
|
||||
```shell
|
||||
$ yarn gclient
|
||||
```
|
||||
3. Configure the build system using `gn` using one of these commands:
|
||||
```shell
|
||||
# for local developement
|
||||
$ yarn gn testing
|
||||
# or for releasing
|
||||
$ yarn gn release
|
||||
# add --ide=xcode if you'd like to generate an Xcode project on macOS
|
||||
$ yarn gn release --ide=xcode
|
||||
```
|
||||
4. Build using `ninja` using one of these commands:
|
||||
```shell
|
||||
# make a testing build
|
||||
$ yarn ninja testing
|
||||
# make a release build
|
||||
$ yarn ninja release
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
We use `docker run` instead of `Dockerfile` for building Chromium to support incremental building.
|
||||
|
||||
```shell
|
||||
# Create the build environment
|
||||
$ docker build . --build-arg "WORKDIR=$(pwd)" --target build-env --tag html2svg-build-env
|
||||
# Clone the Chromium/Electron code
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/gclient.sh --revision "src/electron@cb22573c3e76e09df9fbad36dc372080c04d349e"
|
||||
# Apply html2svg patches
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/patch.sh
|
||||
# Install build dependencies
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/install-build-deps.sh
|
||||
```console
|
||||
$ cd chromium
|
||||
$ gclient sync
|
||||
```
|
||||
|
||||
Now you'll have to build binaries, steps differs depending on the platform you'd like to target:
|
||||
- AMD64:
|
||||
```shell
|
||||
# Fetch compiler files
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/linux/sysroot_scripts/install-sysroot.py --arch=amd64
|
||||
# Generate build files
|
||||
$ docker run -ti -v $(pwd):$(pwd) --workdir $(pwd)/electron/src html2svg-build-env gn gen "out/release-amd64" --args="import(\"//electron/build/args/release.gn\") cc_wrapper=\"ccache\""
|
||||
# Build binaries
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/build.sh release-amd64
|
||||
```
|
||||
- ARM64:
|
||||
```shell
|
||||
# Fetch compiler files
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/linux/sysroot_scripts/install-sysroot.py --arch=arm64
|
||||
# Generate build files
|
||||
$ docker run -ti -v $(pwd):$(pwd) --workdir $(pwd)/electron/src html2svg-build-env gn gen "out/release-arm64" --args="import(\"//electron/build/args/release.gn\") cc_wrapper=\"ccache\" target_cpu=\"arm64\""
|
||||
# Build binaries
|
||||
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/build.sh release-arm64 --target-cpu=arm64
|
||||
```
|
||||
### Configure
|
||||
|
||||
Finally, build the Docker image:
|
||||
```shell
|
||||
docker build .
|
||||
> You need to disable `lld` on macOS because of a linking bug related to Rust and `compact_unwind`
|
||||
|
||||
```console
|
||||
$ cd chromium/src
|
||||
$ gn gen out/Default
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```console
|
||||
$ cd chromium/src
|
||||
$ ninja -C out/Default headless:headless_shell
|
||||
```
|
||||
|
@ -5,4 +5,4 @@ set -e
|
||||
export DISPLAY=:99
|
||||
|
||||
Xvfb $DISPLAY -screen 0 1920x1080x24 &
|
||||
node /app/build/html2svg.cli.js "$@"
|
||||
node /app/build/carbonyl.cli.js "$@"
|
||||
|
3
src/browser.rs
Normal file
3
src/browser.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod ffi;
|
||||
|
||||
pub use ffi::*;
|
189
src/browser/ffi.rs
Normal file
189
src/browser/ffi.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use std::ffi::CStr;
|
||||
use std::io::{stderr, Write};
|
||||
use std::process::{self, Command, Stdio};
|
||||
use std::{env, io};
|
||||
|
||||
use libc::{c_char, c_int, c_uchar, c_uint, size_t};
|
||||
|
||||
use crate::gfx::{Color, Point, Rect, Size};
|
||||
use crate::terminal::output::Renderer;
|
||||
use crate::terminal::{input, output};
|
||||
|
||||
/// This file bridges the C++ code with Rust.
|
||||
/// "C-unwind" combined with .unwrap() is used to allow catching Rust panics
|
||||
/// using C++ exception handling.
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CSize {
|
||||
width: c_uint,
|
||||
height: c_uint,
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct CPoint {
|
||||
x: c_uint,
|
||||
y: c_uint,
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct CRect {
|
||||
origin: CPoint,
|
||||
size: CSize,
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct CColor {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct BrowserDelegate {
|
||||
shutdown: extern "C" fn(),
|
||||
scroll: extern "C" fn(c_int),
|
||||
key_press: extern "C" fn(c_char),
|
||||
mouse_up: extern "C" fn(c_uint, c_uint),
|
||||
mouse_down: extern "C" fn(c_uint, c_uint),
|
||||
mouse_move: extern "C" fn(c_uint, c_uint),
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
const CARBONYL_INSIDE_SHELL: &str = "CARBONYL_INSIDE_SHELL";
|
||||
|
||||
if env::vars().find(|(key, value)| key == CARBONYL_INSIDE_SHELL && value == "1") != None {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
input::setup()?;
|
||||
Renderer::setup()?;
|
||||
|
||||
let output = Command::new(env::current_exe()?)
|
||||
.args(env::args().skip(1))
|
||||
.arg("--disable-threaded-scrolling")
|
||||
.arg("--disable-threaded-animation")
|
||||
.env(CARBONYL_INSIDE_SHELL, "1")
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
Renderer::teardown()?;
|
||||
stderr().write_all(&output.stderr)?;
|
||||
|
||||
if let Some(code) = output.status.code() {
|
||||
process::exit(code);
|
||||
} else {
|
||||
process::exit(127);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_shell_main() {
|
||||
main().unwrap()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_renderer_create() -> *mut Renderer {
|
||||
let mut renderer = Box::new(Renderer::new());
|
||||
let src = output::size().unwrap();
|
||||
|
||||
renderer.set_size(Size::new(7, 14), src);
|
||||
|
||||
Box::into_raw(renderer)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_renderer_clear_text(renderer: *mut Renderer) {
|
||||
let renderer = unsafe { &mut *renderer };
|
||||
|
||||
renderer.clear_text()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_renderer_draw_text(
|
||||
renderer: *mut Renderer,
|
||||
utf8: *const c_char,
|
||||
rect: *const CRect,
|
||||
color: *const CColor,
|
||||
) {
|
||||
let (renderer, string, rect, color) =
|
||||
unsafe { (&mut *renderer, CStr::from_ptr(utf8), &*rect, &*color) };
|
||||
|
||||
renderer.draw_text(
|
||||
string.to_str().unwrap(),
|
||||
Point::new(rect.origin.x as i32, rect.origin.y as i32),
|
||||
Size::new(rect.size.width as u32, rect.size.height as u32),
|
||||
Color::new(color.r, color.g, color.b),
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_renderer_draw_background(
|
||||
renderer: *mut Renderer,
|
||||
pixels: *mut c_uchar,
|
||||
pixels_size: size_t,
|
||||
rect: *const CRect,
|
||||
) {
|
||||
let (renderer, pixels, rect) = unsafe {
|
||||
(
|
||||
&mut *renderer,
|
||||
std::slice::from_raw_parts_mut(pixels, pixels_size),
|
||||
&*rect,
|
||||
)
|
||||
};
|
||||
|
||||
renderer
|
||||
.draw_background(
|
||||
pixels,
|
||||
Rect {
|
||||
origin: Point::new(rect.origin.x as i32, rect.origin.y as i32),
|
||||
size: Size::new(rect.size.width, rect.size.height),
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_output_get_size(size: *mut CSize) {
|
||||
let dst = unsafe { &mut *size };
|
||||
let src = output::size().unwrap().cast::<c_uint>();
|
||||
|
||||
dst.width = src.width * 7;
|
||||
dst.height = src.height * 14;
|
||||
}
|
||||
|
||||
/// Function called by the C++ code to listen for input events.
|
||||
///
|
||||
/// This will block so the calling code should start and own a dedicated thread.
|
||||
/// It will panic if there is any error.
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn carbonyl_input_listen(delegate: *mut BrowserDelegate) {
|
||||
let char_width = 7;
|
||||
let char_height = 14;
|
||||
let BrowserDelegate {
|
||||
shutdown,
|
||||
scroll,
|
||||
key_press,
|
||||
mouse_up,
|
||||
mouse_down,
|
||||
mouse_move,
|
||||
} = unsafe { &*delegate };
|
||||
|
||||
input::listen(|event| {
|
||||
match event {
|
||||
input::Event::Exit => return Some(shutdown()),
|
||||
input::Event::KeyPress { key } => key_press(key as c_char),
|
||||
input::Event::Scroll { delta } => scroll(delta as c_int * char_height as c_int),
|
||||
input::Event::MouseUp { col, row } => {
|
||||
mouse_up(col as c_uint * char_width, row as c_uint * char_height)
|
||||
}
|
||||
input::Event::MouseDown { col, row } => {
|
||||
mouse_down(col as c_uint * char_width, row as c_uint * char_height)
|
||||
}
|
||||
input::Event::MouseMove { col, row } => {
|
||||
mouse_move(col as c_uint * char_width, row as c_uint * char_height)
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.unwrap()
|
||||
}
|
64
src/browser/host_display_client.cc
Normal file
64
src/browser/host_display_client.cc
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2019 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "headless/lib/browser/headless_host_display_client.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "components/viz/common/resources/resource_format.h"
|
||||
#include "components/viz/common/resources/resource_sizes.h"
|
||||
#include "mojo/public/cpp/system/platform_handle.h"
|
||||
#include "skia/ext/platform_canvas.h"
|
||||
#include "third_party/skia/include/core/SkColor.h"
|
||||
#include "third_party/skia/include/core/SkRect.h"
|
||||
#include "third_party/skia/src/core/SkDevice.h"
|
||||
#include "ui/gfx/skia_util.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include "skia/ext/skia_utils_win.h"
|
||||
#endif
|
||||
|
||||
#include "headless/app/carbonyl_rust_bridge.h"
|
||||
|
||||
namespace carbonyl {
|
||||
|
||||
LayeredWindowUpdater::LayeredWindowUpdater(
|
||||
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver)
|
||||
: receiver_(this, std::move(receiver)) {}
|
||||
|
||||
LayeredWindowUpdater::~LayeredWindowUpdater() = default;
|
||||
|
||||
void LayeredWindowUpdater::OnAllocatedSharedMemory(
|
||||
const gfx::Size& pixel_size,
|
||||
base::UnsafeSharedMemoryRegion region) {
|
||||
if (region.IsValid())
|
||||
shm_mapping_ = region.Map();
|
||||
}
|
||||
|
||||
void LayeredWindowUpdater::Draw(const gfx::Rect& damage_rect,
|
||||
DrawCallback draw_callback) {
|
||||
Renderer::Main()->DrawBackgrond(
|
||||
shm_mapping_.GetMemoryAs<uint8_t>(),
|
||||
shm_mapping_.size()
|
||||
);
|
||||
|
||||
std::move(draw_callback).Run();
|
||||
}
|
||||
|
||||
HostDisplayClient::HostDisplayClient()
|
||||
: viz::HostDisplayClient(gfx::kNullAcceleratedWidget) {}
|
||||
HostDisplayClient::~HostDisplayClient() = default;
|
||||
|
||||
void HostDisplayClient::CreateLayeredWindowUpdater(
|
||||
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver) {
|
||||
layered_window_updater_ =
|
||||
std::make_unique<LayeredWindowUpdater>(std::move(receiver));
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
|
||||
void HostDisplayClient::DidCompleteSwapWithNewSize(
|
||||
const gfx::Size& size) {}
|
||||
#endif
|
||||
|
||||
} // namespace carbonyl
|
72
src/browser/host_display_client.h
Normal file
72
src/browser/host_display_client.h
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2019 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_
|
||||
#define HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/memory/shared_memory_mapping.h"
|
||||
#include "components/viz/host/host_display_client.h"
|
||||
#include "services/viz/privileged/mojom/compositing/layered_window_updater.mojom.h"
|
||||
#include "ui/gfx/native_widget_types.h"
|
||||
|
||||
namespace carbonyl {
|
||||
|
||||
typedef base::RepeatingCallback<void(const gfx::Rect&, const SkBitmap&)>
|
||||
OnPaintCallback;
|
||||
|
||||
class LayeredWindowUpdater : public viz::mojom::LayeredWindowUpdater {
|
||||
public:
|
||||
explicit LayeredWindowUpdater(
|
||||
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver);
|
||||
~LayeredWindowUpdater() override;
|
||||
|
||||
// disable copy
|
||||
LayeredWindowUpdater(const LayeredWindowUpdater&) = delete;
|
||||
LayeredWindowUpdater& operator=(const LayeredWindowUpdater&) = delete;
|
||||
|
||||
// viz::mojom::LayeredWindowUpdater implementation.
|
||||
void OnAllocatedSharedMemory(const gfx::Size& pixel_size,
|
||||
base::UnsafeSharedMemoryRegion region) override;
|
||||
void Draw(const gfx::Rect& damage_rect, DrawCallback draw_callback) override;
|
||||
|
||||
private:
|
||||
mojo::Receiver<viz::mojom::LayeredWindowUpdater> receiver_;
|
||||
base::WritableSharedMemoryMapping shm_mapping_;
|
||||
};
|
||||
|
||||
class HostDisplayClient : public viz::HostDisplayClient {
|
||||
public:
|
||||
explicit HostDisplayClient();
|
||||
~HostDisplayClient() override;
|
||||
|
||||
// disable copy
|
||||
HostDisplayClient(const HostDisplayClient&) = delete;
|
||||
HostDisplayClient& operator=(const HostDisplayClient&) =
|
||||
delete;
|
||||
|
||||
private:
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
void OnDisplayReceivedCALayerParams(
|
||||
const gfx::CALayerParams& ca_layer_params) override;
|
||||
#endif
|
||||
|
||||
void CreateLayeredWindowUpdater(
|
||||
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver)
|
||||
override;
|
||||
|
||||
#if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
|
||||
void DidCompleteSwapWithNewSize(const gfx::Size& size) override;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<LayeredWindowUpdater> layered_window_updater_;
|
||||
OnPaintCallback callback_;
|
||||
bool active_ = false;
|
||||
};
|
||||
|
||||
} // namespace carbonyl
|
||||
|
||||
#endif // HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_
|
3014
src/chromium.patch
3014
src/chromium.patch
File diff suppressed because it is too large
Load Diff
11
src/gfx.rs
Normal file
11
src/gfx.rs
Normal file
@ -0,0 +1,11 @@
|
||||
mod color;
|
||||
mod point;
|
||||
mod rect;
|
||||
mod size;
|
||||
mod vector;
|
||||
|
||||
pub use color::*;
|
||||
pub use point::*;
|
||||
pub use rect::*;
|
||||
pub use size::*;
|
||||
pub use vector::*;
|
26
src/gfx/color.rs
Normal file
26
src/gfx/color.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use super::Vector3;
|
||||
use crate::impl_vector_overload;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Color<T: Copy = u8> {
|
||||
pub r: T,
|
||||
pub g: T,
|
||||
pub b: T,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn from_iter<'a, T>(iter: &mut T) -> Option<Color>
|
||||
where
|
||||
T: Iterator<Item = &'a u8>,
|
||||
{
|
||||
let (b, g, r, _) = (iter.next(), iter.next(), iter.next(), iter.next());
|
||||
|
||||
Some(Color::<u8>::new(*r?, *g?, *b?))
|
||||
}
|
||||
|
||||
pub fn black() -> Color {
|
||||
Color::<u8>::new(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl_vector_overload!(Color r g b);
|
10
src/gfx/point.rs
Normal file
10
src/gfx/point.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use super::Vector2;
|
||||
use crate::impl_vector_overload;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Point<T: Copy = i32> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl_vector_overload!(Point x y);
|
7
src/gfx/rect.rs
Normal file
7
src/gfx/rect.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use super::{Point, Size};
|
||||
|
||||
// #[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Rect<P: Copy = i32, S: Copy = u32> {
|
||||
pub origin: Point<P>,
|
||||
pub size: Size<S>,
|
||||
}
|
10
src/gfx/size.rs
Normal file
10
src/gfx/size.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use super::Vector2;
|
||||
use crate::impl_vector_overload;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Size<T: Copy = u32> {
|
||||
pub width: T,
|
||||
pub height: T,
|
||||
}
|
||||
|
||||
impl_vector_overload!(Size width height);
|
381
src/gfx/vector.rs
Normal file
381
src/gfx/vector.rs
Normal file
@ -0,0 +1,381 @@
|
||||
pub trait Vector2<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
fn x(&self) -> T;
|
||||
fn y(&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Vector3<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
fn x(&self) -> T;
|
||||
fn y(&self) -> T;
|
||||
fn z(&self) -> T;
|
||||
}
|
||||
pub trait Cast<T> {
|
||||
fn cast(self) -> T;
|
||||
}
|
||||
|
||||
pub trait ToIntUnchecked<T> {
|
||||
unsafe fn to_int_unchecked(self) -> T;
|
||||
}
|
||||
|
||||
macro_rules! impl_cast_trait {
|
||||
() => {
|
||||
impl_cast_trait!(u8 as int);
|
||||
impl_cast_trait!(i8 as int);
|
||||
impl_cast_trait!(u16 as int);
|
||||
impl_cast_trait!(i16 as int);
|
||||
impl_cast_trait!(u32 as int);
|
||||
impl_cast_trait!(i32 as int);
|
||||
impl_cast_trait!(u64 as int);
|
||||
impl_cast_trait!(i64 as int);
|
||||
impl_cast_trait!(f32 as float);
|
||||
impl_cast_trait!(f64 as float);
|
||||
impl_cast_trait!(usize as int);
|
||||
impl_cast_trait!(isize as int);
|
||||
};
|
||||
($from:ty as int) => {
|
||||
impl_cast_trait!($from);
|
||||
|
||||
impl ToIntUnchecked<$from> for f32 {
|
||||
unsafe fn to_int_unchecked(self) -> $from {
|
||||
f32::to_int_unchecked(self)
|
||||
}
|
||||
}
|
||||
impl ToIntUnchecked<$from> for f64 {
|
||||
unsafe fn to_int_unchecked(self) -> $from {
|
||||
f64::to_int_unchecked(self)
|
||||
}
|
||||
}
|
||||
};
|
||||
($from:ty as float) => {
|
||||
impl_cast_trait!($from);
|
||||
};
|
||||
($from:ty) => {
|
||||
impl_cast_trait!($from => u8);
|
||||
impl_cast_trait!($from => i8);
|
||||
impl_cast_trait!($from => u16);
|
||||
impl_cast_trait!($from => i16);
|
||||
impl_cast_trait!($from => u32);
|
||||
impl_cast_trait!($from => i32);
|
||||
impl_cast_trait!($from => u64);
|
||||
impl_cast_trait!($from => i64);
|
||||
impl_cast_trait!($from => f32);
|
||||
impl_cast_trait!($from => f64);
|
||||
impl_cast_trait!($from => usize);
|
||||
impl_cast_trait!($from => isize);
|
||||
};
|
||||
($from:ty => $to:ty) => {
|
||||
impl Cast<$to> for $from {
|
||||
fn cast(self) -> $to {
|
||||
self as $to
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_cast_trait!();
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_vector_overload {
|
||||
($struct:ident $x:ident $y:ident) => (
|
||||
impl<T: Copy> $struct<T> {
|
||||
pub const fn new($x: T, $y: T) -> $struct<T> {
|
||||
$struct { $x, $y }
|
||||
}
|
||||
|
||||
pub const fn splat(value: T) -> Self {
|
||||
Self::new(value, value)
|
||||
}
|
||||
|
||||
pub const fn to_array(&self) -> [T; 2] {
|
||||
[self.$x, self.$y]
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::array::IntoIter<T, 2> {
|
||||
self.to_array().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Vector2<T> for $struct<T> {
|
||||
fn x(&self) -> T {
|
||||
self.$x
|
||||
}
|
||||
fn y(&self) -> T {
|
||||
self.$y
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> std::iter::FromIterator<T> for $struct<T> {
|
||||
fn from_iter<I>(iter: I) -> $struct<T>
|
||||
where
|
||||
I: IntoIterator<Item = T>
|
||||
{
|
||||
let mut iter = iter.into_iter();
|
||||
let expect = "initialized a vector with a small iter";
|
||||
|
||||
Self::new(
|
||||
iter.next().expect(expect),
|
||||
iter.next().expect(expect)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<T> for $struct<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<(T, T)> for $struct<T> {
|
||||
fn from((x, y): (T, T)) -> Self {
|
||||
Self::new(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<[T; 2]> for $struct<T> {
|
||||
fn from(array: [T; 2]) -> Self {
|
||||
Self::new(array[0], array[1])
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_vector_traits!($struct Vector2);
|
||||
);
|
||||
($struct:ident $x:ident $y:ident $z:ident) => (
|
||||
impl<T: Copy> $struct<T> {
|
||||
pub const fn new($x: T, $y: T, $z: T) -> $struct<T> {
|
||||
$struct { $x, $y, $z }
|
||||
}
|
||||
|
||||
pub const fn splat(value: T) -> Self {
|
||||
Self::new(value, value, value)
|
||||
}
|
||||
|
||||
pub const fn to_array(&self) -> [T; 3] {
|
||||
[self.$x, self.$y, self.$z]
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::array::IntoIter<T, 3> {
|
||||
self.to_array().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Vector3<T> for $struct<T> {
|
||||
fn x(&self) -> T {
|
||||
self.$x
|
||||
}
|
||||
fn y(&self) -> T {
|
||||
self.$y
|
||||
}
|
||||
fn z(&self) -> T {
|
||||
self.$z
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> std::iter::FromIterator<T> for $struct<T> {
|
||||
fn from_iter<I>(iter: I) -> $struct<T>
|
||||
where
|
||||
I: IntoIterator<Item = T>
|
||||
{
|
||||
let mut iter = iter.into_iter();
|
||||
let expect = "initialized a vector with a small iter";
|
||||
|
||||
Self::new(
|
||||
iter.next().expect(expect),
|
||||
iter.next().expect(expect),
|
||||
iter.next().expect(expect)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<T> for $struct<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value, value, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<(T, T, T)> for $struct<T> {
|
||||
fn from((x, y, z): (T, T, T)) -> Self {
|
||||
Self::new(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<[T; 3]> for $struct<T> {
|
||||
fn from(array: [T; 3]) -> Self {
|
||||
Self::new(array[0], array[1], array[2])
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_vector_traits!($struct Vector3);
|
||||
);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_vector_traits {
|
||||
($struct:ident $vector:ident) => {
|
||||
impl<T: Copy> $struct<T> {
|
||||
pub fn dot<U>(&self, rhs: U) -> T
|
||||
where
|
||||
U: Into<$struct<T>>,
|
||||
T: std::ops::Mul<T, Output = T> + std::iter::Sum,
|
||||
{
|
||||
(self * rhs.into()).sum()
|
||||
}
|
||||
|
||||
pub fn sum(&self) -> T
|
||||
where
|
||||
T: std::iter::Sum,
|
||||
{
|
||||
self.iter().sum::<T>()
|
||||
}
|
||||
|
||||
pub fn cast<U>(&self) -> $struct<U>
|
||||
where
|
||||
T: super::Cast<U>,
|
||||
U: Copy
|
||||
{
|
||||
self.map(|v| v.cast())
|
||||
}
|
||||
|
||||
pub fn map<U, F>(&self, f: F) -> $struct<U>
|
||||
where
|
||||
U: Copy,
|
||||
F: FnMut(T) -> U
|
||||
{
|
||||
self.iter().map(f).collect()
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_vector_traits!($struct $vector i8);
|
||||
crate::impl_vector_traits!($struct $vector u8);
|
||||
crate::impl_vector_traits!($struct $vector i16);
|
||||
crate::impl_vector_traits!($struct $vector u16);
|
||||
crate::impl_vector_traits!($struct $vector i32);
|
||||
crate::impl_vector_traits!($struct $vector u32);
|
||||
crate::impl_vector_traits!($struct $vector i64);
|
||||
crate::impl_vector_traits!($struct $vector u64);
|
||||
crate::impl_vector_traits!($struct $vector isize);
|
||||
crate::impl_vector_traits!($struct $vector usize);
|
||||
crate::impl_vector_traits!($struct $vector f32 float);
|
||||
crate::impl_vector_traits!($struct $vector f64 float);
|
||||
|
||||
crate::impl_vector_traits!($struct $vector Add add);
|
||||
crate::impl_vector_traits!($struct $vector Sub sub);
|
||||
crate::impl_vector_traits!($struct $vector Mul mul);
|
||||
crate::impl_vector_traits!($struct $vector Div div);
|
||||
crate::impl_vector_traits!($struct $vector BitOr bitor);
|
||||
crate::impl_vector_traits!($struct $vector BitXor bitxor);
|
||||
crate::impl_vector_traits!($struct $vector BitAnd bitand);
|
||||
};
|
||||
($struct:ident $vector:ident $type:ident) => (
|
||||
impl $struct<$type> {
|
||||
pub fn avg_with<T>(&self, rhs: T) -> Self
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
let rhs = rhs.into();
|
||||
|
||||
(self & rhs) + (self ^ rhs) / 2
|
||||
}
|
||||
}
|
||||
);
|
||||
($struct:ident $vector:ident $type:ident float) => (
|
||||
impl $struct<$type> {
|
||||
pub unsafe fn to_int_unchecked<U>(&self) -> $struct<U>
|
||||
where
|
||||
$type: super::ToIntUnchecked<U>,
|
||||
U: Copy
|
||||
{
|
||||
self.map(|x| <$type as super::ToIntUnchecked<U>>::to_int_unchecked(x))
|
||||
}
|
||||
|
||||
pub fn mul_add<M, A>(&self, mul: M, add: A) -> Self
|
||||
where
|
||||
M: Into<$struct<$type>>,
|
||||
A: Into<$struct<$type>>,
|
||||
{
|
||||
self.iter()
|
||||
.zip(mul.into().iter())
|
||||
.zip(add.into().iter())
|
||||
.map(|((x, y), z)| x.mul_add(y, z))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn round(&self) -> Self {
|
||||
self.map(|v| v.round())
|
||||
}
|
||||
|
||||
pub fn min<U>(&self, min: U) -> Self
|
||||
where
|
||||
U: Into<$struct<$type>>
|
||||
{
|
||||
self.iter()
|
||||
.zip(min.into().iter())
|
||||
.map(|(x, y)| x.min(y))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn max<U>(&self, max: U) -> Self
|
||||
where
|
||||
U: Into<$struct<$type>>
|
||||
{
|
||||
self.iter()
|
||||
.zip(max.into().iter())
|
||||
.map(|(x, y)| x.max(y))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn clamp<U>(&self, min: U, max: U) -> Self
|
||||
where
|
||||
U: Into<$struct<$type>>
|
||||
{
|
||||
self.iter()
|
||||
.zip(min.into().iter())
|
||||
.zip(max.into().iter())
|
||||
.map(|((x, y), z)| x.clamp(y, z))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
);
|
||||
($struct:ident $vector:ident $trait:ident $name:ident) => {
|
||||
impl<T: Copy> $struct<T> {
|
||||
pub fn $name<U>(&self, rhs: U) -> Self
|
||||
where
|
||||
T: std::ops::$trait<T, Output = T>,
|
||||
U: Copy + Into<$struct<T>>
|
||||
{
|
||||
self.iter()
|
||||
.zip(rhs.into().iter())
|
||||
.map(|(x, y)| x.$name(y))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> std::ops::$trait<U> for $struct<T>
|
||||
where
|
||||
T: Copy + std::ops::$trait<T, Output = T>,
|
||||
U: Copy + Into<$struct<T>>,
|
||||
{
|
||||
type Output = $struct<T>;
|
||||
|
||||
fn $name(self, rhs: U) -> Self::Output {
|
||||
$struct::$name(&self, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, U> std::ops::$trait<U> for &'a $struct<T>
|
||||
where
|
||||
T: Copy + std::ops::$trait<T, Output = T>,
|
||||
U: Copy + Into<$struct<T>>,
|
||||
{
|
||||
type Output = $struct<T>;
|
||||
|
||||
fn $name(self, rhs: U) -> Self::Output {
|
||||
$struct::$name(&self, rhs)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
477
src/gfx/vector.simd.rs
Normal file
477
src/gfx/vector.simd.rs
Normal file
@ -0,0 +1,477 @@
|
||||
use std::simd::{self};
|
||||
|
||||
pub trait Vector2<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
fn x(&self) -> T;
|
||||
fn y(&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Vector3<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
fn x(&self) -> T;
|
||||
fn y(&self) -> T;
|
||||
fn z(&self) -> T;
|
||||
}
|
||||
|
||||
pub trait VectorElement {
|
||||
type Vector2;
|
||||
type Vector3;
|
||||
}
|
||||
|
||||
impl VectorElement for u8 {
|
||||
type Vector2 = simd::u8x4;
|
||||
type Vector3 = simd::u8x4;
|
||||
}
|
||||
impl VectorElement for i8 {
|
||||
type Vector2 = simd::i8x4;
|
||||
type Vector3 = simd::i8x4;
|
||||
}
|
||||
impl VectorElement for u16 {
|
||||
type Vector2 = simd::u16x2;
|
||||
type Vector3 = simd::u16x4;
|
||||
}
|
||||
impl VectorElement for i16 {
|
||||
type Vector2 = simd::i16x2;
|
||||
type Vector3 = simd::i16x4;
|
||||
}
|
||||
impl VectorElement for u32 {
|
||||
type Vector2 = simd::u32x2;
|
||||
type Vector3 = simd::u32x4;
|
||||
}
|
||||
impl VectorElement for i32 {
|
||||
type Vector2 = simd::i32x2;
|
||||
type Vector3 = simd::i32x4;
|
||||
}
|
||||
impl VectorElement for u64 {
|
||||
type Vector2 = simd::u64x2;
|
||||
type Vector3 = simd::u64x4;
|
||||
}
|
||||
impl VectorElement for i64 {
|
||||
type Vector2 = simd::i64x2;
|
||||
type Vector3 = simd::i64x4;
|
||||
}
|
||||
impl VectorElement for f32 {
|
||||
type Vector2 = simd::f32x2;
|
||||
type Vector3 = simd::f32x4;
|
||||
}
|
||||
impl VectorElement for f64 {
|
||||
type Vector2 = simd::f64x2;
|
||||
type Vector3 = simd::f64x4;
|
||||
}
|
||||
impl VectorElement for usize {
|
||||
type Vector2 = simd::usizex2;
|
||||
type Vector3 = simd::usizex4;
|
||||
}
|
||||
impl VectorElement for isize {
|
||||
type Vector2 = simd::isizex2;
|
||||
type Vector3 = simd::isizex4;
|
||||
}
|
||||
|
||||
pub trait CreateVector2<T> {
|
||||
fn create(a: T, b: T) -> Self;
|
||||
}
|
||||
pub trait CreateVector3<T> {
|
||||
fn create(a: T, b: T, z: T) -> Self;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_vector_overload {
|
||||
($struct:ident $x:ident $y:ident = $default:ty) => (
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct $struct<T: Copy + super::VectorElement = $default> {
|
||||
simd: T::Vector2
|
||||
}
|
||||
|
||||
impl<T: Copy + super::VectorElement> $struct<T> {
|
||||
pub fn new($x: T, $y: T) -> $struct<T>
|
||||
where
|
||||
$struct<T>: super::Vector2<T>,
|
||||
$struct<T>: super::CreateVector2<T>,
|
||||
{
|
||||
<$struct<T> as super::CreateVector2<T>>::create($x, $y)
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_simd2_overload!($struct $x $y u8 4);
|
||||
crate::impl_simd2_overload!($struct $x $y i8 4);
|
||||
crate::impl_simd2_overload!($struct $x $y u16 2);
|
||||
crate::impl_simd2_overload!($struct $x $y i16 2);
|
||||
crate::impl_simd2_overload!($struct $x $y u32 2);
|
||||
crate::impl_simd2_overload!($struct $x $y i32 2);
|
||||
crate::impl_simd2_overload!($struct $x $y u64 2);
|
||||
crate::impl_simd2_overload!($struct $x $y i64 2);
|
||||
crate::impl_simd2_overload!($struct $x $y f32 2);
|
||||
crate::impl_simd2_overload!($struct $x $y f64 2);
|
||||
crate::impl_simd2_overload!($struct $x $y usize 2);
|
||||
crate::impl_simd2_overload!($struct $x $y isize 2);
|
||||
|
||||
crate::impl_vector_traits!($struct Vector2);
|
||||
);
|
||||
($struct:ident $x:ident $y:ident $z:ident = $default:ty) => (
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct $struct<T: Copy = $default>
|
||||
where
|
||||
T: super::VectorElement
|
||||
{
|
||||
simd: T::Vector3
|
||||
}
|
||||
|
||||
impl<T: Copy + super::VectorElement> $struct<T> {
|
||||
pub fn new($x: T, $y: T, $z: T) -> $struct<T>
|
||||
where
|
||||
$struct<T>: super::Vector3<T>,
|
||||
$struct<T>: super::CreateVector3<T>,
|
||||
{
|
||||
<$struct<T> as super::CreateVector3<T>>::create($x, $y, $z)
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_simd3_overload!($struct $x $y $z u8 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z i8 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z u16 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z i16 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z u32 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z i32 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z u64 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z i64 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z f32 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z f64 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z usize 4);
|
||||
crate::impl_simd3_overload!($struct $x $y $z isize 4);
|
||||
|
||||
crate::impl_vector_traits!($struct Vector3);
|
||||
);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_vector_traits {
|
||||
($struct:ident $vector:ident) => {
|
||||
crate::impl_vector_traits!($struct $vector i8);
|
||||
crate::impl_vector_traits!($struct $vector u8);
|
||||
crate::impl_vector_traits!($struct $vector i16);
|
||||
crate::impl_vector_traits!($struct $vector u16);
|
||||
crate::impl_vector_traits!($struct $vector i32);
|
||||
crate::impl_vector_traits!($struct $vector u32);
|
||||
crate::impl_vector_traits!($struct $vector i64);
|
||||
crate::impl_vector_traits!($struct $vector u64);
|
||||
crate::impl_vector_traits!($struct $vector isize);
|
||||
crate::impl_vector_traits!($struct $vector usize);
|
||||
crate::impl_vector_traits!($struct $vector f32 float);
|
||||
crate::impl_vector_traits!($struct $vector f64 float);
|
||||
};
|
||||
($struct:ident $vector:ident $type:ident) => (
|
||||
crate::impl_vector_traits!($struct $vector $type ops);
|
||||
crate::impl_vector_traits!($struct $vector BitOr bitor $type);
|
||||
crate::impl_vector_traits!($struct $vector BitXor bitxor $type);
|
||||
crate::impl_vector_traits!($struct $vector BitAnd bitand $type);
|
||||
|
||||
impl $struct<$type>
|
||||
where
|
||||
$type: super::VectorElement
|
||||
{
|
||||
pub fn avg_with<T>(self, rhs: T) -> Self
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
let rhs = rhs.into();
|
||||
|
||||
(self & rhs) + (self ^ rhs) / 2
|
||||
}
|
||||
}
|
||||
);
|
||||
($struct:ident $vector:ident $type:ident float) => (
|
||||
crate::impl_vector_traits!($struct $vector $type ops);
|
||||
|
||||
impl $struct<$type>
|
||||
where
|
||||
$type: super::VectorElement
|
||||
{
|
||||
pub fn mul_add<A, B>(self, a: A, b: B) -> Self
|
||||
where
|
||||
A: Into<$struct<$type>>,
|
||||
B: Into<$struct<$type>>,
|
||||
{
|
||||
std::simd::StdFloat::mul_add(
|
||||
self.to_simd(),
|
||||
a.into().to_simd(),
|
||||
b.into().to_simd(),
|
||||
).into()
|
||||
}
|
||||
|
||||
pub fn round(&self) -> Self {
|
||||
std::simd::StdFloat::round(
|
||||
self.to_simd()
|
||||
).into()
|
||||
}
|
||||
|
||||
pub fn min<T>(&self, min: T) -> Self
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
std::simd::SimdFloat::simd_min(
|
||||
self.to_simd(),
|
||||
min.into().to_simd()
|
||||
).into()
|
||||
}
|
||||
|
||||
pub fn max<T>(&self, max: T) -> Self
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
std::simd::SimdFloat::simd_max(
|
||||
self.to_simd(),
|
||||
max.into().to_simd()
|
||||
).into()
|
||||
}
|
||||
|
||||
pub fn clamp<T>(&self, min: T, max: T) -> Self
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
std::simd::SimdFloat::simd_clamp(
|
||||
self.to_simd(),
|
||||
min.into().to_simd(),
|
||||
max.into().to_simd()
|
||||
).into()
|
||||
}
|
||||
}
|
||||
);
|
||||
($struct:ident $vector:ident $type:ident ops) => (
|
||||
crate::impl_vector_traits!($struct $vector Add add $type);
|
||||
crate::impl_vector_traits!($struct $vector Sub sub $type);
|
||||
crate::impl_vector_traits!($struct $vector Mul mul $type);
|
||||
crate::impl_vector_traits!($struct $vector Div div $type);
|
||||
|
||||
impl $struct<$type>
|
||||
where
|
||||
$type: super::VectorElement
|
||||
{
|
||||
pub fn dot<T>(self, rhs: T) -> $type
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
(self * rhs).sum()
|
||||
}
|
||||
}
|
||||
);
|
||||
($struct:ident $vector:ident $trait:ident $name:ident $type:ident) => {
|
||||
impl<T> std::ops::$trait<T> for $struct<$type>
|
||||
where
|
||||
T: Into<$struct<$type>>
|
||||
{
|
||||
type Output = $struct<$type>;
|
||||
|
||||
fn $name(self, rhs: T) -> Self::Output {
|
||||
self.to_simd().$name(rhs.into().to_simd()).into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_simd2_overload {
|
||||
($struct:ident $x:ident $y:ident $type:ident $lanes:expr) => {
|
||||
impl super::CreateVector2<$type> for $struct<$type> {
|
||||
fn create($x: $type, $y: $type) -> $struct<$type> {
|
||||
$struct::<$type>::create($x, $y)
|
||||
}
|
||||
}
|
||||
|
||||
impl $struct<$type> {
|
||||
pub const fn create($x: $type, $y: $type) -> $struct<$type> {
|
||||
let mut array = [1 as $type; $lanes];
|
||||
|
||||
array[0] = $x;
|
||||
array[1] = $y;
|
||||
|
||||
$struct {
|
||||
simd: std::simd::Simd::from_array(array),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn splat(value: $type) -> Self {
|
||||
Self::create(value, value)
|
||||
}
|
||||
|
||||
pub const fn $x(self) -> $type {
|
||||
self.simd.as_array()[0]
|
||||
}
|
||||
pub const fn $y(self) -> $type {
|
||||
self.simd.as_array()[1]
|
||||
}
|
||||
|
||||
pub fn sum(self) -> $type
|
||||
where
|
||||
$type: std::ops::Add<$type, Output = $type>,
|
||||
{
|
||||
self.$x() + self.$y()
|
||||
}
|
||||
}
|
||||
|
||||
impl Vector2<$type> for $struct<$type> {
|
||||
fn x(&self) -> $type {
|
||||
$struct::<$type>::$x(*self)
|
||||
}
|
||||
fn y(&self) -> $type {
|
||||
$struct::<$type>::$y(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$type> for $struct<$type> {
|
||||
fn from(value: $type) -> $struct<$type> {
|
||||
$struct::<$type>::create(value, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[$type; $lanes]> for $struct<$type> {
|
||||
fn from(array: [$type; $lanes]) -> $struct<$type> {
|
||||
$struct::<$type>::create(array[0], array[1])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<($type, $type)> for $struct<$type> {
|
||||
fn from(tuple: ($type, $type)) -> $struct<$type> {
|
||||
$struct::<$type>::create(tuple.0, tuple.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<std::simd::Simd<$type, $lanes>> for $struct<$type> {
|
||||
fn into(self) -> std::simd::Simd<$type, $lanes> {
|
||||
self.to_simd()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<$struct<$type>> for std::simd::Simd<$type, $lanes> {
|
||||
fn into(self) -> $struct<$type> {
|
||||
$struct::<$type>::from_simd(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl $struct<$type> {
|
||||
pub const fn from_simd(simd: std::simd::Simd<$type, $lanes>) -> Self {
|
||||
$struct { simd }
|
||||
}
|
||||
|
||||
pub const fn to_simd(&self) -> std::simd::Simd<$type, $lanes> {
|
||||
self.simd
|
||||
}
|
||||
|
||||
pub fn cast<U>(&self) -> $struct<U>
|
||||
where
|
||||
U: std::simd::SimdElement + super::VectorElement,
|
||||
std::simd::Simd<U, $lanes>: Into<$struct<U>>,
|
||||
{
|
||||
self.to_simd().cast().into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_simd3_overload {
|
||||
($struct:ident $x:ident $y:ident $z:ident $type:ident $lanes:expr) => {
|
||||
impl super::CreateVector3<$type> for $struct<$type> {
|
||||
fn create($x: $type, $y: $type, $z: $type) -> $struct<$type> {
|
||||
$struct::<$type>::create($x, $y, $z)
|
||||
}
|
||||
}
|
||||
impl $struct<$type> {
|
||||
pub const fn create($x: $type, $y: $type, $z: $type) -> $struct<$type> {
|
||||
let mut array = [1 as $type; $lanes];
|
||||
|
||||
array[0] = $x;
|
||||
array[1] = $y;
|
||||
array[2] = $z;
|
||||
|
||||
$struct {
|
||||
simd: std::simd::Simd::from_array(array),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn splat(value: $type) -> Self {
|
||||
Self::create(value, value, value)
|
||||
}
|
||||
|
||||
pub const fn $x(self) -> $type {
|
||||
self.simd.as_array()[0]
|
||||
}
|
||||
pub const fn $y(self) -> $type {
|
||||
self.simd.as_array()[1]
|
||||
}
|
||||
pub const fn $z(self) -> $type {
|
||||
self.simd.as_array()[2]
|
||||
}
|
||||
|
||||
pub fn sum(self) -> $type
|
||||
where
|
||||
$type: std::ops::Add<$type, Output = $type>,
|
||||
{
|
||||
self.$x() + self.$y() + self.$z()
|
||||
}
|
||||
}
|
||||
|
||||
impl Vector3<$type> for $struct<$type> {
|
||||
fn x(&self) -> $type {
|
||||
self.$x()
|
||||
}
|
||||
fn y(&self) -> $type {
|
||||
self.$y()
|
||||
}
|
||||
fn z(&self) -> $type {
|
||||
self.$z()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$type> for $struct<$type> {
|
||||
fn from(value: $type) -> $struct<$type> {
|
||||
$struct::<$type>::create(value, value, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[$type; $lanes]> for $struct<$type> {
|
||||
fn from(array: [$type; $lanes]) -> $struct<$type> {
|
||||
$struct::<$type>::create(array[0], array[1], array[2])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<($type, $type, $type)> for $struct<$type> {
|
||||
fn from(tuple: ($type, $type, $type)) -> $struct<$type> {
|
||||
$struct::<$type>::create(tuple.0, tuple.1, tuple.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<std::simd::Simd<$type, $lanes>> for $struct<$type> {
|
||||
fn into(self) -> std::simd::Simd<$type, $lanes> {
|
||||
self.to_simd()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<$struct<$type>> for std::simd::Simd<$type, $lanes> {
|
||||
fn into(self) -> $struct<$type> {
|
||||
$struct::<$type>::from_simd(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl $struct<$type> {
|
||||
pub const fn from_simd(simd: std::simd::Simd<$type, $lanes>) -> Self {
|
||||
$struct { simd }
|
||||
}
|
||||
|
||||
pub const fn to_simd(self) -> std::simd::Simd<$type, $lanes> {
|
||||
self.simd
|
||||
}
|
||||
|
||||
pub fn cast<U>(self) -> $struct<U>
|
||||
where
|
||||
U: std::simd::SimdElement + super::VectorElement,
|
||||
std::simd::Simd<U, $lanes>: Into<$struct<U>>,
|
||||
{
|
||||
self.to_simd().cast().into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { program } from 'commander'
|
||||
import { pipeline } from 'stream/promises'
|
||||
import { mkdir, rm } from 'fs/promises'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { ListenOptions } from 'net'
|
||||
import { ChildProcess, spawn } from 'child_process'
|
||||
import { IncomingMessage, request } from 'http'
|
||||
|
||||
import { Options } from './html2svg'
|
||||
|
||||
if (require.main === module) {
|
||||
const entry = process.argv.find((a) => a.endsWith(__filename))
|
||||
const index = entry ? process.argv.indexOf(entry) : -1
|
||||
const args = process.argv.slice(Math.max(2, index + 1))
|
||||
|
||||
cli(args)
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
export async function cli(args: string[]) {
|
||||
program
|
||||
.name('html2svg')
|
||||
.showHelpAfterError()
|
||||
.showSuggestionAfterError()
|
||||
.argument('<url>', 'URL to the web page to render')
|
||||
.option('-f, --full', 'capture the entire page')
|
||||
.option(
|
||||
'-W, --wait <seconds>',
|
||||
'set the amount of seconds to wait between the page loaded event and taking the screenshot',
|
||||
validateInt,
|
||||
1,
|
||||
)
|
||||
.option(
|
||||
'-w, --width <width>',
|
||||
'set the viewport width in pixels',
|
||||
validateInt,
|
||||
1920,
|
||||
)
|
||||
.option(
|
||||
'-h, --height <height>',
|
||||
'set the viewport height in pixels',
|
||||
validateInt,
|
||||
1080,
|
||||
)
|
||||
.option(
|
||||
'-f, --format <format>',
|
||||
'set the output format, should one of these values: svg, pdf, png, jpg, webp',
|
||||
'svg',
|
||||
)
|
||||
.action(async (url, options) => {
|
||||
const id = Array.from(randomBytes(16))
|
||||
.map((x) => x.toString(36).padStart(2, '0'))
|
||||
.join('')
|
||||
const dir = join(tmpdir(), 'html2svg-server')
|
||||
const path = join(dir, `${id}.sock`)
|
||||
|
||||
await mkdir(dir, { recursive: true })
|
||||
|
||||
try {
|
||||
const server = serve({ path, log: false })
|
||||
|
||||
await Promise.all([
|
||||
server.wait(),
|
||||
callServer(url, options, server.process, path),
|
||||
])
|
||||
} finally {
|
||||
await rm(path, { force: true })
|
||||
}
|
||||
})
|
||||
|
||||
program
|
||||
.command('serve')
|
||||
.option(
|
||||
'-H, --host <hostname>',
|
||||
'set the hostname to listen on',
|
||||
'0.0.0.0',
|
||||
)
|
||||
.option(
|
||||
'-p, --port <hostname>',
|
||||
'set the port to listen on',
|
||||
validateInt,
|
||||
8080,
|
||||
)
|
||||
.option('-u, --unix <path>', 'set the unix socket to listen on')
|
||||
.action(
|
||||
async ({ host, port, unix }) =>
|
||||
await serve(unix ? { path: unix } : { host, port }).wait(),
|
||||
)
|
||||
|
||||
await program.parseAsync(args, { from: 'user' })
|
||||
}
|
||||
|
||||
async function callServer(
|
||||
url: string,
|
||||
options: Options,
|
||||
server: ChildProcess,
|
||||
socketPath: string,
|
||||
) {
|
||||
const start = Date.now()
|
||||
|
||||
while (Date.now() - start < 10_000) {
|
||||
const done = await new Promise<boolean>((resolve, reject) =>
|
||||
request({ method: 'POST', socketPath })
|
||||
.on('error', (error: any) => {
|
||||
if (error?.code === 'ENOENT') {
|
||||
resolve(false)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
.on('response', (res) =>
|
||||
printRequest(res)
|
||||
.then(() => resolve(true))
|
||||
.catch(reject),
|
||||
)
|
||||
.end(JSON.stringify({ url, ...options })),
|
||||
)
|
||||
|
||||
if (done) {
|
||||
return server.kill()
|
||||
} else {
|
||||
await sleep(100)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for server to start')
|
||||
}
|
||||
|
||||
async function printRequest(res: IncomingMessage) {
|
||||
if (res.statusCode !== 200) {
|
||||
throw new Error(`Server error ${res.statusCode}`)
|
||||
}
|
||||
|
||||
await pipeline(res, process.stdout)
|
||||
}
|
||||
|
||||
function validateInt(string: string) {
|
||||
const number = parseInt(string, 10)
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
throw new Error(`Invalid number value: ${string}`)
|
||||
}
|
||||
|
||||
return number
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function serve(options: ListenOptions & { log?: boolean }) {
|
||||
const child = spawn(
|
||||
require.resolve('./runtime/electron'),
|
||||
['--no-sandbox', require.resolve('./html2svg.server')],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
HTML2SVG_SERVER_OPTIONS: JSON.stringify(options),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
process: child,
|
||||
async wait() {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child.on('error', reject).on('close', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`Server quit with signal ${signal}`))
|
||||
} else if (code !== 0) {
|
||||
reject(new Error(`Server quit with code ${code}`))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { createServer } from 'http'
|
||||
import { ListenOptions } from 'net'
|
||||
|
||||
import { readStream } from './read-stream'
|
||||
import { html2svg, Options } from './html2svg'
|
||||
|
||||
if (require.main === module) {
|
||||
const options = JSON.parse(process.env.HTML2SVG_SERVER_OPTIONS ?? '{}')
|
||||
const { path, host, port, log } = options
|
||||
|
||||
server(options)
|
||||
.then(() => {
|
||||
if (log !== false) {
|
||||
process.stderr.write(
|
||||
`Listening on ${
|
||||
path ? `unix socket ${path}` : `${host}:${port}`
|
||||
}\n`,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
export async function server(listen: ListenOptions) {
|
||||
const server = createServer((req, res) => {
|
||||
const { url } = req
|
||||
|
||||
if (url !== '/') {
|
||||
return res.writeHead(404).end('Not Found')
|
||||
}
|
||||
|
||||
readStream(req)
|
||||
.then(async (data) => {
|
||||
const body = parseOptions(parseJSON(data.toString('utf-8')))
|
||||
|
||||
if (!body) {
|
||||
return res.writeHead(400).end('Invalid request params')
|
||||
}
|
||||
|
||||
const buffer = await html2svg(body.url, body.options)
|
||||
|
||||
res.writeHead(200).end(buffer)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Internal server error', error)
|
||||
|
||||
res.writeHead(500).end('Internal Server Error')
|
||||
})
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.on('error', reject).on('listening', resolve).listen(listen),
|
||||
)
|
||||
}
|
||||
|
||||
function parseOptions(data: any): null | { url: string; options?: Options } {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return { url: data }
|
||||
}
|
||||
|
||||
if (typeof data !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const { url, ...options } = data
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return { url, options }
|
||||
}
|
||||
|
||||
function parseJSON(data: string) {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}
|
129
src/html2svg.ts
129
src/html2svg.ts
@ -1,129 +0,0 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
|
||||
export interface Options {
|
||||
full?: boolean
|
||||
wait?: number
|
||||
width?: number
|
||||
height?: number
|
||||
format?: 'svg' | 'pdf' | 'png' | 'jpg' | 'webp'
|
||||
}
|
||||
|
||||
app.dock?.hide()
|
||||
app.disableHardwareAcceleration()
|
||||
app.commandLine.appendSwitch('no-sandbox')
|
||||
app.on('window-all-closed', () => {})
|
||||
|
||||
export async function html2svg(
|
||||
url: string,
|
||||
{ full, wait, format, width = 1920, height = 1080 }: Options = {},
|
||||
) {
|
||||
const mode = getMode(format ?? 'svg')
|
||||
|
||||
await app.whenReady()
|
||||
|
||||
const args = [
|
||||
'--mute-audio',
|
||||
'--disable-audio-output',
|
||||
'--disable-dev-shm-usage',
|
||||
'--force-color-profile=srgb',
|
||||
]
|
||||
|
||||
if (mode === 0) {
|
||||
args.push('--html2svg-svg-mode', '--disable-remote-fonts')
|
||||
}
|
||||
|
||||
const page = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
sandbox: false,
|
||||
offscreen: true,
|
||||
additionalArguments: args,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const timeout = setTimeout(() => {
|
||||
page.webContents.off('did-finish-load', listener)
|
||||
|
||||
reject(new Error('timeout'))
|
||||
}, 10_000)
|
||||
const listener = () => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
page.webContents.once('did-finish-load', listener)
|
||||
|
||||
await page.loadURL(url)
|
||||
})
|
||||
.catch(reject),
|
||||
)
|
||||
|
||||
await page.webContents.executeJavaScriptInIsolatedWorld(1, [
|
||||
{
|
||||
code: `
|
||||
new Promise(resolve => {
|
||||
const style = document.createElement('style')
|
||||
|
||||
style.innerHTML = trustedTypes
|
||||
.createPolicy('html2svg/scrollbar-css', { createHTML: x => x })
|
||||
.createHTML(\`
|
||||
*::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar-track,
|
||||
*::-webkit-scrollbar-thumb {
|
||||
display: none;
|
||||
}
|
||||
\`)
|
||||
|
||||
document.head.appendChild(style)
|
||||
scrollTo({ top: document.body.scrollHeight })
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
scrollTo({ top: 0 })
|
||||
|
||||
requestAnimationFrame(() =>
|
||||
setTimeout(resolve, ${(wait ?? 0) * 1000})
|
||||
)
|
||||
})
|
||||
})
|
||||
`,
|
||||
},
|
||||
])
|
||||
|
||||
const buffer: ArrayBuffer = await page.webContents.executeJavaScript(`
|
||||
getPageContentsAsSVG(
|
||||
${full ? 0 : height} * devicePixelRatio,
|
||||
${mode},
|
||||
document.title,
|
||||
)
|
||||
`)
|
||||
|
||||
return Buffer.from(buffer)
|
||||
} finally {
|
||||
page.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function getMode(format: string) {
|
||||
switch (format) {
|
||||
case 'svg':
|
||||
return 0
|
||||
case 'pdf':
|
||||
return 1
|
||||
case 'png':
|
||||
return 2
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 3
|
||||
case 'webp':
|
||||
return 4
|
||||
default:
|
||||
throw new Error(`Unsupported output format: ${format}`)
|
||||
}
|
||||
}
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#![feature(c_unwind)]
|
||||
pub mod browser;
|
||||
pub mod gfx;
|
||||
pub mod terminal;
|
10
src/mojom/BUILD.gn
Normal file
10
src/mojom/BUILD.gn
Normal file
@ -0,0 +1,10 @@
|
||||
import("//mojo/public/tools/bindings/mojom.gni")
|
||||
|
||||
mojom("mojom") {
|
||||
sources = [ "carbonyl.mojom" ]
|
||||
|
||||
deps = [
|
||||
"//ui/gfx/geometry/mojom",
|
||||
"//skia/public/mojom",
|
||||
]
|
||||
}
|
14
src/mojom/carbonyl.mojom
Normal file
14
src/mojom/carbonyl.mojom
Normal file
@ -0,0 +1,14 @@
|
||||
module carbonyl.mojom;
|
||||
|
||||
import "ui/gfx/geometry/mojom/geometry.mojom";
|
||||
import "skia/public/mojom/skcolor.mojom";
|
||||
|
||||
struct TextData {
|
||||
string contents;
|
||||
gfx.mojom.RectF bounds;
|
||||
skia.mojom.SkColor color;
|
||||
};
|
||||
|
||||
interface CarbonylRenderService {
|
||||
DrawText(array<TextData> data);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
export function readStream(stream: NodeJS.ReadableStream) {
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
return new Promise<Buffer>((resolve, reject) =>
|
||||
stream
|
||||
.on('data', (chunk) => chunks.push(chunk))
|
||||
.on('error', (error) => reject(error))
|
||||
.on('end', () => resolve(Buffer.concat(chunks))),
|
||||
)
|
||||
}
|
719
src/skia.patch
719
src/skia.patch
@ -1,708 +1,15 @@
|
||||
diff --git a/src/svg/SkSVGDevice.cpp b/src/svg/SkSVGDevice.cpp
|
||||
index dcb51de458..9049072b19 100644
|
||||
--- a/src/svg/SkSVGDevice.cpp
|
||||
+++ b/src/svg/SkSVGDevice.cpp
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "src/core/SkClipStack.h"
|
||||
#include "src/core/SkDevice.h"
|
||||
#include "src/core/SkFontPriv.h"
|
||||
+#include "src/core/SkMaskFilterBase.h"
|
||||
#include "src/core/SkTLazy.h"
|
||||
#include "src/image/SkImage_Base.h"
|
||||
#include "src/shaders/SkShaderBase.h"
|
||||
@@ -57,6 +58,9 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
+#include <iostream>
|
||||
+
|
||||
+#include "src/utils/SkUTF.h"
|
||||
|
||||
#if SK_SUPPORT_GPU
|
||||
class SkMesh;
|
||||
@@ -185,6 +189,7 @@ struct Resources {
|
||||
|
||||
SkString fPaintServer;
|
||||
SkString fColorFilter;
|
||||
+ SkString fMaskFilter;
|
||||
};
|
||||
|
||||
// Determine if the paint requires us to reset the viewport.
|
||||
@@ -241,7 +246,9 @@ public:
|
||||
, fPathCount(0)
|
||||
, fImageCount(0)
|
||||
, fPatternCount(0)
|
||||
- , fColorFilterCount(0) {}
|
||||
+ , fColorFilterCount(0)
|
||||
+ , fGroupCount(0)
|
||||
+ , fFilterCount(0) {}
|
||||
|
||||
SkString addLinearGradient() {
|
||||
return SkStringPrintf("gradient_%d", fGradientCount++);
|
||||
@@ -261,12 +268,22 @@ public:
|
||||
return SkStringPrintf("pattern_%d", fPatternCount++);
|
||||
}
|
||||
|
||||
+ SkString addGroup() {
|
||||
+ return SkStringPrintf("group_%d", fGroupCount++);
|
||||
+ }
|
||||
+
|
||||
+ SkString addFilter() {
|
||||
+ return SkStringPrintf("filter_%d", fFilterCount++);
|
||||
+ }
|
||||
+
|
||||
private:
|
||||
uint32_t fGradientCount;
|
||||
uint32_t fPathCount;
|
||||
uint32_t fImageCount;
|
||||
uint32_t fPatternCount;
|
||||
uint32_t fColorFilterCount;
|
||||
+ uint32_t fGroupCount;
|
||||
+ uint32_t fFilterCount;
|
||||
};
|
||||
|
||||
struct SkSVGDevice::MxCp {
|
||||
@@ -296,6 +313,14 @@ public:
|
||||
svgdev->syncClipStack(*mc.fClipStack);
|
||||
Resources res = this->addResources(mc, paint);
|
||||
|
||||
+ if (!svgdev->fClipStack.empty()) {
|
||||
+ const auto clip_path = SkStringPrintf("url(#cl_%x)", svgdev->fClipStack.back());
|
||||
+
|
||||
+ fCloseGroup = true;
|
||||
+ fWriter->startElement("g");
|
||||
+ fWriter->addAttribute("clip-path", clip_path.c_str());
|
||||
+ }
|
||||
+
|
||||
fWriter->startElement(name);
|
||||
|
||||
this->addPaint(paint, res);
|
||||
@@ -307,6 +332,10 @@ public:
|
||||
|
||||
~AutoElement() {
|
||||
fWriter->endElement();
|
||||
+
|
||||
+ if (fCloseGroup) {
|
||||
+ fWriter->endElement();
|
||||
+ }
|
||||
}
|
||||
|
||||
void addAttribute(const char name[], const char val[]) {
|
||||
@@ -351,6 +380,7 @@ private:
|
||||
|
||||
SkXMLWriter* fWriter;
|
||||
ResourceBucket* fResourceBucket;
|
||||
+ bool fCloseGroup = false;
|
||||
};
|
||||
|
||||
void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& resources) {
|
||||
@@ -366,18 +396,25 @@ void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& r
|
||||
static constexpr char kDefaultFill[] = "black";
|
||||
if (!resources.fPaintServer.equals(kDefaultFill)) {
|
||||
this->addAttribute("fill", resources.fPaintServer);
|
||||
+ }
|
||||
|
||||
- if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
|
||||
- this->addAttribute("fill-opacity", svg_opacity(paint.getColor()));
|
||||
- }
|
||||
+ if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
|
||||
+ this->addAttribute("fill-opacity", svg_opacity(paint.getColor()));
|
||||
}
|
||||
} else {
|
||||
SkASSERT(style == SkPaint::kStroke_Style);
|
||||
this->addAttribute("fill", "none");
|
||||
}
|
||||
|
||||
- if (!resources.fColorFilter.isEmpty()) {
|
||||
- this->addAttribute("filter", resources.fColorFilter.c_str());
|
||||
+ if (!resources.fColorFilter.isEmpty() || !resources.fMaskFilter.isEmpty()) {
|
||||
+ this->addAttribute(
|
||||
+ "filter",
|
||||
+ SkStringPrintf(
|
||||
+ "%s %s",
|
||||
+ resources.fColorFilter.isEmpty() ? "" : resources.fColorFilter.c_str(),
|
||||
+ resources.fMaskFilter.isEmpty() ? "" : resources.fMaskFilter.c_str()
|
||||
+ )
|
||||
+ );
|
||||
}
|
||||
|
||||
if (style == SkPaint::kStroke_Style || style == SkPaint::kStrokeAndFill_Style) {
|
||||
@@ -416,16 +453,29 @@ Resources SkSVGDevice::AutoElement::addResources(const MxCp& mc, const SkPaint&
|
||||
Resources resources(paint);
|
||||
|
||||
if (paint.getShader()) {
|
||||
- AutoElement defs("defs", fWriter);
|
||||
-
|
||||
this->addShaderResources(paint, &resources);
|
||||
}
|
||||
|
||||
if (const SkColorFilter* cf = paint.getColorFilter()) {
|
||||
// TODO: Implement skia color filters for blend modes other than SrcIn
|
||||
- SkBlendMode mode;
|
||||
- if (cf->asAColorMode(nullptr, &mode) && mode == SkBlendMode::kSrcIn) {
|
||||
- this->addColorFilterResources(*cf, &resources);
|
||||
+ this->addColorFilterResources(*cf, &resources);
|
||||
+ }
|
||||
+
|
||||
+ if (const SkMaskFilter* mf = paint.getMaskFilter()) {
|
||||
+ SkMaskFilterBase::BlurRec maskBlur;
|
||||
+
|
||||
+ if (as_MFB(mf)->asABlur(&maskBlur) && maskBlur.fStyle == kNormal_SkBlurStyle) {
|
||||
+ SkString maskfilterID = fResourceBucket->addColorFilter();
|
||||
+
|
||||
+ AutoElement filterElement("filter", fWriter);
|
||||
+
|
||||
+ filterElement.addAttribute("id", maskfilterID);
|
||||
+
|
||||
+ AutoElement floodElement("feGaussianBlur", fWriter);
|
||||
+
|
||||
+ floodElement.addAttribute("stdDeviation", maskBlur.fSigma);
|
||||
+
|
||||
+ resources.fMaskFilter.printf("url(#%s)", maskfilterID.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +514,14 @@ void SkSVGDevice::AutoElement::addGradientShaderResources(const SkShader* shader
|
||||
void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
|
||||
Resources* resources) {
|
||||
SkString colorfilterID = fResourceBucket->addColorFilter();
|
||||
+ SkColor filterColor;
|
||||
+ SkBlendMode mode;
|
||||
+ bool asAColorMode = cf.asAColorMode(&filterColor, &mode);
|
||||
+
|
||||
+ if (!asAColorMode) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
{
|
||||
AutoElement filterElement("filter", fWriter);
|
||||
filterElement.addAttribute("id", colorfilterID);
|
||||
@@ -472,12 +530,6 @@ void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
|
||||
filterElement.addAttribute("width", "100%");
|
||||
filterElement.addAttribute("height", "100%");
|
||||
|
||||
- SkColor filterColor;
|
||||
- SkBlendMode mode;
|
||||
- bool asAColorMode = cf.asAColorMode(&filterColor, &mode);
|
||||
- SkAssertResult(asAColorMode);
|
||||
- SkASSERT(mode == SkBlendMode::kSrcIn);
|
||||
-
|
||||
{
|
||||
// first flood with filter color
|
||||
AutoElement floodElement("feFlood", fWriter);
|
||||
@@ -490,7 +542,7 @@ void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
|
||||
// apply the transform to filter color
|
||||
AutoElement compositeElement("feComposite", fWriter);
|
||||
compositeElement.addAttribute("in", "flood");
|
||||
- compositeElement.addAttribute("operator", "in");
|
||||
+ compositeElement.addAttribute("operator", mode == SkBlendMode::kSrcIn ? "atop" : "out");
|
||||
}
|
||||
}
|
||||
resources->fColorFilter.printf("url(#%s)", colorfilterID.c_str());
|
||||
@@ -707,12 +759,16 @@ void SkSVGDevice::AutoElement::addTextAttributes(const SkFont& font) {
|
||||
continue;
|
||||
}
|
||||
familySet.add(familyString.fString);
|
||||
- familyName.appendf((familyName.isEmpty() ? "%s" : ", %s"), familyString.fString.c_str());
|
||||
+ familyName.appendf((familyName.isEmpty() ? "'%s'" : ", '%s'"), familyString.fString.c_str());
|
||||
}
|
||||
}
|
||||
- if (!familyName.isEmpty()) {
|
||||
- this->addAttribute("font-family", familyName);
|
||||
- }
|
||||
+
|
||||
+ familyName.appendf(
|
||||
+ (familyName.isEmpty() ? "%s" : ", %s"),
|
||||
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
|
||||
+ );
|
||||
+
|
||||
+ this->addAttribute("font-family", familyName);
|
||||
diff --git a/src/core/SkBitmapDevice.cpp b/src/core/SkBitmapDevice.cpp
|
||||
index b497d690f7..5e4ee2b1ce 100644
|
||||
--- a/src/core/SkBitmapDevice.cpp
|
||||
+++ b/src/core/SkBitmapDevice.cpp
|
||||
@@ -522,8 +522,8 @@ void SkBitmapDevice::onDrawGlyphRunList(SkCanvas* canvas,
|
||||
const sktext::GlyphRunList& glyphRunList,
|
||||
const SkPaint& initialPaint,
|
||||
const SkPaint& drawingPaint) {
|
||||
- SkASSERT(!glyphRunList.hasRSXForm());
|
||||
- LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr )
|
||||
+ // SkASSERT(!glyphRunList.hasRSXForm());
|
||||
+ // LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr )
|
||||
}
|
||||
|
||||
sk_sp<SkBaseDevice> SkSVGDevice::Make(const SkISize& size, std::unique_ptr<SkXMLWriter> writer,
|
||||
@@ -721,12 +777,18 @@ sk_sp<SkBaseDevice> SkSVGDevice::Make(const SkISize& size, std::unique_ptr<SkXML
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
+
|
||||
SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> writer, uint32_t flags)
|
||||
+ : SkSVGDevice(size, std::move(writer), flags, nullptr)
|
||||
+{}
|
||||
+
|
||||
+SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> writer, uint32_t flags, SkSVGDevice* root)
|
||||
: INHERITED(SkImageInfo::MakeUnknown(size.fWidth, size.fHeight),
|
||||
SkSurfaceProps(0, kUnknown_SkPixelGeometry))
|
||||
, fWriter(std::move(writer))
|
||||
- , fResourceBucket(new ResourceBucket)
|
||||
+ , fResourceBucket(root ? nullptr : new ResourceBucket)
|
||||
, fFlags(flags)
|
||||
+ , fRootDevice(root ? root : this)
|
||||
{
|
||||
SkASSERT(fWriter);
|
||||
|
||||
@@ -739,15 +801,44 @@ SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> write
|
||||
fRootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||
fRootElement->addAttribute("width", size.width());
|
||||
fRootElement->addAttribute("height", size.height());
|
||||
+
|
||||
+ fRootDefsElement = std::make_unique<AutoElement>("defs", fWriter);
|
||||
+ fRootGroupElement = std::make_unique<AutoElement>("g", fWriter);
|
||||
+ fRootElementID = bucket()->addGroup();
|
||||
+
|
||||
+ fRootGroupElement->addAttribute("id", fRootElementID);
|
||||
}
|
||||
|
||||
-SkSVGDevice::~SkSVGDevice() {
|
||||
+void SkSVGDevice::closeClipStack() {
|
||||
// Pop order is important.
|
||||
while (!fClipStack.empty()) {
|
||||
fClipStack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
+void SkSVGDevice::closeWriter() {
|
||||
+ closeClipStack();
|
||||
+
|
||||
+ if (!fRootElement) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ fRootGroupElement.reset();
|
||||
+ fRootDefsElement.reset();
|
||||
+
|
||||
+ if (fWriteUseElement) {
|
||||
+ AutoElement elem("use", fWriter);
|
||||
+
|
||||
+ elem.addAttribute("href", SkStringPrintf("#%s", fRootElementID.c_str()));
|
||||
+ }
|
||||
+
|
||||
+ fRootElement.reset();
|
||||
+}
|
||||
+
|
||||
+SkSVGDevice::~SkSVGDevice() {
|
||||
+ closeWriter();
|
||||
+}
|
||||
+
|
||||
SkParsePath::PathEncoding SkSVGDevice::pathEncoding() const {
|
||||
return (fFlags & SkSVGCanvas::kRelativePathEncoding_Flag)
|
||||
? SkParsePath::PathEncoding::Relative
|
||||
@@ -762,7 +853,7 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
|
||||
|
||||
// First, find/preserve the common bottom.
|
||||
while ((elem = iter.next()) && (rec_idx < fClipStack.size())) {
|
||||
- if (fClipStack[SkToInt(rec_idx)].fGenID != elem->getGenID()) {
|
||||
+ if (fClipStack[SkToInt(rec_idx)] != elem->getGenID()) {
|
||||
break;
|
||||
}
|
||||
rec_idx++;
|
||||
@@ -779,6 +870,13 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
|
||||
AutoElement clip_path("clipPath", fWriter);
|
||||
clip_path.addAttribute("id", cid);
|
||||
|
||||
+ if (!fClipStack.empty()) {
|
||||
+ clip_path.addAttribute(
|
||||
+ "clip-path",
|
||||
+ SkStringPrintf("url(#cl_%x)", fClipStack.back())
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
// TODO: handle non-intersect clips.
|
||||
|
||||
switch (e->getDeviceSpaceType()) {
|
||||
@@ -812,25 +910,25 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
|
||||
// TODO: handle shader clipping, perhaps rasterize and apply as a mask image?
|
||||
break;
|
||||
}
|
||||
-
|
||||
- return cid;
|
||||
};
|
||||
|
||||
// Rebuild the top.
|
||||
while (elem) {
|
||||
- const auto cid = define_clip(elem);
|
||||
-
|
||||
- auto clip_grp = std::make_unique<AutoElement>("g", fWriter);
|
||||
- clip_grp->addAttribute("clip-path", SkStringPrintf("url(#%s)", cid.c_str()));
|
||||
-
|
||||
- fClipStack.push_back({ std::move(clip_grp), elem->getGenID() });
|
||||
+ if (
|
||||
+ elem->getDeviceSpaceType() != SkClipStack::Element::DeviceSpaceType::kEmpty &&
|
||||
+ elem->getOp() == SkClipOp::kIntersect
|
||||
+ ) {
|
||||
+ define_clip(elem);
|
||||
+
|
||||
+ fClipStack.push_back(elem->getGenID());
|
||||
+ }
|
||||
|
||||
elem = iter.next();
|
||||
}
|
||||
}
|
||||
|
||||
void SkSVGDevice::drawPaint(const SkPaint& paint) {
|
||||
- AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
|
||||
+ AutoElement rect("rect", this, bucket(), MxCp(this), paint);
|
||||
rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()),
|
||||
SkIntToScalar(this->height())));
|
||||
}
|
||||
@@ -890,11 +988,11 @@ void SkSVGDevice::drawPoints(SkCanvas::PointMode mode, size_t count,
|
||||
void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
|
||||
std::unique_ptr<AutoElement> svg;
|
||||
if (RequiresViewportReset(paint)) {
|
||||
- svg = std::make_unique<AutoElement>("svg", this, fResourceBucket.get(), MxCp(this), paint);
|
||||
+ svg = std::make_unique<AutoElement>("svg", this, bucket(), MxCp(this), paint);
|
||||
svg->addRectAttributes(r);
|
||||
}
|
||||
|
||||
- AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
|
||||
+ AutoElement rect("rect", this, bucket(), MxCp(this), paint);
|
||||
|
||||
if (svg) {
|
||||
rect.addAttribute("x", 0);
|
||||
@@ -907,7 +1005,7 @@ void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
|
||||
}
|
||||
|
||||
void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
|
||||
- AutoElement ellipse("ellipse", this, fResourceBucket.get(), MxCp(this), paint);
|
||||
+ AutoElement ellipse("ellipse", this, bucket(), MxCp(this), paint);
|
||||
ellipse.addAttribute("cx", oval.centerX());
|
||||
ellipse.addAttribute("cy", oval.centerY());
|
||||
ellipse.addAttribute("rx", oval.width() / 2);
|
||||
@@ -915,7 +1013,7 @@ void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
|
||||
}
|
||||
|
||||
void SkSVGDevice::drawRRect(const SkRRect& rr, const SkPaint& paint) {
|
||||
- AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), paint);
|
||||
+ AutoElement elem("path", this, bucket(), MxCp(this), paint);
|
||||
elem.addPathAttributes(SkPath::RRect(rr), this->pathEncoding());
|
||||
}
|
||||
|
||||
@@ -948,7 +1046,7 @@ void SkSVGDevice::drawPath(const SkPath& path, const SkPaint& paint, bool pathIs
|
||||
}
|
||||
|
||||
// Create path element.
|
||||
- AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), *path_paint);
|
||||
+ AutoElement elem("path", this, bucket(), MxCp(this), *path_paint);
|
||||
elem.addPathAttributes(*pathPtr, this->pathEncoding());
|
||||
|
||||
// TODO: inverse fill types?
|
||||
@@ -975,7 +1073,7 @@ void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkP
|
||||
SkString svgImageData("data:image/png;base64,");
|
||||
svgImageData.append(b64Data.get(), b64Size);
|
||||
|
||||
- SkString imageID = fResourceBucket->addImage();
|
||||
+ SkString imageID = bucket()->addImage();
|
||||
{
|
||||
AutoElement defs("defs", fWriter);
|
||||
{
|
||||
@@ -988,7 +1086,7 @@ void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkP
|
||||
}
|
||||
|
||||
{
|
||||
- AutoElement imageUse("use", this, fResourceBucket.get(), mc, paint);
|
||||
+ AutoElement imageUse("use", this, bucket(), mc, paint);
|
||||
imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str()));
|
||||
}
|
||||
}
|
||||
@@ -1023,18 +1121,60 @@ public:
|
||||
SkAutoSTArray<64, SkUnichar> unichars(runSize);
|
||||
SkFontPriv::GlyphsToUnichars(glyphRun.font(), glyphRun.glyphsIDs().data(),
|
||||
runSize, unichars.get());
|
||||
- auto positions = glyphRun.positions();
|
||||
+ auto position = fOrigin;
|
||||
+
|
||||
+ if (runSize > 0) {
|
||||
+ position += glyphRun.positions()[0];
|
||||
+ }
|
||||
+
|
||||
+ fPosXStr.appendf("%.8g", position.fX);
|
||||
+ fPosYStr.appendf("%.8g", position.fY);
|
||||
+
|
||||
+ auto input = std::make_unique<char[]>(runSize);
|
||||
+
|
||||
for (size_t i = 0; i < runSize; ++i) {
|
||||
- this->appendUnichar(unichars[i], positions[i]);
|
||||
+ input[i] = unichars[i];
|
||||
+ }
|
||||
+
|
||||
+ size_t size = 0;
|
||||
+ auto error = SkBase64::Decode(input.get(), runSize, nullptr, &size);
|
||||
+
|
||||
+ if (error != SkBase64::kNoError) {
|
||||
+ std::cerr << "Failed to decode SVG base64 text data, size=" << runSize << std::endl;
|
||||
+
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ auto utf8 = std::make_unique<char[]>(size);
|
||||
+
|
||||
+ error = SkBase64::Decode(input.get(), runSize, utf8.get(), &size);
|
||||
+
|
||||
+ if (error != SkBase64::kNoError) {
|
||||
+ std::cerr << "Failed to decode SVG base64 text data, size=" << runSize << std::endl;
|
||||
+
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ auto* utf8_start = static_cast<const char*>(utf8.get());
|
||||
+ auto* utf8_end = utf8_start + std::strlen(utf8_start);
|
||||
+
|
||||
+ while (utf8_start < utf8_end) {
|
||||
+ SkUnichar unichar = SkUTF::NextUTF8(&utf8_start, utf8_end);
|
||||
+
|
||||
+ if (unichar == '\0') {
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ this->appendUnichar(unichar);
|
||||
}
|
||||
}
|
||||
|
||||
const SkString& text() const { return fText; }
|
||||
const SkString& posX() const { return fPosXStr; }
|
||||
- const SkString& posY() const { return fHasConstY ? fConstYStr : fPosYStr; }
|
||||
+ const SkString& posY() const { return fPosYStr; }
|
||||
|
||||
private:
|
||||
- void appendUnichar(SkUnichar c, SkPoint position) {
|
||||
+ void appendUnichar(SkUnichar c) {
|
||||
bool discardPos = false;
|
||||
bool isWhitespace = false;
|
||||
|
||||
@@ -1077,31 +1217,13 @@ private:
|
||||
}
|
||||
|
||||
fLastCharWasWhitespace = isWhitespace;
|
||||
-
|
||||
- if (discardPos) {
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- position += fOrigin;
|
||||
- fPosXStr.appendf("%.8g, ", position.fX);
|
||||
- fPosYStr.appendf("%.8g, ", position.fY);
|
||||
-
|
||||
- if (fConstYStr.isEmpty()) {
|
||||
- fConstYStr = fPosYStr;
|
||||
- fConstY = position.fY;
|
||||
- } else {
|
||||
- fHasConstY &= SkScalarNearlyEqual(fConstY, position.fY);
|
||||
- }
|
||||
}
|
||||
|
||||
const SkPoint fOrigin;
|
||||
|
||||
SkString fText,
|
||||
- fPosXStr, fPosYStr,
|
||||
- fConstYStr;
|
||||
- SkScalar fConstY;
|
||||
- bool fLastCharWasWhitespace = true, // start off in whitespace mode to strip leading space
|
||||
- fHasConstY = true;
|
||||
+ fPosXStr, fPosYStr;
|
||||
+ bool fLastCharWasWhitespace = true; // start off in whitespace mode to strip leading space
|
||||
};
|
||||
|
||||
void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas,
|
||||
@@ -1126,7 +1248,7 @@ void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas,
|
||||
|
||||
// Emit one <text> element for each run.
|
||||
for (auto& glyphRun : glyphRunList) {
|
||||
- AutoElement elem("text", this, fResourceBucket.get(), MxCp(this), drawingPaint);
|
||||
+ AutoElement elem("text", this, bucket(), MxCp(this), drawingPaint);
|
||||
elem.addTextAttributes(glyphRun.font());
|
||||
|
||||
SVGTextBuilder builder(glyphRunList.origin(), glyphRun);
|
||||
@@ -1145,3 +1267,106 @@ void SkSVGDevice::drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) {
|
||||
// todo
|
||||
}
|
||||
#endif
|
||||
+
|
||||
+SkBaseDevice* SkSVGDevice::onCreateDevice(const CreateInfo& info, const SkPaint* paint) {
|
||||
+ return fLayers.emplace_back(
|
||||
+ std::make_unique<Layer>(
|
||||
+ SkISize::Make(info.fInfo.width(), info.fInfo.height()),
|
||||
+ fFlags,
|
||||
+ fRootDevice
|
||||
+ )
|
||||
+ )->fDevice;
|
||||
+}
|
||||
+
|
||||
+void SkSVGDevice::drawDevice(SkBaseDevice* baseDevice, const SkSamplingOptions& options, const SkPaint& paint) {
|
||||
+ for (auto& layerPtr : fLayers) {
|
||||
+ auto* layer = layerPtr.get();
|
||||
+ auto* device = layer->fDevice;
|
||||
+
|
||||
+ if (device == baseDevice) {
|
||||
+ auto blendMode = paint.getBlendMode_or(SkBlendMode::kClear);
|
||||
+
|
||||
+ SkASSERT(device->fRootDevice == fRootDevice);
|
||||
+ SkASSERT(device->bucket() == bucket());
|
||||
+
|
||||
+ if (layer->fNode == nullptr) {
|
||||
+ if(blendMode == SkBlendMode::kDstIn) {
|
||||
+ device->fWriteUseElement = false;
|
||||
+ }
|
||||
+
|
||||
+ device->closeWriter();
|
||||
+ layer->fDevice = nullptr;
|
||||
+ layer->fNode = layer->fDom.finishParsing();
|
||||
+ }
|
||||
+
|
||||
+ bool skip_root = true; // the root element is <svg>, skip it
|
||||
+ SkMatrix matrix = device->getRelativeTransform(*this);
|
||||
+
|
||||
+ if (blendMode == SkBlendMode::kDstIn) {
|
||||
+ const auto srcID = device->fRootElementID;
|
||||
+ const auto dstID = fRootElementID;
|
||||
+ const auto filterID = bucket()->addFilter();
|
||||
+
|
||||
+ {
|
||||
+ closeClipStack();
|
||||
+
|
||||
+ fRootGroupElement.reset();
|
||||
+ fRootGroupElement = std::make_unique<AutoElement>("g", fWriter);
|
||||
+ fRootElementID = bucket()->addGroup();
|
||||
+ fRootGroupElement->addAttribute("id", fRootElementID);
|
||||
+ }
|
||||
+
|
||||
+ fWriter->writeDOM(layer->fDom, layer->fNode, skip_root);
|
||||
+
|
||||
+ {
|
||||
+ AutoElement filter("filter", fWriter);
|
||||
+
|
||||
+ filter.addAttribute("x", "0");
|
||||
+ filter.addAttribute("y", "0");
|
||||
+ filter.addAttribute("width", "100%");
|
||||
+ filter.addAttribute("height", "100%");
|
||||
+ filter.addAttribute("id", filterID);
|
||||
+ filter.addAttribute("filterUnits", "userSpaceOnUse");
|
||||
+
|
||||
+ {
|
||||
+ AutoElement srcImage("feImage", fWriter);
|
||||
+
|
||||
+ srcImage.addAttribute("href", SkStringPrintf("#%s", srcID.c_str()));
|
||||
+ srcImage.addAttribute("result", "src");
|
||||
+ }
|
||||
+
|
||||
+ {
|
||||
+ AutoElement dstImage("feImage", fWriter);
|
||||
+
|
||||
+ dstImage.addAttribute("href", SkStringPrintf("#%s", dstID.c_str()));
|
||||
+ dstImage.addAttribute("result", "dst");
|
||||
+ }
|
||||
+
|
||||
+ {
|
||||
+ AutoElement feComposite("feComposite", fWriter);
|
||||
+
|
||||
+ feComposite.addAttribute("in", "dst");
|
||||
+ feComposite.addAttribute("in2", "src");
|
||||
+ feComposite.addAttribute("operator", "in");
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ {
|
||||
+ AutoElement rect("rect", fWriter);
|
||||
+
|
||||
+ rect.addAttribute("x", "0");
|
||||
+ rect.addAttribute("y", "0");
|
||||
+ rect.addAttribute("width", "100%");
|
||||
+ rect.addAttribute("height", "100%");
|
||||
+ rect.addAttribute("filter", SkStringPrintf("url(#%s)", filterID.c_str()));
|
||||
+ }
|
||||
+ } else {
|
||||
+ AutoElement group("g", this, bucket(), MxCp(&matrix, &cs()), paint);
|
||||
+
|
||||
+ fWriter->writeDOM(layer->fDom, layer->fNode, skip_root);
|
||||
+ }
|
||||
+
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/src/svg/SkSVGDevice.h b/src/svg/SkSVGDevice.h
|
||||
index 8705ad5066..470962c068 100644
|
||||
--- a/src/svg/SkSVGDevice.h
|
||||
+++ b/src/svg/SkSVGDevice.h
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "include/private/SkTArray.h"
|
||||
#include "include/utils/SkParsePath.h"
|
||||
#include "src/core/SkClipStackDevice.h"
|
||||
+#include "src/core/SkDraw.h"
|
||||
+#include "src/xml/SkXMLParser.h"
|
||||
+#include "src/xml/SkXMLWriter.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -34,6 +37,7 @@ class SkPath;
|
||||
class SkRRect;
|
||||
class SkVertices;
|
||||
class SkXMLWriter;
|
||||
+class SkSVGDevice;
|
||||
struct SkISize;
|
||||
struct SkPoint;
|
||||
struct SkRect;
|
||||
@@ -70,15 +74,20 @@ protected:
|
||||
#ifdef SK_ENABLE_SKSL
|
||||
void drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) override;
|
||||
#endif
|
||||
+
|
||||
+ SkBaseDevice* onCreateDevice(const CreateInfo&, const SkPaint*) override;
|
||||
+ virtual void drawDevice(SkBaseDevice*, const SkSamplingOptions&, const SkPaint&) override;
|
||||
private:
|
||||
SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter>, uint32_t);
|
||||
+ SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter>, uint32_t, SkSVGDevice*);
|
||||
~SkSVGDevice() override;
|
||||
|
||||
struct MxCp;
|
||||
void drawBitmapCommon(const MxCp&, const SkBitmap& bm, const SkPaint& paint);
|
||||
|
||||
+ void closeWriter();
|
||||
+ void closeClipStack();
|
||||
void syncClipStack(const SkClipStack&);
|
||||
-
|
||||
SkParsePath::PathEncoding pathEncoding() const;
|
||||
|
||||
class AutoElement;
|
||||
@@ -88,13 +97,38 @@ private:
|
||||
const std::unique_ptr<ResourceBucket> fResourceBucket;
|
||||
const uint32_t fFlags;
|
||||
|
||||
- struct ClipRec {
|
||||
- std::unique_ptr<AutoElement> fClipPathElem;
|
||||
- uint32_t fGenID;
|
||||
+ SkSVGDevice* fRootDevice;
|
||||
+ std::unique_ptr<AutoElement> fRootElement;
|
||||
+ std::unique_ptr<AutoElement> fRootDefsElement;
|
||||
+ std::unique_ptr<AutoElement> fRootGroupElement;
|
||||
+ SkTArray<uint32_t> fClipStack;
|
||||
+ SkString fRootElementID;
|
||||
+ SkString fClipPathID;
|
||||
+ bool fWriteUseElement = true;
|
||||
+
|
||||
+ ResourceBucket* bucket() {
|
||||
+ return fRootDevice->fResourceBucket.get();
|
||||
+ }
|
||||
+
|
||||
+ class Layer {
|
||||
+ public:
|
||||
+ Layer(const SkISize& size, uint32_t flags, SkSVGDevice* root)
|
||||
+ : fDevice(
|
||||
+ new SkSVGDevice(
|
||||
+ size,
|
||||
+ std::make_unique<SkXMLParserWriter>(fDom.beginParsing()),
|
||||
+ flags,
|
||||
+ root
|
||||
+ )
|
||||
+ )
|
||||
+ {}
|
||||
+
|
||||
+ SkDOM fDom;
|
||||
+ SkSVGDevice* fDevice;
|
||||
+ const SkDOMNode* fNode = nullptr;
|
||||
};
|
||||
|
||||
- std::unique_ptr<AutoElement> fRootElement;
|
||||
- SkTArray<ClipRec> fClipStack;
|
||||
+ std::vector<std::unique_ptr<Layer>> fLayers;
|
||||
|
||||
using INHERITED = SkClipStackDevice;
|
||||
};
|
||||
void SkBitmapDevice::drawVertices(const SkVertices* vertices,
|
||||
|
2
src/terminal.rs
Normal file
2
src/terminal.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod input;
|
||||
pub mod output;
|
10
src/terminal/input.rs
Normal file
10
src/terminal/input.rs
Normal file
@ -0,0 +1,10 @@
|
||||
mod event;
|
||||
mod listen;
|
||||
mod mouse;
|
||||
mod parser;
|
||||
mod raw_tty;
|
||||
|
||||
pub use event::*;
|
||||
pub use listen::*;
|
||||
pub use mouse::*;
|
||||
pub use parser::*;
|
59
src/terminal/input/event.rs
Normal file
59
src/terminal/input/event.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use std::ops::BitAnd;
|
||||
|
||||
use super::Mouse;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
KeyPress { key: u8 },
|
||||
MouseUp { row: usize, col: usize },
|
||||
MouseDown { row: usize, col: usize },
|
||||
MouseMove { row: usize, col: usize },
|
||||
Scroll { delta: isize },
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn from(mouse: &mut Mouse, release: bool) -> Option<Event> {
|
||||
if !mouse.read() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match (mouse.btn, mouse.col, mouse.row) {
|
||||
(Some(btn), Some(col), Some(row)) => Some({
|
||||
if Mask::ScrollDown & btn {
|
||||
Event::Scroll { delta: -1 }
|
||||
} else if Mask::ScrollUp & btn {
|
||||
Event::Scroll { delta: 1 }
|
||||
} else {
|
||||
let col = col as usize - 1;
|
||||
let row = row as usize - 1;
|
||||
|
||||
if release {
|
||||
Event::MouseUp { row, col }
|
||||
} else if Mask::MouseMove & btn {
|
||||
Event::MouseMove { row, col }
|
||||
} else {
|
||||
Event::MouseDown { row, col }
|
||||
}
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Mask {
|
||||
MouseMove = 0x20,
|
||||
ScrollUp = 0x40,
|
||||
ScrollDown = 0x41,
|
||||
}
|
||||
|
||||
impl BitAnd<u32> for Mask {
|
||||
type Output = bool;
|
||||
|
||||
fn bitand(self, rhs: u32) -> bool {
|
||||
let mask = self as u32;
|
||||
|
||||
mask & rhs == mask
|
||||
}
|
||||
}
|
40
src/terminal/input/listen.rs
Normal file
40
src/terminal/input/listen.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::io::{self, Read};
|
||||
|
||||
use crate::terminal::input::*;
|
||||
|
||||
pub fn setup() -> io::Result<()> {
|
||||
raw_tty::setup()
|
||||
}
|
||||
|
||||
/// Listen for input events in stdin.
|
||||
/// This will block, so it should run from a dedicated thread.
|
||||
pub fn listen<T, F>(mut callback: F) -> io::Result<T>
|
||||
where
|
||||
F: FnMut(Event) -> Option<T>,
|
||||
{
|
||||
let mut buf = [0u8; 1024];
|
||||
let mut stdin = io::stdin();
|
||||
let mut parser = Parser::new();
|
||||
|
||||
loop {
|
||||
// Wait for some input
|
||||
let size = stdin.read(&mut buf)?;
|
||||
let mut scroll = 0;
|
||||
|
||||
// Parse the input for xterm commands
|
||||
for event in parser.parse(&buf[0..size]) {
|
||||
// Allow the callback to return early (ie. handle ctrl+c)
|
||||
if let Event::Scroll { delta } = event {
|
||||
scroll += delta;
|
||||
} else if let Some(result) = callback(event) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
if scroll != 0 {
|
||||
if let Some(result) = callback(Event::Scroll { delta: scroll }) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
src/terminal/input/mouse.rs
Normal file
65
src/terminal/input/mouse.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use super::Event;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mouse {
|
||||
pub buf: Option<Vec<u8>>,
|
||||
pub btn: Option<u32>,
|
||||
pub col: Option<u32>,
|
||||
pub row: Option<u32>,
|
||||
}
|
||||
|
||||
impl Mouse {
|
||||
pub fn start(&mut self) {
|
||||
self.buf = Some(Vec::new())
|
||||
}
|
||||
|
||||
pub fn end(&mut self, key: u8, events: &mut Vec<Event>) {
|
||||
if let Some(event) = Event::from(self, key == 0x6d) {
|
||||
events.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.buf = None;
|
||||
self.btn = None;
|
||||
self.col = None;
|
||||
self.row = None;
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> bool {
|
||||
if self.parse() == None {
|
||||
self.reset();
|
||||
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(&mut self) -> Option<()> {
|
||||
if let Some(ref buf) = self.buf {
|
||||
let string = std::str::from_utf8(buf).ok()?;
|
||||
let data = string.parse().ok()?;
|
||||
|
||||
self.update(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, data: u32) -> Option<()> {
|
||||
if self.btn == None {
|
||||
self.btn = Some(data)
|
||||
} else if self.col == None {
|
||||
self.col = Some(data)
|
||||
} else if self.row == None {
|
||||
self.row = Some(data)
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.buf = Some(Vec::new());
|
||||
|
||||
return Some(());
|
||||
}
|
||||
}
|
93
src/terminal/input/parser.rs
Normal file
93
src/terminal/input/parser.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use crate::terminal::input::*;
|
||||
|
||||
pub struct Parser {
|
||||
esc: bool,
|
||||
csi: bool,
|
||||
mouse: Mouse,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Parser {
|
||||
Parser {
|
||||
esc: false,
|
||||
csi: false,
|
||||
mouse: Mouse {
|
||||
buf: None,
|
||||
btn: None,
|
||||
col: None,
|
||||
row: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, input: &[u8]) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
let Parser {
|
||||
mut esc,
|
||||
mut csi,
|
||||
ref mut mouse,
|
||||
} = self;
|
||||
|
||||
for &key in input {
|
||||
if esc {
|
||||
// Inside an escape sequence
|
||||
if let Some(ref mut buf) = mouse.buf {
|
||||
// Process mouse input sequence
|
||||
match key {
|
||||
// Delimiter: concat characters, parse number, and clear buffer
|
||||
0x3b => esc = mouse.read(),
|
||||
// Terminator: emit mouse move, movement, or release based on terminator
|
||||
0x4d | 0x6d => {
|
||||
mouse.end(key, &mut events);
|
||||
|
||||
esc = false
|
||||
}
|
||||
// Consider anything else part of the value
|
||||
_ => buf.push(key),
|
||||
}
|
||||
} else if csi {
|
||||
// Inside a control sequence
|
||||
match key {
|
||||
// Mouse input
|
||||
0x3c => mouse.start(),
|
||||
// Map arrow keys events to key codes
|
||||
0x41..=0x44 => events.push(Event::KeyPress {
|
||||
key: [0x26, 0x28, 0x27, 0x25][(key - 0x41) as usize],
|
||||
}),
|
||||
// Ignore anything else
|
||||
_ => esc = false,
|
||||
}
|
||||
} else if key == 0x5b {
|
||||
// [ character, start a CSI sequence
|
||||
csi = true
|
||||
} else {
|
||||
// Unrecognized sequence, emit an ESC keypress
|
||||
events.push(Event::KeyPress { key: 0x1b });
|
||||
|
||||
if key != 0x1b {
|
||||
// Cancel the sequence only if this isn't an ESC character
|
||||
esc = false;
|
||||
|
||||
events.push(Event::KeyPress { key });
|
||||
}
|
||||
}
|
||||
} else if key == 0x1b {
|
||||
// ESC character, start an escape sequence
|
||||
esc = true;
|
||||
csi = false;
|
||||
mouse.reset();
|
||||
} else if key == 0x03 {
|
||||
// CTRL-C pressed
|
||||
events.push(Event::Exit)
|
||||
} else {
|
||||
// Any other character should be parser as text input
|
||||
events.push(Event::KeyPress { key })
|
||||
}
|
||||
}
|
||||
|
||||
self.esc = esc;
|
||||
self.csi = csi;
|
||||
|
||||
events
|
||||
}
|
||||
}
|
40
src/terminal/input/raw_tty.rs
Normal file
40
src/terminal/input/raw_tty.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
use std::{fs, io};
|
||||
|
||||
/// Setup the input stream to operate in raw mode.
|
||||
/// Allows for reading characters without waiting for the return key to be pressed.
|
||||
pub fn setup() -> io::Result<()> {
|
||||
unsafe {
|
||||
let tty;
|
||||
|
||||
let fd = if libc::isatty(libc::STDIN_FILENO) == 1 {
|
||||
libc::STDIN_FILENO
|
||||
} else {
|
||||
// Use /dev/tty in the input stream is not a terminal.
|
||||
// Happens if something is piped to stdin.
|
||||
tty = fs::File::open("/dev/tty")?;
|
||||
|
||||
tty.as_raw_fd()
|
||||
};
|
||||
|
||||
let mut ptr = core::mem::MaybeUninit::uninit();
|
||||
|
||||
// Load the terminal parameters
|
||||
if libc::tcgetattr(fd, ptr.as_mut_ptr()) == 0 {
|
||||
let mut termios = ptr.assume_init();
|
||||
let c_oflag = termios.c_oflag;
|
||||
|
||||
// Set the terminal to raw mode
|
||||
libc::cfmakeraw(&mut termios);
|
||||
// Restore output flags, ensures carriage returns are consistent
|
||||
termios.c_oflag = c_oflag;
|
||||
|
||||
// Save the terminal parameters
|
||||
if libc::tcsetattr(fd, libc::TCSANOW, &termios) == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
12
src/terminal/output.rs
Normal file
12
src/terminal/output.rs
Normal file
@ -0,0 +1,12 @@
|
||||
// mod kd_tree;
|
||||
// mod quantizer;
|
||||
mod cell;
|
||||
mod painter;
|
||||
mod renderer;
|
||||
mod size;
|
||||
mod xterm;
|
||||
|
||||
pub use cell::*;
|
||||
pub use painter::*;
|
||||
pub use renderer::*;
|
||||
pub use size::*;
|
35
src/terminal/output/cell.rs
Normal file
35
src/terminal/output/cell.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::gfx::{Color, Point};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Grapheme {
|
||||
/// Unicode character in UTF-8, might contain multiple code points (Emoji, CJK).
|
||||
pub char: String,
|
||||
pub index: usize,
|
||||
pub width: usize,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
/// Terminal cell with `height = width * 2`
|
||||
#[derive(PartialEq)]
|
||||
pub struct Cell {
|
||||
/// Top pixel color value
|
||||
pub top: Color,
|
||||
/// Bottom pixel color value
|
||||
pub bottom: Color,
|
||||
pub cursor: Point<u32>,
|
||||
/// Text grapheme if any
|
||||
pub grapheme: Option<Rc<Grapheme>>,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn new(x: u32, y: u32) -> Cell {
|
||||
Cell {
|
||||
top: Color::black(),
|
||||
bottom: Color::black(),
|
||||
cursor: Point::new(x, y),
|
||||
grapheme: None,
|
||||
}
|
||||
}
|
||||
}
|
65
src/terminal/output/kd_tree.rs
Normal file
65
src/terminal/output/kd_tree.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::ops::Mul;
|
||||
|
||||
use crate::gfx::Color;
|
||||
|
||||
struct KDNode {
|
||||
left: Option<Box<KDNode>>,
|
||||
right: Option<Box<KDNode>>,
|
||||
normal: Color<f64>,
|
||||
middle: (usize, Color<f64>),
|
||||
}
|
||||
|
||||
impl KDNode {
|
||||
fn new(colors: &[Color]) {
|
||||
let (sum, sum_squared) = colors.iter().fold(
|
||||
(Color::black(), Color::black()),
|
||||
|(sum, sum_squared), color| (sum + color, sum_squared + color * color),
|
||||
);
|
||||
}
|
||||
|
||||
fn nearest(&self, color: Color<f64>, mut limit: f64) -> Option<(usize, f64)> {
|
||||
let diff = color - self.middle.1;
|
||||
let distance = diff.mul(&diff).sum().sqrt();
|
||||
let mut result = None;
|
||||
|
||||
if distance < limit {
|
||||
limit = distance;
|
||||
}
|
||||
|
||||
let dot = diff.mul(self.normal).sum();
|
||||
|
||||
if dot <= 0.0 {
|
||||
if let Some(ref left) = self.left {
|
||||
if let Some(nearest) = left.nearest(color, limit) {
|
||||
limit = nearest.1;
|
||||
result = Some(nearest);
|
||||
}
|
||||
}
|
||||
|
||||
if -dot < limit {
|
||||
if let Some(ref right) = self.right {
|
||||
if let Some(nearest) = right.nearest(color, limit) {
|
||||
result = Some(nearest);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(ref right) = self.right {
|
||||
if let Some(nearest) = right.nearest(color, limit) {
|
||||
limit = nearest.1;
|
||||
result = Some(nearest);
|
||||
}
|
||||
}
|
||||
|
||||
if dot < limit {
|
||||
if let Some(ref left) = self.left {
|
||||
if let Some(nearest) = left.nearest(color, limit) {
|
||||
result = Some(nearest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
154
src/terminal/output/painter.rs
Normal file
154
src/terminal/output/painter.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use std::io::{self, Stdout, Write};
|
||||
|
||||
use crate::gfx::{Color, Point};
|
||||
|
||||
use super::Cell;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum PaintMode {
|
||||
Text,
|
||||
Bitmap,
|
||||
}
|
||||
|
||||
pub struct Painter {
|
||||
mode: PaintMode,
|
||||
output: Stdout,
|
||||
buffer: Vec<u8>,
|
||||
cursor: Option<Point<u32>>,
|
||||
true_color: bool,
|
||||
background: Option<Color>,
|
||||
foreground: Option<Color>,
|
||||
background_code: Option<u8>,
|
||||
foreground_code: Option<u8>,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
pub fn new() -> Painter {
|
||||
Painter {
|
||||
mode: PaintMode::Text,
|
||||
buffer: Vec::new(),
|
||||
cursor: None,
|
||||
output: io::stdout(),
|
||||
true_color: if let Ok(value) = std::env::var("COLORTERM") {
|
||||
match value.as_str() {
|
||||
"truecolor" | "24bit" => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
},
|
||||
background: None,
|
||||
foreground: None,
|
||||
background_code: None,
|
||||
foreground_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn true_color(&self) -> bool {
|
||||
self.true_color
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
self.output.write(self.buffer.as_slice())?;
|
||||
self.output.flush()?;
|
||||
self.buffer.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, cell: &Cell) -> io::Result<()> {
|
||||
let &Cell {
|
||||
cursor,
|
||||
ref grapheme,
|
||||
top: mut background,
|
||||
bottom: mut foreground,
|
||||
} = cell;
|
||||
|
||||
let (char, width, escape) = if let Some(grapheme) = grapheme {
|
||||
if grapheme.index > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
background = background.avg_with(foreground);
|
||||
foreground = grapheme.color;
|
||||
|
||||
(
|
||||
grapheme.char.as_str(),
|
||||
grapheme.width as u32,
|
||||
if self.mode == PaintMode::Bitmap {
|
||||
self.mode = PaintMode::Text;
|
||||
|
||||
Some("\x1b[22m\x1b[24m")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"▄",
|
||||
1,
|
||||
if self.mode == PaintMode::Text {
|
||||
self.mode = PaintMode::Bitmap;
|
||||
|
||||
Some("\x1b[1m\x1b[4m")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
if self.cursor != Some(cursor) {
|
||||
write!(self.buffer, "\x1b[{};{}H", cursor.y + 1, cursor.x + 1)?;
|
||||
};
|
||||
|
||||
self.cursor = Some(cursor + Point::new(width, 0));
|
||||
|
||||
if self.background != Some(background) {
|
||||
self.background = Some(background);
|
||||
|
||||
if self.true_color {
|
||||
write!(
|
||||
self.buffer,
|
||||
"\x1b[48;2;{};{};{}m",
|
||||
background.r, background.g, background.b,
|
||||
)?
|
||||
} else {
|
||||
let code = background.to_xterm();
|
||||
|
||||
if self.background_code != Some(code) {
|
||||
self.background_code = Some(code);
|
||||
|
||||
write!(self.buffer, "\x1b[48;5;{}m", code)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.foreground != Some(foreground) {
|
||||
self.foreground = Some(foreground);
|
||||
|
||||
if self.true_color {
|
||||
write!(
|
||||
self.buffer,
|
||||
"\x1b[38;2;{};{};{}m",
|
||||
foreground.r, foreground.g, foreground.b,
|
||||
)?
|
||||
} else {
|
||||
let code = foreground.to_xterm();
|
||||
|
||||
if self.foreground_code != Some(code) {
|
||||
self.foreground_code = Some(code);
|
||||
|
||||
write!(self.buffer, "\x1b[38;5;{}m", code)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(escape) = escape {
|
||||
self.buffer.write_all(escape.as_bytes())?
|
||||
}
|
||||
|
||||
self.buffer.write_all(char.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
111
src/terminal/output/quantizer.rs
Normal file
111
src/terminal/output/quantizer.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use crate::gfx::Color;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Channel {
|
||||
R,
|
||||
G,
|
||||
B,
|
||||
}
|
||||
|
||||
const COLOR_BUCKETS: usize = 8;
|
||||
const COLORS: usize = 2_usize.pow(COLOR_BUCKETS as u32);
|
||||
|
||||
/// Find the closest color to `color` on `palette` using a binary search
|
||||
pub fn palette_color(palette: &[Color; COLORS], color: Color) {
|
||||
let mut size = palette.len() / 2;
|
||||
let mut iter = palette.iter();
|
||||
let mut prev = iter.next();
|
||||
}
|
||||
|
||||
fn distance(a: Color, b: Color) {}
|
||||
|
||||
pub fn quantize(pixels: &[u8]) -> [Color; COLORS] {
|
||||
let mut min = Color::black();
|
||||
let mut max = Color::black();
|
||||
let mut bucket = Vec::<Color>::new();
|
||||
let mut pixels_iter = pixels.iter();
|
||||
let mut bucket_iter = bucket.iter_mut();
|
||||
|
||||
// Step 1: find the dominant channel
|
||||
loop {
|
||||
match (
|
||||
bucket_iter.next(),
|
||||
pixels_iter.next(),
|
||||
pixels_iter.next(),
|
||||
pixels_iter.next(),
|
||||
pixels_iter.next(),
|
||||
) {
|
||||
(Some(color), Some(r), Some(g), Some(b), Some(_)) => {
|
||||
// Save the color in a bucket
|
||||
color.set_r(*r);
|
||||
color.set_g(*g);
|
||||
color.set_b(*b);
|
||||
|
||||
min.set_r(min.r().min(color.r()));
|
||||
min.set_g(min.g().min(color.g()));
|
||||
min.set_b(min.b().min(color.b()));
|
||||
|
||||
max.set_r(max.r().max(color.r()));
|
||||
max.set_g(max.g().max(color.g()));
|
||||
max.set_b(max.b().max(color.b()));
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
let ranges = [
|
||||
(Channel::R, max.r() - min.r()),
|
||||
(Channel::G, max.g() - min.g()),
|
||||
(Channel::B, max.b() - min.b()),
|
||||
];
|
||||
let (channel, _) = ranges
|
||||
.iter()
|
||||
.reduce(|a, b| if a.1 > b.1 { a } else { b })
|
||||
.unwrap();
|
||||
|
||||
// Step 2: perform median-cut
|
||||
for i in 1..=COLOR_BUCKETS {
|
||||
let buckets = 2_usize.pow(i as u32);
|
||||
let size = bucket.len() / buckets;
|
||||
|
||||
for j in 0..buckets {
|
||||
let start = j * size;
|
||||
let end = start + size;
|
||||
let slice = &mut bucket[start..end];
|
||||
|
||||
slice.sort_unstable_by(match channel {
|
||||
Channel::R => |a: &Color, b: &Color| a.r().cmp(&b.r()),
|
||||
Channel::G => |a: &Color, b: &Color| a.g().cmp(&b.g()),
|
||||
Channel::B => |a: &Color, b: &Color| a.b().cmp(&b.b()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: get the average color in each bucket
|
||||
let mut palette = [Color::black(); COLORS];
|
||||
let size = bucket.len() / palette.len();
|
||||
|
||||
for (i, color) in palette.iter_mut().enumerate() {
|
||||
let start = i * size;
|
||||
let end = start + size;
|
||||
let slice = &bucket[start..end];
|
||||
let mut sum = None;
|
||||
|
||||
for color in slice.into_iter() {
|
||||
sum = Some(match sum {
|
||||
None => color.cast(),
|
||||
Some(sum) => color.cast() + sum,
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(sum) = sum {
|
||||
let avg = sum / size as u32;
|
||||
|
||||
color.set_r(avg.r() as u8);
|
||||
color.set_g(avg.g() as u8);
|
||||
color.set_b(avg.b() as u8);
|
||||
}
|
||||
}
|
||||
|
||||
palette
|
||||
}
|
231
src/terminal/output/renderer.rs
Normal file
231
src/terminal/output/renderer.rs
Normal file
@ -0,0 +1,231 @@
|
||||
use std::{
|
||||
io::{self, Write as _},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
gfx::{Color, Point, Rect, Size},
|
||||
terminal,
|
||||
};
|
||||
|
||||
use super::{Cell, Grapheme, Painter};
|
||||
|
||||
struct Dimensions {
|
||||
/// Size of a terminal cell in pixels
|
||||
cell: Size,
|
||||
/// Size of the browser window in pixels
|
||||
browser: Size,
|
||||
/// Size of the terminal window in cells
|
||||
terminal: Size,
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
cells: Vec<(Cell, Cell)>,
|
||||
dimensions: Dimensions,
|
||||
painter: Painter,
|
||||
}
|
||||
|
||||
const SEQUENCES: [(u32, bool); 4] = [(1049, true), (1003, true), (1006, true), (25, false)];
|
||||
|
||||
impl Renderer {
|
||||
pub fn new() -> Renderer {
|
||||
Renderer {
|
||||
cells: Vec::with_capacity(0),
|
||||
painter: Painter::new(),
|
||||
dimensions: Dimensions {
|
||||
cell: Size::new(7, 14),
|
||||
browser: Size::new(0, 0),
|
||||
terminal: Size::new(0, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup() -> io::Result<()> {
|
||||
terminal::input::setup()?;
|
||||
|
||||
let mut out = io::stdout();
|
||||
|
||||
for (sequence, enable) in SEQUENCES {
|
||||
write!(out, "\x1b[?{}{}", sequence, if enable { "h" } else { "l" })?;
|
||||
}
|
||||
|
||||
out.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown() -> io::Result<()> {
|
||||
let mut out = io::stdout();
|
||||
|
||||
for (sequence, enable) in SEQUENCES {
|
||||
write!(out, "\x1b[?{}{}", sequence, if enable { "l" } else { "h" })?;
|
||||
}
|
||||
|
||||
out.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, cell: Size, terminal: Size) {
|
||||
let size = (terminal.width * terminal.height) as usize;
|
||||
|
||||
self.dimensions.cell = cell;
|
||||
self.dimensions.terminal = terminal;
|
||||
self.dimensions.browser = cell * terminal;
|
||||
|
||||
let mut x = 0;
|
||||
let mut y = 0;
|
||||
let bound = terminal.width - 1;
|
||||
|
||||
self.cells.resize_with(size, || {
|
||||
let cell = (Cell::new(x, y), Cell::new(x, y));
|
||||
|
||||
if x < bound {
|
||||
x += 1;
|
||||
} else {
|
||||
x = 0;
|
||||
y += 1;
|
||||
}
|
||||
|
||||
cell
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> io::Result<()> {
|
||||
for (previous, current) in self.cells.iter_mut() {
|
||||
if current == previous {
|
||||
continue;
|
||||
}
|
||||
|
||||
previous.top = current.top;
|
||||
previous.bottom = current.bottom;
|
||||
previous.grapheme = current.grapheme.clone();
|
||||
|
||||
self.painter.paint(current)?;
|
||||
}
|
||||
|
||||
self.painter.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draw the background from a pixel array encoded in RGBA8888
|
||||
pub fn draw_background(&mut self, pixels: &mut [u8], rect: Rect) -> io::Result<()> {
|
||||
let viewport = self.dimensions.terminal.cast::<usize>();
|
||||
let pixels_row = viewport.width * 4;
|
||||
|
||||
if pixels.len() != pixels_row * viewport.height * 2 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pos = rect.origin.cast::<usize>() / Point::new(1, 2);
|
||||
let size = rect.size.cast::<usize>() / Size::new(1, 2);
|
||||
let pixels_left = pos.x * 4;
|
||||
let pixels_width = size.width * 4;
|
||||
|
||||
// Iterate over each row
|
||||
for y in pos.y..pos.y + size.height {
|
||||
// Terminal chars have an aspect ratio of 2:1.
|
||||
// In order to display perfectly squared pixels, we
|
||||
// render a unicode glyph taking the bottom half of the cell
|
||||
// using a foreground representing the bottom pixel,
|
||||
// and a background representing the top pixel.
|
||||
// This means that the pixel input buffer should be twice the size
|
||||
// of the terminal cell buffer (two pixels take one terminal cell).
|
||||
let left = pixels_left + y * 2 * pixels_row;
|
||||
let right = left + pixels_width;
|
||||
// Get a slice pointing to the top pixel row
|
||||
let mut top_row = pixels[left..right].iter();
|
||||
// Get a slice pointing to the bottom pixel row
|
||||
let mut bottom_row = pixels[left + pixels_row..right + pixels_row].iter();
|
||||
let cells_left = y * viewport.width + pos.x;
|
||||
let cells = self.cells[cells_left..].iter_mut();
|
||||
|
||||
// Iterate over each column
|
||||
for (_, cell) in cells {
|
||||
match (
|
||||
Color::from_iter(&mut top_row),
|
||||
Color::from_iter(&mut bottom_row),
|
||||
) {
|
||||
(Some(top), Some(bottom)) => {
|
||||
cell.top = top;
|
||||
cell.bottom = bottom;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.render()
|
||||
}
|
||||
|
||||
pub fn clear_text(&mut self) {
|
||||
for (_, cell) in self.cells.iter_mut() {
|
||||
cell.grapheme = None
|
||||
}
|
||||
}
|
||||
|
||||
/// Render some text into the terminal output
|
||||
pub fn draw_text(&mut self, string: &str, origin: Point, size: Size, color: Color) {
|
||||
// Get an iterator starting at the text origin
|
||||
let len = self.cells.len();
|
||||
let viewport = &self.dimensions.terminal;
|
||||
|
||||
if size.width > 2 && size.height > 2 {
|
||||
let (scaled_origin, scaled_size) = (origin + 0, size - 0);
|
||||
let x = scaled_origin.x.max(0).min(viewport.width as i32);
|
||||
let y = scaled_origin.y.max(0).min(viewport.height as i32 * 2);
|
||||
let y_end = y + scaled_size.height as i32;
|
||||
|
||||
for y in y..y_end {
|
||||
let index = x + y / 2 * (viewport.width as i32);
|
||||
let start = len.min(index as usize);
|
||||
let end = len.min(start + scaled_size.width as usize);
|
||||
|
||||
for (_, cell) in self.cells[start..end].iter_mut() {
|
||||
cell.grapheme = None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Compute the buffer index based on the position
|
||||
let index = origin.x + origin.y / 2 * (self.dimensions.terminal.width as i32);
|
||||
let mut iter = self.cells[len.min(index as usize)..].iter_mut();
|
||||
|
||||
// Get every Unicode grapheme in the input string
|
||||
for grapheme in UnicodeSegmentation::graphemes(string, true) {
|
||||
let width = grapheme.width();
|
||||
|
||||
for index in 0..width {
|
||||
// Get the next terminal cell at the given position
|
||||
match iter.next() {
|
||||
// Stop if we're at the end of the buffer
|
||||
None => return,
|
||||
// Set the cell to the current grapheme
|
||||
Some((_, cell)) => {
|
||||
let next = Grapheme {
|
||||
// Create a new shared reference to the text
|
||||
color,
|
||||
index,
|
||||
width,
|
||||
// Export the set of unicode code points for this graphene into an UTF-8 string
|
||||
char: grapheme.to_string(),
|
||||
};
|
||||
|
||||
if match cell.grapheme {
|
||||
None => true,
|
||||
Some(ref previous) => {
|
||||
previous.color != next.color || previous.char != next.char
|
||||
}
|
||||
} {
|
||||
cell.grapheme = Some(Rc::new(next))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/terminal/output/size.rs
Normal file
18
src/terminal/output/size.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use core::mem::MaybeUninit;
|
||||
use std::io;
|
||||
|
||||
use crate::gfx::Size;
|
||||
|
||||
pub fn size() -> io::Result<Size> {
|
||||
let mut ptr = MaybeUninit::<libc::winsize>::uninit();
|
||||
|
||||
unsafe {
|
||||
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ptr.as_mut_ptr()) == 0 {
|
||||
let size = ptr.assume_init();
|
||||
|
||||
Ok(Size::new(size.ws_col as u32, size.ws_row as u32))
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
17
src/terminal/output/xterm.rs
Normal file
17
src/terminal/output/xterm.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use crate::gfx::Color;
|
||||
|
||||
impl Color {
|
||||
pub fn to_xterm(&self) -> u8 {
|
||||
if self.r == self.g && self.g == self.b && self.r > 4 && self.r < 239 {
|
||||
232 + (self.r - 8) / 10
|
||||
} else {
|
||||
(16.0
|
||||
+ self
|
||||
.cast::<f32>()
|
||||
.mul_add(5.0 / 200.0, -(55.0 * (5.0 / 200.0)))
|
||||
.max(0.0)
|
||||
.round()
|
||||
.dot((36.0, 6.0, 1.0))) as u8
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user