diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f159c7..eb099e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ FetchContent_MakeAvailable(hjson) FetchContent_Declare( litehtml GIT_REPOSITORY https://github.com/litehtml/litehtml.git - GIT_TAG d4453f5d4e03cd4d902b867ca553d0ad81b09939 # I had some problems with the latest version, so I used this one + GIT_TAG 8836bc1bc35ca0cfd71dc0386ef841d5cbc3bd5e ) set(LITEHTML_BUILD_TESTING OFF CACHE BOOL "Skip building tests" FORCE) FetchContent_MakeAvailable(litehtml) diff --git a/README.md b/README.md index 33f08ae..e2a736f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The goal is to provide a simple way to build native desktop applications with si ## Features - Cross-platform (Windows, macOS, Linux) -- Language agnostic (communicates via stdin/stdout) +- Language agnostic (communicates via stdin/stdout, Unix sockets, or named pipes) - Supports a reduced sub-set of HTML/CSS for UI layouting and styling - Pane-based layout system - Interactive widgets: @@ -58,6 +58,7 @@ The goal is to provide a simple way to build native desktop applications with si - [Todo List](./example/todo): Simple todo list app - [Chat](./example/chat): Simple chat interface with message input and display (no real networking, just simulates a conversation) - [IRC Client](./example/irc): A working, but simple IRC client +- [IPC Transports](./example/ipc): Demonstrates connecting via Unix domain socket or named pipe instead of stdin/stdout All these examples are based on the Go SDK. @@ -67,12 +68,13 @@ This uses litehtml for html/css layouting. This means that only a reduced sub-se ## Usage -stdui is a **compiled C++ binary** that opens a native OS window and renders HTML with interactive widgets. Your application controls it by spawning it as a child process and exchanging **newline-delimited JSON** over stdin/stdout. Any language that can start a process and read/write pipes can drive it. +stdui is a **compiled C++ binary** that opens a native OS window and renders HTML with interactive widgets. Your application controls it by spawning it as a child process and exchanging **newline-delimited JSON**. By default communication uses stdin/stdout; Unix domain sockets and named pipes are also supported. Any language that can start a process and read/write pipes (or connect to a socket) can drive it. ``` Your App (Go, Python, anything) - │ stdin → JSON commands - │ stdout ← JSON events + │ JSON commands → + │ ← JSON events + │ (stdin/stdout, Unix socket, or named pipe) ▼ stdui binary (C++) ┌-----------------------------─┐ @@ -166,6 +168,22 @@ stdui → You {"action":"window-closed"} That's the entire model. Every interaction follows this same pattern: send action, receive events, send new action. +### Alternative Transports + +By default stdui reads/writes via stdin/stdout. You can also connect over a **Unix domain socket** or a **named pipe** (Windows named pipe on Windows, Unix domain socket alias on Unix/macOS) by passing a CLI flag when spawning the binary: + +```sh +# Unix / macOS +./stdui --socket /tmp/myapp.sock # Unix domain socket +./stdui --pipe /tmp/myapp.pipe # named pipe (alias for Unix domain socket) + +# Windows +stdui.exe --socket /tmp/myapp.sock # Unix domain socket (Windows 10 1803+) +stdui.exe --pipe \\.\pipe\myapp # Windows named pipe +``` + +stdui creates the socket/pipe file itself. You do not need to create it beforehand. The Go SDK exposes `StartWithSocket` and `StartWithNamedPipe` as drop-in replacements for `Start`. See the [IPC example](./example/ipc) and the [IPC Transports docs](https://bigjk.github.io/StdUI/docs/ipc-transports) for details. + **You can learn more about the protocol in the [protocol specification](./PROTOCOL.md).** ## SDKs diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 0a0dba5..fca0d59 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -4,7 +4,7 @@ sidebar_position: 1 # Introduction -StdUI is a lightweight cross-platform UI engine that can be used with any programming language. It spawns as a child process and communicates with your application via stdin/stdout using newline-delimited JSON. This allows easy integration with any language that can start a process and read/write pipes. +StdUI is a lightweight cross-platform UI engine that can be used with any programming language. It spawns as a child process and communicates with your application via newline-delimited JSON. By default it uses stdin/stdout, but Unix domain sockets and named pipes are also supported for cases where subprocess pipes are inconvenient. :::warning This project is experimental. Expect bugs, missing features, and breaking changes. The API is not stable and may change without deprecation. @@ -14,8 +14,9 @@ This project is experimental. Expect bugs, missing features, and breaking change ``` Your App (Go, Python, anything) - │ stdin → JSON commands - │ stdout ← JSON events + │ JSON commands → + │ ← JSON events + │ (stdin/stdout, Unix socket, or named pipe) ▼ stdui binary (C++) ┌──────────────────────────────┐ @@ -31,7 +32,7 @@ StdUI uses a reduced subset of HTML/CSS (via [litehtml](https://github.com/liteh ## Features - Cross-platform (Windows, macOS, Linux) -- Language agnostic (communicates via stdin/stdout) +- Language agnostic (communicates via stdin/stdout, Unix sockets, or named pipes) - Supports a reduced subset of HTML/CSS for layout and styling - Pane-based layout system - Interactive widgets: buttons, text/number/password inputs, checkboxes, sliders, progress bars, color picker diff --git a/docs/docs/ipc-transports.md b/docs/docs/ipc-transports.md new file mode 100644 index 0000000..511f2f2 --- /dev/null +++ b/docs/docs/ipc-transports.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 3 +sidebar_label: IPC Transports +--- + +# IPC Transports + +By default stdui communicates with the controlling application via **stdin/stdout**. Two alternative transports are available for cases where subprocess pipes are inconvenient — for example, when the controlling process is not the direct parent of stdui, or when you want to connect to an already-running stdui instance. + +| Transport | CLI flag | Platform | +| --------- | -------- | -------- | +| stdin/stdout | _(default, no flag needed)_ | All | +| Unix domain socket | `--socket ` | All (Windows 10 1803+) | +| Named pipe | `--pipe ` | All | + +:::note Platform note — named pipes on Unix +On Unix/macOS, `--pipe` is an alias for a Unix domain socket. FIFOs cannot carry reliable bidirectional IPC, so stdui creates a Unix domain socket at the given path regardless. On Windows, `--pipe` uses a real Windows named pipe (`CreateNamedPipe`). +::: + +## How it works + +When either flag is passed, stdui **creates** the socket or pipe itself before entering its main loop. The controlling application connects after the file appears on disk. The socket/pipe file is removed by stdui on shutdown. + +``` +App (Go, Python, anything) + │ JSON commands → + │ ← JSON events + ▼ (over socket or pipe instead of stdin/stdout) + stdui binary (C++) +``` + +The message format and protocol are identical to stdin/stdout — every message is a single line of JSON terminated by a newline. + +## CLI flags + +### `--socket ` + +Starts stdui in Unix domain socket mode. stdui creates and listens on a Unix domain socket at ``. The controlling application dials that path after the file is visible. + +```sh +./stdui --socket /tmp/myapp.sock +``` + +### `--pipe ` + +Starts stdui in named-pipe mode. + +- **Unix/macOS**: creates a Unix domain socket at `` (same as `--socket`). +- **Windows**: creates a Windows named pipe at `` (must be of the form `\\.\pipe\`). + +```sh +# Unix / macOS +./stdui --pipe /tmp/myapp.pipe + +# Windows +stdui.exe --pipe \\.\pipe\myapp +``` + +## Go SDK + +The Go SDK provides two convenience functions that spawn stdui with the appropriate flag and connect to it automatically. Both functions retry the connection with exponential back-off for up to 5 seconds while stdui is starting up. + +### `StartWithSocket` + +```go +import stdui "github.com/BigJk/stdui/sdk/go" + +client, err := stdui.StartWithSocket( + "./build/stdui", // path to stdui binary + "/tmp/myapp.sock", // socket path — created by stdui + stdui.Settings{ + Title: "My App", + WindowWidth: stdui.Ptr(800), + WindowHeight: stdui.Ptr(600), + }, +) +if err != nil { + log.Fatal(err) +} +``` + +### `StartWithNamedPipe` + +```go +client, err := stdui.StartWithNamedPipe( + "./build/stdui", // path to stdui binary + "/tmp/myapp.pipe", // pipe path — created by stdui + stdui.Settings{ + Title: "My App", + WindowWidth: stdui.Ptr(800), + WindowHeight: stdui.Ptr(600), + }, +) +if err != nil { + log.Fatal(err) +} +``` + +On Windows pass a named-pipe path: + +```go +client, err := stdui.StartWithNamedPipe( + `C:\path\to\stdui.exe`, + `\\.\pipe\myapp`, + settings, +) +``` + +After obtaining the `*Client`, usage is identical to `Start()` — register handlers and call `client.Wait()`. + +## Full example + +A runnable example is in [`example/ipc/main.go`](https://github.com/BigJk/stdui/blob/main/example/ipc/main.go). It accepts `-socket` and `-pipe` flags to select the transport at runtime: + +```sh +# Use Unix domain socket +go run ./example/ipc -socket /tmp/stdui-demo.sock + +# Use named pipe (default when no -socket flag is given) +go run ./example/ipc +``` diff --git a/docs/docs/protocol/index.md b/docs/docs/protocol/index.md index 88d4fcf..b814042 100644 --- a/docs/docs/protocol/index.md +++ b/docs/docs/protocol/index.md @@ -5,7 +5,9 @@ sidebar_label: Protocol # Protocol -StdUI communicates with the controlling application via **stdin/stdout** using line-delimited JSON. Every message is a single line of JSON terminated by a newline character. +StdUI communicates with the controlling application using line-delimited JSON. Every message is a single line of JSON terminated by a newline character. + +By default the channel is **stdin/stdout**, but Unix domain sockets and named pipes are also available — see [IPC Transports](/docs/ipc-transports). The message format is identical regardless of the transport. ## Message Format diff --git a/example/ipc/README.md b/example/ipc/README.md new file mode 100644 index 0000000..28c4427 --- /dev/null +++ b/example/ipc/README.md @@ -0,0 +1,31 @@ +# IPC Transports + +Demonstrates connecting to stdui via a **Unix domain socket** or a **named pipe** instead of the default stdin/stdout transport. + +## Usage + +```sh +# Named pipe (default — Unix domain socket on non-Windows, Windows named pipe on Windows) +go run . -binary ../../build/stdui + +# Unix domain socket +go run . -binary ../../build/stdui -socket /tmp/stdui-ipc-example.sock +``` + +## Flags + +| Flag | Default | Description | +| --------- | ----------------------------------------------------------------------------- | ------------------------------------------- | +| `-binary` | `./build/stdui` | Path to the stdui binary | +| `-socket` | `/tmp/stdui-ipc-example.sock` | Connect via Unix domain socket at this path | +| `-pipe` | `/tmp/stdui-ipc-example.pipe` (Unix) / `\\.\pipe\stdui-ipc-example` (Windows) | Connect via named pipe | + +When `-socket` is passed explicitly it takes priority. Otherwise `-pipe` is used. + +## Notes + +- stdui **creates** the socket/pipe itself — you do not need to create it beforehand. +- On Unix/macOS `--pipe` is backed by a Unix domain socket (FIFOs cannot carry bidirectional IPC reliably). +- On Windows `--pipe` uses a real Windows named pipe (`CreateNamedPipe`). + +See the [IPC Transports docs](https://bigjk.github.io/StdUI/docs/ipc-transports) for a full explanation. diff --git a/example/ipc/go.mod b/example/ipc/go.mod new file mode 100644 index 0000000..4553829 --- /dev/null +++ b/example/ipc/go.mod @@ -0,0 +1,7 @@ +module github.com/BigJk/stdui/example/ipc + +go 1.21 + +require github.com/BigJk/stdui/sdk/go v0.0.0 + +replace github.com/BigJk/stdui/sdk/go => ../../sdk/go diff --git a/example/ipc/main.go b/example/ipc/main.go new file mode 100644 index 0000000..391e3cf --- /dev/null +++ b/example/ipc/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + + stdui "github.com/BigJk/stdui/sdk/go" +) + +const contentTpl = ` + + +
IPC transport example
+
%s
+
+ +
+` + +func main() { + socket := flag.String("socket", defaultSocketPath(), "connect via Unix domain socket at this path") + pipe := flag.String("pipe", defaultPipePath(), "connect via named pipe (Unix domain socket on non-Windows, Windows named pipe on Windows)") + binary := flag.String("binary", "./build/stdui", "path to the stdui binary") + flag.Parse() + + settings := stdui.Settings{ + Title: "IPC Example", + WindowWidth: stdui.Ptr(500), + WindowHeight: stdui.Ptr(300), + Resizable: stdui.Ptr(false), + } + + var ( + client *stdui.Client + err error + transportID string + ) + + switch { + case isFlagSet("socket"): + transportID = "Unix domain socket: " + *socket + client, err = stdui.StartWithSocket(*binary, *socket, settings) + default: + transportID = "Named pipe: " + *pipe + client, err = stdui.StartWithNamedPipe(*binary, *pipe, settings) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to start stdui: %v\n", err) + os.Exit(1) + } + + client.OnReady(func() { + _ = client.UpdateContent(fmt.Sprintf(contentTpl, transportID)) + }) + + client.OnButtonClicked(func(attrs map[string]string, _ string) { + if attrs["action"] == "ping" { + fmt.Println("pong") + } + }) + + client.OnLog(func(namespace, message string) { + fmt.Fprintf(os.Stderr, "[log] %s: %s\n", namespace, message) + }) + + client.OnError(func(err error) { + fmt.Fprintf(os.Stderr, "[error] %v\n", err) + }) + + client.Wait() +} + +// defaultSocketPath returns the default Unix domain socket path. +func defaultSocketPath() string { + return "/tmp/stdui-ipc-example.sock" +} + +// defaultPipePath returns a sensible default named-pipe path for the current +// platform. +func defaultPipePath() string { + if runtime.GOOS == "windows" { + return `\\.\pipe\stdui-ipc-example` + } + return "/tmp/stdui-ipc-example.pipe" +} + +// isFlagSet reports whether the flag with the given name was explicitly +// provided on the command line. +func isFlagSet(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} diff --git a/go.work b/go.work index 7d5f1a8..f874833 100644 --- a/go.work +++ b/go.work @@ -3,6 +3,7 @@ go 1.25.5 use ( ./example/chat ./example/interactive-elements + ./example/ipc ./example/irc ./example/simple ./example/todo diff --git a/main.cpp b/main.cpp index 92587f5..a3dace1 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,6 @@ #include +#include +#include // Vendor #include "action.hpp" @@ -30,11 +32,42 @@ __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; } #endif -int main() { +/** + * @brief Parse argv for transport flags and return a TransportConfig. + * + * Supported flags: + * --socket Unix domain socket at + * --pipe Named pipe at (Unix domain socket on non-Windows) + * + * If no flag is present the default StdIO transport is used. + * + * @param argc Argument count from main(). + * @param argv Argument vector from main(). + * @return Configured IO::TransportConfig. + */ +static IO::TransportConfig ParseTransportFlags(int argc, char **argv) { + IO::TransportConfig cfg; + for (int i = 1; i < argc - 1; ++i) { + if (std::strcmp(argv[i], "--socket") == 0) { + cfg.mode = IO::TransportMode::UnixSocket; + cfg.path = argv[i + 1]; + return cfg; + } + if (std::strcmp(argv[i], "--pipe") == 0) { + cfg.mode = IO::TransportMode::NamedPipe; + cfg.path = argv[i + 1]; + return cfg; + } + } + return cfg; // default: StdIO +} + +int main(int argc, char **argv) { // - // Initialize IO subsystem to read from stdin. + // Parse CLI flags to select transport mode, then initialize the IO subsystem. // - IO::Init(); + IO::TransportConfig transportConfig = ParseTransportFlags(argc, argv); + IO::Init(transportConfig); // // Route raylib log messages through the protocol. @@ -111,10 +144,10 @@ int main() { return ImHTML::ImageMeta{tex->width, tex->height}; }; conf->GetImageTexture = [](const char *url, const char *) { return (ImTextureID)TextureCache::GetPtr(url); }; - conf->FontRegular = UI::Style::GetFont(UI::Style::FontStyle::Regular); - conf->FontBold = UI::Style::GetFont(UI::Style::FontStyle::Bold); - conf->FontItalic = UI::Style::GetFont(UI::Style::FontStyle::Italic); - conf->FontBoldItalic = UI::Style::GetFont(UI::Style::FontStyle::BoldItalic); + conf->DefaultFont.Regular = UI::Style::GetFont(UI::Style::FontStyle::Regular); + conf->DefaultFont.Bold = UI::Style::GetFont(UI::Style::FontStyle::Bold); + conf->DefaultFont.Italic = UI::Style::GetFont(UI::Style::FontStyle::Italic); + conf->DefaultFont.BoldItalic = UI::Style::GetFont(UI::Style::FontStyle::BoldItalic); // // Notify the controlling process that stdui is fully initialized and diff --git a/sdk/go/transport.go b/sdk/go/transport.go new file mode 100644 index 0000000..473e4d0 --- /dev/null +++ b/sdk/go/transport.go @@ -0,0 +1,156 @@ +package stdui + +import ( + "bufio" + "fmt" + "net" + "os/exec" + "time" +) + +// StartWithSocket spawns the stdui binary with the --socket flag, waits for it +// to create the Unix domain socket (or Windows AF_UNIX socket), connects to +// it, sends the initial settings, and begins reading events. +// +// The socket file is created by the stdui process; the caller must not create +// it beforehand. The file is removed by stdui on shutdown. +// +// socketPath is the filesystem path for the socket, e.g. "/tmp/myapp.sock". +// On Windows this must be a path on a local volume; the same Windows 10 1803+ +// AF_UNIX support that stdui relies on is used here. +// +// The binary path may be absolute or relative to the working directory of the +// calling process. +func StartWithSocket(binaryPath string, socketPath string, settings Settings) (*Client, error) { + cmd := exec.Command(binaryPath, "--socket", socketPath) + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("stdui: start process: %w", err) + } + + // The stdui process needs a moment to create and bind the socket before we + // can connect. We retry with a short back-off for up to 5 seconds. + conn, err := dialRetry("unix", socketPath, 5*time.Second) + if err != nil { + cmd.Process.Kill() //nolint:errcheck + return nil, fmt.Errorf("stdui: connect to socket %s: %w", socketPath, err) + } + + return newClientFromConn(cmd, conn, settings) +} + +// StartWithNamedPipe spawns the stdui binary with the --pipe flag, waits for +// it to create the named pipe, connects to it, sends the initial settings, +// and begins reading events. +// +// On Unix/macOS, pipePath is the filesystem path of the Unix domain socket +// that stdui creates as the pipe backend, e.g. "/tmp/myapp.pipe". FIFOs are +// not used because they cannot carry bidirectional IPC reliably. +// +// On Windows, pipePath must be a valid Windows named-pipe path of the form +// \\.\pipe\, e.g. `\\.\pipe\myapp`. +// +// The binary path may be absolute or relative to the working directory of the +// calling process. +func StartWithNamedPipe(binaryPath string, pipePath string, settings Settings) (*Client, error) { + cmd := exec.Command(binaryPath, "--pipe", pipePath) + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("stdui: start process: %w", err) + } + + conn, err := openNamedPipe(pipePath, 5*time.Second) + if err != nil { + cmd.Process.Kill() //nolint:errcheck + return nil, fmt.Errorf("stdui: connect to pipe %s: %w", pipePath, err) + } + + return newClientFromConn(cmd, conn, settings) +} + +// dialRetry tries net.Dial(network, address) repeatedly until it succeeds or +// timeout elapses. The back-off starts at 10ms and doubles up to 250ms. +func dialRetry(network, address string, timeout time.Duration) (net.Conn, error) { + deadline := time.Now().Add(timeout) + delay := 10 * time.Millisecond + for { + conn, err := net.Dial(network, address) + if err == nil { + return conn, nil + } + if time.Now().After(deadline) { + return nil, err + } + time.Sleep(delay) + delay *= 2 + if delay > 250*time.Millisecond { + delay = 250 * time.Millisecond + } + } +} + +// newClientFromConn creates a Client that communicates over an already-open +// net.Conn (socket or pipe) instead of stdin/stdout. +func newClientFromConn(cmd *exec.Cmd, conn net.Conn, settings Settings) (*Client, error) { + c := &Client{ + cmd: cmd, + stdin: &connWriteCloser{conn}, + done: make(chan struct{}), + } + + if err := c.send(message{Action: "settings", Data: settings}); err != nil { + conn.Close() //nolint:errcheck + cmd.Process.Kill() //nolint:errcheck + return nil, fmt.Errorf("stdui: send settings: %w", err) + } + + go c.readLoop(conn) + + return c, nil +} + +// connWriteCloser wraps a net.Conn and exposes only Write and Close so it +// satisfies the io.WriteCloser interface expected by Client.stdin. +type connWriteCloser struct { + conn net.Conn +} + +func (w *connWriteCloser) Write(p []byte) (int, error) { + return w.conn.Write(p) +} + +func (w *connWriteCloser) Close() error { + return w.conn.Close() +} + +// --------------------------------------------------------------------------- +// pipeConn — net.Conn wrapper for a plain io.ReadWriteCloser (named pipes) +// --------------------------------------------------------------------------- + +// pipeConn wraps a bufio.ReadWriter backed by a raw file handle. It implements +// net.Conn via stub methods for the parts the SDK does not use. +type pipeConn struct { + rw *bufio.ReadWriter + f interface{ Close() error } +} + +func (c *pipeConn) Read(p []byte) (int, error) { return c.rw.Read(p) } +func (c *pipeConn) Write(p []byte) (int, error) { + n, err := c.rw.Write(p) + if err != nil { + return n, err + } + return n, c.rw.Flush() +} +func (c *pipeConn) Close() error { return c.f.Close() } +func (c *pipeConn) LocalAddr() net.Addr { return pipeAddr(0) } +func (c *pipeConn) RemoteAddr() net.Addr { return pipeAddr(0) } +func (c *pipeConn) SetDeadline(_ time.Time) error { return nil } +func (c *pipeConn) SetReadDeadline(_ time.Time) error { return nil } +func (c *pipeConn) SetWriteDeadline(_ time.Time) error { return nil } + +// pipeAddr is a minimal net.Addr for named pipes. +type pipeAddr int + +func (pipeAddr) Network() string { return "pipe" } +func (pipeAddr) String() string { return "pipe" } diff --git a/sdk/go/transport_unix.go b/sdk/go/transport_unix.go new file mode 100644 index 0000000..e864f8e --- /dev/null +++ b/sdk/go/transport_unix.go @@ -0,0 +1,18 @@ +//go:build !windows + +package stdui + +import ( + "net" + "time" +) + +// openNamedPipe connects to the Unix domain socket that stdui creates when +// given the --pipe flag on Unix. +// +// On non-Windows platforms stdui backs --pipe with a Unix domain socket +// (identical to --socket) because POSIX FIFOs cannot carry bidirectional IPC +// reliably. We therefore just dial the path as a Unix socket. +func openNamedPipe(path string, timeout time.Duration) (net.Conn, error) { + return dialRetry("unix", path, timeout) +} diff --git a/sdk/go/transport_windows.go b/sdk/go/transport_windows.go new file mode 100644 index 0000000..3e3f670 --- /dev/null +++ b/sdk/go/transport_windows.go @@ -0,0 +1,44 @@ +//go:build windows + +package stdui + +import ( + "bufio" + "fmt" + "net" + "os" + "time" +) + +// openNamedPipe connects to the Windows named pipe at path. +// +// path must be a valid Windows named-pipe path such as \\.\pipe\myapp. +// We retry opening until the pipe server (stdui) has created it. +func openNamedPipe(path string, timeout time.Duration) (net.Conn, error) { + deadline := time.Now().Add(timeout) + delay := 10 * time.Millisecond + + var f *os.File + var err error + for { + // os.OpenFile works for named pipes on Windows. + f, err = os.OpenFile(path, os.O_RDWR, os.ModeNamedPipe) + if err == nil { + break + } + if time.Now().After(deadline) { + return nil, fmt.Errorf("timed out waiting for named pipe %s: %w", path, err) + } + time.Sleep(delay) + delay *= 2 + if delay > 250*time.Millisecond { + delay = 250 * time.Millisecond + } + } + + conn := &pipeConn{ + rw: bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)), + f: f, + } + return conn, nil +} diff --git a/src/io.cpp b/src/io.cpp index 0030b2b..63698a0 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -4,27 +4,133 @@ #include #include +#include #include #include #include #include +#include #include #include +// Platform-specific socket / pipe headers +#ifndef _WIN32 +#include +#include +#include +#else +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + namespace IO { namespace { +// --------------------------------------------------------------------------- +// Shared state +// --------------------------------------------------------------------------- + std::thread THREAD; std::mutex MUTEX; std::queue QUEUE; std::atomic RUNNING{false}; -void ReaderThread() { +TransportConfig CONFIG; + +// Write mutex so concurrent WriteValue calls don't interleave bytes. +std::mutex WRITE_MUTEX; + +// --------------------------------------------------------------------------- +// Platform-specific transport state +// --------------------------------------------------------------------------- + +#ifndef _WIN32 +// Unix domain socket (and --pipe alias on Unix): the accepted connection fd, or -1. +int SOCKET_FD{-1}; +#else +// Windows named pipe handle. +HANDLE PIPE_HANDLE{INVALID_HANDLE_VALUE}; +#endif + +// --------------------------------------------------------------------------- +// Low-level write helpers +// --------------------------------------------------------------------------- + +/** + * @brief Write all bytes of @p data to the active transport. + * + * @param data Pointer to the byte buffer. + * @param len Number of bytes to write. + */ +void RawWrite(const char *data, std::size_t len) { + switch (CONFIG.mode) { + case TransportMode::StdIO: + std::cout.write(data, static_cast(len)); + std::cout.flush(); + break; + +#ifndef _WIN32 + case TransportMode::UnixSocket: { + std::size_t written = 0; + while (written < len) { + ssize_t n = ::send(SOCKET_FD, data + written, len - written, 0); + if (n <= 0) break; + written += static_cast(n); + } + break; + } + + case TransportMode::NamedPipe: { + // On Unix, --pipe is backed by a Unix domain socket (same as --socket). + std::size_t written = 0; + while (written < len) { + ssize_t n = ::send(SOCKET_FD, data + written, len - written, 0); + if (n <= 0) break; + written += static_cast(n); + } + break; + } +#else + case TransportMode::UnixSocket: { + // On Windows, UDS is surfaced through the Winsock API the same way. + std::size_t written = 0; + while (written < len) { + int n = ::send(reinterpret_cast(SOCKET_FD), data + written, static_cast(len - written), 0); + if (n <= 0) break; + written += static_cast(n); + } + break; + } + + case TransportMode::NamedPipe: { + DWORD total = 0; + while (total < static_cast(len)) { + DWORD wrote = 0; + if (!WriteFile(PIPE_HANDLE, data + total, static_cast(len) - total, &wrote, nullptr)) break; + total += wrote; + } + break; + } +#endif + } +} + +// --------------------------------------------------------------------------- +// Reader thread implementations +// --------------------------------------------------------------------------- + +/** + * @brief Reader thread body for StdIO transport. + * + * Blocks on std::getline(std::cin, …) and pushes complete lines onto QUEUE. + */ +void ReaderThreadStdIO() { std::string line; while (RUNNING.load()) { if (!std::getline(std::cin, line)) { - // EOF or error — stop the thread RUNNING.store(false); break; } @@ -33,17 +139,296 @@ void ReaderThread() { } } +#ifndef _WIN32 + +/** + * @brief Reader thread body for Unix domain socket transport. + * + * Reads bytes from SOCKET_FD and reassembles newline-delimited lines. + */ +void ReaderThreadSocket() { + std::string buf; + char chunk[4096]; + while (RUNNING.load()) { + ssize_t n = ::recv(SOCKET_FD, chunk, sizeof(chunk), 0); + if (n <= 0) { + RUNNING.store(false); + break; + } + buf.append(chunk, static_cast(n)); + std::size_t pos; + while ((pos = buf.find('\n')) != std::string::npos) { + std::string line = buf.substr(0, pos); + buf.erase(0, pos + 1); + std::lock_guard lock(MUTEX); + QUEUE.push(std::move(line)); + } + } +} + +#else // _WIN32 + +/** + * @brief Reader thread body for Unix domain socket transport on Windows. + * + * Windows 10 1803+ exposes UDS through Winsock. + */ +void ReaderThreadSocket() { + std::string buf; + char chunk[4096]; + while (RUNNING.load()) { + int n = ::recv(reinterpret_cast(SOCKET_FD), chunk, sizeof(chunk), 0); + if (n <= 0) { + RUNNING.store(false); + break; + } + buf.append(chunk, static_cast(n)); + std::size_t pos; + while ((pos = buf.find('\n')) != std::string::npos) { + std::string line = buf.substr(0, pos); + buf.erase(0, pos + 1); + std::lock_guard lock(MUTEX); + QUEUE.push(std::move(line)); + } + } +} + +/** + * @brief Reader thread body for Windows named-pipe transport. + */ +void ReaderThreadNamedPipe() { + std::string buf; + char chunk[4096]; + while (RUNNING.load()) { + DWORD bytesRead = 0; + BOOL ok = ReadFile(PIPE_HANDLE, chunk, sizeof(chunk), &bytesRead, nullptr); + if (!ok || bytesRead == 0) { + RUNNING.store(false); + break; + } + buf.append(chunk, static_cast(bytesRead)); + std::size_t pos; + while ((pos = buf.find('\n')) != std::string::npos) { + std::string line = buf.substr(0, pos); + buf.erase(0, pos + 1); + std::lock_guard lock(MUTEX); + QUEUE.push(std::move(line)); + } + } +} + +#endif // _WIN32 + +// --------------------------------------------------------------------------- +// Transport setup helpers +// --------------------------------------------------------------------------- + +#ifndef _WIN32 + +/** + * @brief Create and bind a Unix domain socket, then accept one connection. + * + * @param path Filesystem path for the socket file. + * @throws std::runtime_error on any system-call failure. + */ +void SetupUnixSocket(const std::string &path) { + // Remove a stale socket file if present. + ::unlink(path.c_str()); + + int server_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + throw std::runtime_error("IO: socket() failed: " + std::string(::strerror(errno))); + } + + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + if (path.size() >= sizeof(addr.sun_path)) { + ::close(server_fd); + throw std::runtime_error("IO: socket path too long"); + } + std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); + + if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(server_fd); + throw std::runtime_error("IO: bind() failed: " + std::string(::strerror(errno))); + } + + if (::listen(server_fd, 1) < 0) { + ::close(server_fd); + throw std::runtime_error("IO: listen() failed: " + std::string(::strerror(errno))); + } + + // Block until the controlling process connects. + SOCKET_FD = ::accept(server_fd, nullptr, nullptr); + ::close(server_fd); // We only need one connection. + + if (SOCKET_FD < 0) { + throw std::runtime_error("IO: accept() failed: " + std::string(::strerror(errno))); + } +} + +#else // _WIN32 + +/** + * @brief Create and bind a Unix domain socket on Windows (Winsock), then + * accept one connection. + * + * @param path Filesystem path for the socket file. + * @throws std::runtime_error on any system-call failure. + */ +void SetupUnixSocket(const std::string &path) { + WSADATA wsa{}; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + throw std::runtime_error("IO: WSAStartup failed"); + } + + SOCKET server_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd == INVALID_SOCKET) { + throw std::runtime_error("IO: socket() failed"); + } + + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + if (path.size() >= sizeof(addr.sun_path)) { + ::closesocket(server_fd); + throw std::runtime_error("IO: socket path too long"); + } + std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); + + // Remove stale socket file. + DeleteFileA(path.c_str()); + + if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR) { + ::closesocket(server_fd); + throw std::runtime_error("IO: bind() failed"); + } + + if (::listen(server_fd, 1) == SOCKET_ERROR) { + ::closesocket(server_fd); + throw std::runtime_error("IO: listen() failed"); + } + + SOCKET client = ::accept(server_fd, nullptr, nullptr); + ::closesocket(server_fd); + + if (client == INVALID_SOCKET) { + throw std::runtime_error("IO: accept() failed"); + } + + // Store as int for uniformity with the Unix path. + SOCKET_FD = static_cast(client); +} + +/** + * @brief Create a Windows named pipe at @p path and wait for one connection. + * + * @p path should be in the form \\.\pipe\. + * + * @param path Named-pipe path. + * @throws std::runtime_error on any system-call failure. + */ +void SetupNamedPipe(const std::string &path) { + PIPE_HANDLE = CreateNamedPipeA(path.c_str(), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, // max instances + 65536, // out buffer + 65536, // in buffer + 0, // default timeout + nullptr // default security + ); + + if (PIPE_HANDLE == INVALID_HANDLE_VALUE) { + throw std::runtime_error("IO: CreateNamedPipe() failed"); + } + + // Block until the controlling process connects. + if (!ConnectNamedPipe(PIPE_HANDLE, nullptr)) { + DWORD err = GetLastError(); + if (err != ERROR_PIPE_CONNECTED) { + CloseHandle(PIPE_HANDLE); + PIPE_HANDLE = INVALID_HANDLE_VALUE; + throw std::runtime_error("IO: ConnectNamedPipe() failed"); + } + } +} + +#endif // _WIN32 + } // namespace -void Init() { +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void Init(TransportConfig config) { + CONFIG = config; RUNNING.store(true); - THREAD = std::thread(ReaderThread); + + switch (config.mode) { + case TransportMode::StdIO: + THREAD = std::thread(ReaderThreadStdIO); + break; + + case TransportMode::UnixSocket: + SetupUnixSocket(config.path); + THREAD = std::thread(ReaderThreadSocket); + break; + + case TransportMode::NamedPipe: +#ifndef _WIN32 + // On Unix, named pipes (FIFOs) cannot carry bidirectional IPC reliably. + // We implement --pipe as a Unix domain socket so behaviour is identical + // to --socket on all non-Windows platforms. + SetupUnixSocket(config.path); + THREAD = std::thread(ReaderThreadSocket); +#else + SetupNamedPipe(config.path); + THREAD = std::thread(ReaderThreadNamedPipe); +#endif + break; + } } void Shutdown() { RUNNING.store(false); - // Closing stdin unblocks getline so the thread can exit cleanly. - std::cin.setstate(std::ios::eofbit); + + switch (CONFIG.mode) { + case TransportMode::StdIO: + std::cin.setstate(std::ios::eofbit); + break; + +#ifndef _WIN32 + case TransportMode::UnixSocket: + case TransportMode::NamedPipe: + // NamedPipe on Unix is backed by a Unix domain socket (see Init). + if (SOCKET_FD >= 0) { + ::shutdown(SOCKET_FD, SHUT_RDWR); + ::close(SOCKET_FD); + SOCKET_FD = -1; + ::unlink(CONFIG.path.c_str()); + } + break; +#else + case TransportMode::UnixSocket: + if (SOCKET_FD >= 0) { + ::closesocket(static_cast(SOCKET_FD)); + SOCKET_FD = -1; + DeleteFileA(CONFIG.path.c_str()); + WSACleanup(); + } + break; + + case TransportMode::NamedPipe: + if (PIPE_HANDLE != INVALID_HANDLE_VALUE) { + DisconnectNamedPipe(PIPE_HANDLE); + CloseHandle(PIPE_HANDLE); + PIPE_HANDLE = INVALID_HANDLE_VALUE; + } + break; +#endif + } + if (THREAD.joinable()) { THREAD.join(); } @@ -86,13 +471,14 @@ Hjson::Value MustReadValue() { } void Write(const std::string &s) { - std::cout << s; - std::cout.flush(); + std::lock_guard lock(WRITE_MUTEX); + RawWrite(s.data(), s.size()); } void WriteLine(const std::string &s) { - std::cout << s << '\n'; - std::cout.flush(); + std::lock_guard lock(WRITE_MUTEX); + RawWrite(s.data(), s.size()); + RawWrite("\n", 1); } void WriteValue(const Hjson::Value &value) { @@ -104,8 +490,9 @@ void WriteValue(const Hjson::Value &value) { opt.comments = false; opt.indentBy = ""; opt.eol = ""; - std::cout << Hjson::Marshal(value, opt) << '\n'; - std::cout.flush(); + std::string serialized = Hjson::Marshal(value, opt) + "\n"; + std::lock_guard lock(WRITE_MUTEX); + RawWrite(serialized.data(), serialized.size()); } } // namespace IO diff --git a/src/io.hpp b/src/io.hpp index cdac4be..ef70ed9 100644 --- a/src/io.hpp +++ b/src/io.hpp @@ -8,15 +8,49 @@ namespace IO { /** - * @brief Start the background stdin reader thread. + * @brief Selects the transport mechanism used for reading and writing messages. + */ +enum class TransportMode { + /// Read from stdin / write to stdout (default). + StdIO, + /// Unix domain socket (also supported on Windows 10 1803+). + UnixSocket, + /// Named pipe (Unix domain socket on non-Windows; Windows named pipe on Windows). + NamedPipe, +}; + +/** + * @brief Configuration passed to Init() to select and configure the transport. + */ +struct TransportConfig { + /// The transport to use. + TransportMode mode = TransportMode::StdIO; + + /// Path for the socket file (UnixSocket), the socket file used as a named-pipe + /// alias on non-Windows (NamedPipe), or the Windows named-pipe path (NamedPipe on Windows). + /// Ignored when mode == StdIO. + std::string path; +}; + +/** + * @brief Start the background reader thread for the configured transport. + * + * For StdIO the thread blocks on stdin. + * For UnixSocket the socket is created, bound, and a single client connection + * is accepted before the reader thread starts. + * For NamedPipe on Windows a named pipe is created and one client connection + * is awaited. On all other platforms NamedPipe behaves identically to + * UnixSocket (a Unix domain socket is used instead of a FIFO, since FIFOs + * cannot carry bidirectional IPC reliably). * * Must be called once before any call to ReadLine(). - * The thread blocks on stdin and enqueues complete lines. + * + * @param config Transport configuration. Defaults to StdIO. */ -void Init(); +void Init(TransportConfig config = {}); /** - * @brief Stop the background stdin reader thread. + * @brief Stop the background reader thread and release transport resources. * * Signals the reader thread to stop and joins it. * Should be called during application shutdown. @@ -24,25 +58,25 @@ void Init(); void Shutdown(); /** - * @brief Try to dequeue a line that was received from stdin. + * @brief Try to dequeue a line that was received from the transport. * * Non-blocking. Returns std::nullopt if no complete line is available yet. * - * @return The next line from stdin, or std::nullopt if none is available. + * @return The next line, or std::nullopt if none is available. */ std::optional ReadLine(); /** - * @brief Block until a line is available from stdin, then return it. + * @brief Block until a line is available from the transport, then return it. * * Polls ReadLine() every 10ms until a line is available. * - * @return The next line from stdin. + * @return The next line. */ std::string MustReadLine(); /** - * @brief Try to read a line from stdin and parse it as an Hjson value. + * @brief Try to read a line and parse it as an Hjson value. * * Non-blocking. Returns std::nullopt if no line is available. * @@ -51,7 +85,7 @@ std::string MustReadLine(); std::optional ReadValue(); /** - * @brief Block until a line is available from stdin and parse it as an Hjson value. + * @brief Block until a line is available and parse it as an Hjson value. * * Polls every 10ms until a line is available, then parses and returns it. * @@ -60,7 +94,7 @@ std::optional ReadValue(); Hjson::Value MustReadValue(); /** - * @brief Write a string to stdout. + * @brief Write a string to the transport. * * Does not append a newline. Flushes immediately. * @@ -69,7 +103,7 @@ Hjson::Value MustReadValue(); void Write(const std::string &s); /** - * @brief Write a string to stdout followed by a newline. + * @brief Write a string to the transport followed by a newline. * * Flushes immediately. * @@ -78,7 +112,7 @@ void Write(const std::string &s); void WriteLine(const std::string &s); /** - * @brief Serialize an Hjson::Value to stdout followed by a newline. + * @brief Serialize an Hjson::Value to the transport followed by a newline. * * Flushes immediately. * diff --git a/vendor/imhtml/imhtml.cpp b/vendor/imhtml/imhtml.cpp index 0f486f7..f1a8ed2 100644 --- a/vendor/imhtml/imhtml.cpp +++ b/vendor/imhtml/imhtml.cpp @@ -4,9 +4,7 @@ #include #include -#ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS -#endif #include "imgui.h" #include "imgui_internal.h" @@ -26,12 +24,10 @@ class CustomElement : public litehtml::html_tag { std::map attributes = {}; public: - CustomElement(const std::shared_ptr &doc, const std::string &tag, + CustomElement(const std::shared_ptr& doc, const std::string& tag, std::map attributes) : litehtml::html_tag(doc), tag(tag), attributes(attributes) { - // Register the tag name with the base class so that CSS selectors can match - // this element by tag name (e.g. "ui-input { display: block; height: 28px }"). - // Without this, html_tag::m_tag stays empty_id and no tag selector ever fires. + // Register the tag name so that css selectors can modify it set_tagName(tag.c_str()); } @@ -49,11 +45,11 @@ class CustomElement : public litehtml::html_tag { m_css.set_display(litehtml::display_block); } - void draw_background(litehtml::uint_ptr hdc, int x, int y, const litehtml::position *clip, - const std::shared_ptr &ri) override; + void draw_background(litehtml::uint_ptr hdc, litehtml::pixel_t x, litehtml::pixel_t y, const litehtml::position* clip, + const std::shared_ptr& ri) override; }; -std::string DefaultFileLoader(const char *url, const char *baseurl) { +std::string DefaultFileLoader(const char* url, const char* baseurl) { if (url == nullptr || strlen(url) == 0) { return ""; } @@ -85,24 +81,64 @@ Config getCurrentConfig() { return configStack.back(); } -ImFont *getFont(FontStyle fontStyle) { - Config config = getCurrentConfig(); - switch (fontStyle) { +static ImFont* getFontFromFamily(const FontFamily& family, FontStyle style) { + switch (style) { case FontStyle::Regular: - return config.FontRegular ? config.FontRegular : ImGui::GetFont(); + return family.Regular; case FontStyle::Bold: - return config.FontBold ? config.FontBold : ImGui::GetFont(); + return family.Bold ? family.Bold : family.Regular; case FontStyle::Italic: - return config.FontItalic ? config.FontItalic : ImGui::GetFont(); + return family.Italic ? family.Italic : family.Regular; case FontStyle::BoldItalic: - return config.FontBoldItalic ? config.FontBoldItalic : ImGui::GetFont(); + if (family.BoldItalic) return family.BoldItalic; + if (family.Bold) return family.Bold; + if (family.Italic) return family.Italic; + return family.Regular; default: - return config.FontRegular ? config.FontRegular : ImGui::GetFont(); + return family.Regular; } } +static ImFont* resolveFont(const Config& cfg, const std::string& family_name, FontStyle style) { + if (!family_name.empty()) { + auto it = cfg.FontFamilies.find(family_name); + if (it != cfg.FontFamilies.end()) { + if (ImFont* f = getFontFromFamily(it->second, style)) { + return f; + } + } + } + + if (ImFont* f = getFontFromFamily(cfg.DefaultFont, style)) { + return f; + } + + return ImGui::GetFont(); +} + } // namespace +void CustomElement::draw_background(litehtml::uint_ptr hdc, litehtml::pixel_t x, litehtml::pixel_t y, + const litehtml::position* clip, const std::shared_ptr& ri) { + // Let the base class draw background color/image and borders first. + litehtml::html_tag::draw_background(hdc, x, y, clip, ri); + + // ri->pos() is the element's own content box relative to its parent. + // x/y carry the accumulated offset from all ancestors. + // Together they give the absolute document position and correct size. + litehtml::position pos = ri->pos(); + pos.x += x; + pos.y += y; + + if (customElements.find(this->tag) != customElements.end()) { + ImVec2 cursor = ImGui::GetCursorScreenPos(); + customElements[this->tag]( + ImRect(cursor + ImVec2(pos.x, pos.y), cursor + ImVec2(pos.x + pos.width, pos.y + pos.height)), + this->attributes); + ImGui::SetCursorScreenPos(cursor); + } +} + class BrowserContainer : public litehtml::document_container { private: ImVec2 bottomRight = ImVec2(0, 0); @@ -148,56 +184,87 @@ class BrowserContainer : public litehtml::document_container { // Font functions // - virtual litehtml::uint_ptr create_font(const char *faceName, int size, int weight, litehtml::font_style style, - unsigned int decoration, litehtml::font_metrics *fm) override { - bool bold = weight > 400; - bool italic = style == litehtml::font_style_italic; + struct ResolvedFont { + ImFont* Font = nullptr; + FontStyle Style = FontStyle::Regular; + std::string Family; + float Size = 16.0f; + litehtml::font_metrics Metrics{}; + }; + + std::vector> fonts_; + + static ResolvedFont* from_handle(litehtml::uint_ptr hFont) { return reinterpret_cast(hFont); } + + virtual litehtml::uint_ptr create_font(const litehtml::font_description& descr, const litehtml::document* doc, + litehtml::font_metrics* fm) override { + bool bold = descr.weight > 400; + bool italic = descr.style == litehtml::font_style_italic; FontStyle fontStyle = FontStyle::Regular; - if (bold) { - fontStyle = FontStyle::Bold; - } - if (italic) { - fontStyle = FontStyle::Italic; - } if (bold && italic) { fontStyle = FontStyle::BoldItalic; + } else if (bold) { + fontStyle = FontStyle::Bold; + } else if (italic) { + fontStyle = FontStyle::Italic; } - ImGui::PushFont(getFont(fontStyle), size); - fm->height = ImGui::GetTextLineHeight(); - ImGui::PopFont(); + ImFont* font = resolveFont(config, descr.family, fontStyle); + + auto rf = std::make_unique(); + rf->Font = font; + rf->Style = fontStyle; + rf->Family = descr.family; + rf->Size = descr.size; - litehtml::uint_ptr hFont = (int)fontStyle << 16 | size; + const float base_size = font ? font->GetFontBaked(descr.size)->Size : ImGui::GetFontSize(); + const float scale = base_size > 0.0f ? (descr.size / base_size) : 1.0f; - return hFont; + rf->Metrics.font_size = (int)descr.size; + rf->Metrics.height = (int)(base_size * scale); + rf->Metrics.ascent = font ? (int)(font->GetFontBaked(descr.size)->Ascent * scale) : (int)(base_size * 0.8f); + rf->Metrics.descent = font ? (int)(-font->GetFontBaked(descr.size)->Descent * scale) : (int)(base_size * 0.2f); + rf->Metrics.x_height = rf->Metrics.ascent / 2; + + if (fm) { + *fm = rf->Metrics; + } + + ResolvedFont* raw = rf.get(); + fonts_.push_back(std::move(rf)); + return reinterpret_cast(raw); } virtual void delete_font(litehtml::uint_ptr hFont) override { // do nothing for now } - virtual int text_width(const char *text, litehtml::uint_ptr hFont) override { - int fontStyle = hFont >> 16; - int fontSize = hFont & 0xffff; + virtual litehtml::pixel_t text_width(const char* text, litehtml::uint_ptr hFont) override { + auto* rf = from_handle(hFont); + if (!rf || !rf->Font || !text) { + return 0; + } - ImGui::PushFont(getFont((FontStyle)fontStyle), fontSize); - auto size = ImGui::CalcTextSize(text); - ImGui::PopFont(); - return size.x; + const char* end = text + strlen(text); + ImVec2 size = rf->Font->CalcTextSizeA(rf->Size, FLT_MAX, 0.0f, text, end, nullptr); + return (litehtml::pixel_t)size.x; } - virtual void draw_text(litehtml::uint_ptr hdc, const char *text, litehtml::uint_ptr hFont, litehtml::web_color color, - const litehtml::position &pos) override { - int fontStyle = hFont >> 16; - int fontSize = hFont & 0xffff; + virtual void draw_text(litehtml::uint_ptr hdc, const char* text, litehtml::uint_ptr hFont, litehtml::web_color color, + const litehtml::position& pos) override { + auto* rf = from_handle(hFont); + if (!rf || !rf->Font || !text) { + return; + } + + ImVec2 p = ImGui::GetCursorScreenPos() + ImVec2(pos.x, pos.y); + ImU32 col = IM_COL32(color.red, color.green, color.blue, color.alpha); - ImGui::PushFont(getFont((FontStyle)fontStyle), fontSize); - ImGui::GetWindowDrawList()->AddText(ImGui::GetCursorScreenPos() + ImVec2(pos.x, pos.y), - IM_COL32(color.red, color.green, color.blue, color.alpha), - text); - auto size = ImGui::CalcTextSize(text); - ImGui::PopFont(); + ImGui::GetWindowDrawList()->AddText(rf->Font, rf->Size, p, col, text); + + const char* end = text + strlen(text); + ImVec2 size = rf->Font->CalcTextSizeA(rf->Size, FLT_MAX, 0.0f, text, end, nullptr); push_bottom_right(ImVec2(pos.x + size.x, pos.y + size.y)); } @@ -205,16 +272,42 @@ class BrowserContainer : public litehtml::document_container { // Measurement and defaults // - virtual int pt_to_px(int pt) const override { return pt; } - virtual int get_default_font_size() const override { return config.BaseFontSize; } - virtual const char *get_default_font_name() const override { return "Default"; } + virtual litehtml::pixel_t pt_to_px(float pt) const override { return pt; } + virtual litehtml::pixel_t get_default_font_size() const override { return config.BaseFontSize; } + virtual const char* get_default_font_name() const override { return "Default"; } // // Drawing functions // - virtual void draw_list_marker(litehtml::uint_ptr hdc, const litehtml::list_marker &marker) override { - ImDrawList *draw_list = ImGui::GetWindowDrawList(); + struct LayerGeometry { + ImVec2 border_min; + ImVec2 border_max; + ImVec2 clip_min; + ImVec2 clip_max; + float tl, tr, br, bl; + }; + + LayerGeometry get_layer_geometry(const litehtml::background_layer& layer) const { + ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + + LayerGeometry g; + g.border_min = screen_pos + ImVec2((float)layer.border_box.x, (float)layer.border_box.y); + g.border_max = screen_pos + ImVec2((float)(layer.border_box.x + layer.border_box.width), + (float)(layer.border_box.y + layer.border_box.height)); + g.clip_min = screen_pos + ImVec2((float)layer.clip_box.x, (float)layer.clip_box.y); + g.clip_max = screen_pos + ImVec2((float)(layer.clip_box.x + layer.clip_box.width), + (float)(layer.clip_box.y + layer.clip_box.height)); + + g.tl = (float)layer.border_radius.top_left_x; + g.tr = (float)layer.border_radius.top_right_x; + g.br = (float)layer.border_radius.bottom_right_x; + g.bl = (float)layer.border_radius.bottom_left_x; + return g; + } + + virtual void draw_list_marker(litehtml::uint_ptr hdc, const litehtml::list_marker& marker) override { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 center = ImGui::GetCursorScreenPos() + ImVec2(marker.pos.x + marker.pos.width / 2.0f, marker.pos.y + marker.pos.height / 2.0f); float radius = marker.pos.width / 2.0f; @@ -241,7 +334,7 @@ class BrowserContainer : public litehtml::document_container { push_bottom_right(ImVec2(marker.pos.x + marker.pos.width, marker.pos.y + marker.pos.height)); } - virtual void load_image(const char *src, const char *baseurl, bool redraw_on_ready) override { + virtual void load_image(const char* src, const char* baseurl, bool redraw_on_ready) override { if (!config.LoadImage) { return; } @@ -249,7 +342,7 @@ class BrowserContainer : public litehtml::document_container { config.LoadImage(src, baseurl); } - virtual void get_image_size(const char *src, const char *baseurl, litehtml::size &sz) override { + virtual void get_image_size(const char* src, const char* baseurl, litehtml::size& sz) override { if (!config.GetImageMeta) { return; } @@ -259,79 +352,561 @@ class BrowserContainer : public litehtml::document_container { sz.height = imageMeta.height; } - virtual void draw_background(litehtml::uint_ptr hdc, const std::vector &bg) override { - for (auto &paint : bg) { - ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + virtual void draw_image(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, const std::string& url, + const std::string& base_url) override { + if (!config.GetImageTexture) { + return; + } - litehtml::position bg_box = paint.border_box; - litehtml::position clip_box = paint.clip_box; + ImTextureID texture = config.GetImageTexture(url.c_str(), base_url.c_str()); + if (!texture) { + return; + } - ImVec2 p_min = screen_pos + ImVec2(bg_box.x, bg_box.y); - ImVec2 p_max = screen_pos + ImVec2(bg_box.x + bg_box.width, bg_box.y + bg_box.height); + LayerGeometry lgm = this->get_layer_geometry(layer); + ImVec2 p_min = lgm.border_min; + ImVec2 p_max = lgm.border_max; - float tl = paint.border_radius.top_left_x; - float tr = paint.border_radius.top_right_x; - float br = paint.border_radius.bottom_right_x; - float bl = paint.border_radius.bottom_left_x; - ImU32 bg_color = IM_COL32(paint.color.red, paint.color.green, paint.color.blue, paint.color.alpha); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); - if (paint.color.alpha > 0) { - if (tl == tr && tr == br && br == bl) { - ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, bg_color, tl); - } else { - auto *draw_list = ImGui::GetWindowDrawList(); - draw_list->PathClear(); - if (tl > 0.0f) - draw_list->PathArcTo(ImVec2(p_min.x + tl, p_min.y + tl), tl, IM_PI, IM_PI * 1.5f); - else - draw_list->PathLineTo(ImVec2(p_min.x, p_min.y)); + float radius = std::min({lgm.tl, lgm.tr, lgm.br, lgm.bl}); - if (tr > 0.0f) - draw_list->PathArcTo(ImVec2(p_max.x - tr, p_min.y + tr), tr, IM_PI * 1.5f, IM_PI * 2.0f); - else - draw_list->PathLineTo(ImVec2(p_max.x, p_min.y)); + draw_list->PushClipRect(lgm.clip_min, lgm.clip_max, true); - if (br > 0.0f) - draw_list->PathArcTo(ImVec2(p_max.x - br, p_max.y - br), br, 0.0f, IM_PI * 0.5f); - else - draw_list->PathLineTo(ImVec2(p_max.x, p_max.y)); + if (radius > 0.0f) { + draw_list->AddImageRounded(texture, p_min, p_max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, lgm.tl); + } else { + draw_list->AddImage(texture, p_min, p_max); + } - if (bl > 0.0f) - draw_list->PathArcTo(ImVec2(p_min.x + bl, p_max.y - bl), bl, IM_PI * 0.5f, IM_PI); - else - draw_list->PathLineTo(ImVec2(p_min.x, p_max.y)); + draw_list->PopClipRect(); - draw_list->PathFillConvex(bg_color); - } + push_bottom_right(ImVec2(lgm.border_max.x, lgm.border_max.y)); + } + + virtual void draw_solid_fill(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, + const litehtml::web_color& color) override { + if (color.alpha == 0) { + return; + } + + const litehtml::position& bg_box = layer.border_box; + const litehtml::position& clip_box = layer.clip_box; + + if (bg_box.width <= 0 || bg_box.height <= 0 || clip_box.width <= 0 || clip_box.height <= 0) { + return; + } + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + LayerGeometry lgm = this->get_layer_geometry(layer); + + ImU32 fill_col = IM_COL32(color.red, color.green, color.blue, color.alpha); + + draw_list->PushClipRect(lgm.clip_min, lgm.clip_max, true); + + if (lgm.tl == lgm.tr && lgm.tr == lgm.br && lgm.br == lgm.bl) { + draw_list->AddRectFilled(lgm.border_min, lgm.border_max, fill_col, lgm.tl); + } else { + draw_list->PathClear(); + + if (lgm.tl > 0.0f) + draw_list->PathArcTo(ImVec2(lgm.border_min.x + lgm.tl, lgm.border_min.y + lgm.tl), lgm.tl, IM_PI, IM_PI * 1.5f); + else + draw_list->PathLineTo(ImVec2(lgm.border_min.x, lgm.border_min.y)); + + if (lgm.tr > 0.0f) + draw_list->PathArcTo( + ImVec2(lgm.border_max.x - lgm.tr, lgm.border_min.y + lgm.tr), lgm.tr, IM_PI * 1.5f, IM_PI * 2.0f); + else + draw_list->PathLineTo(ImVec2(lgm.border_max.x, lgm.border_min.y)); + + if (lgm.br > 0.0f) + draw_list->PathArcTo(ImVec2(lgm.border_max.x - lgm.br, lgm.border_max.y - lgm.br), lgm.br, 0.0f, IM_PI * 0.5f); + else + draw_list->PathLineTo(ImVec2(lgm.border_max.x, lgm.border_max.y)); + + if (lgm.bl > 0.0f) + draw_list->PathArcTo(ImVec2(lgm.border_min.x + lgm.bl, lgm.border_max.y - lgm.bl), lgm.bl, IM_PI * 0.5f, IM_PI); + else + draw_list->PathLineTo(ImVec2(lgm.border_min.x, lgm.border_max.y)); + + draw_list->PathFillConvex(fill_col); + } + + draw_list->PopClipRect(); + + push_bottom_right(ImVec2((float)(bg_box.x + bg_box.width), (float)(bg_box.y + bg_box.height))); + } + + static constexpr float kEpsilon = 1e-6f; + + static ImU32 to_im_col32(const litehtml::web_color& c) { return IM_COL32(c.red, c.green, c.blue, c.alpha); } + + static litehtml::web_color sample_gradient_color(const std::vector& points, + float t) { + litehtml::web_color out{0, 0, 0, 0}; + + if (points.empty()) { + return out; + } + + if (t <= points.front().offset) { + return points.front().color; + } + + if (t >= points.back().offset) { + return points.back().color; + } + + for (size_t i = 1; i < points.size(); ++i) { + const auto& a = points[i - 1]; + const auto& b = points[i]; + + if (t >= a.offset && t <= b.offset) { + const float span = b.offset - a.offset; + const float u = (span > 0.0f) ? ((t - a.offset) / span) : 0.0f; + + auto lerp_u8 = [u](unsigned char x, unsigned char y) -> unsigned char { + return (unsigned char)(x + (y - x) * u); + }; + + out.red = lerp_u8(a.color.red, b.color.red); + out.green = lerp_u8(a.color.green, b.color.green); + out.blue = lerp_u8(a.color.blue, b.color.blue); + out.alpha = lerp_u8(a.color.alpha, b.color.alpha); + return out; } + } - if (!paint.image.empty() && config.GetImageTexture) { - ImTextureID texture = config.GetImageTexture(paint.image.c_str(), paint.baseurl.c_str()); - ImVec2 img_p_min = screen_pos + ImVec2(clip_box.x, clip_box.y); - ImVec2 img_p_max = screen_pos + ImVec2(clip_box.x + clip_box.width, clip_box.y + clip_box.height); + return points.back().color; + } - float radius = std::max({tl, tr, bl, br}); - if (radius > 0.0f) { - ImGui::GetWindowDrawList()->AddImageRounded( - texture, img_p_min, img_p_max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, radius); - } else { - ImGui::GetWindowDrawList()->AddImage(texture, img_p_min, img_p_max); + static float cross2(const ImVec2& a, const ImVec2& b) { return a.x * b.y - a.y * b.x; } + + static ImVec2 line_intersection(const ImVec2& p1, const ImVec2& p2, const ImVec2& q1, const ImVec2& q2) { + const ImVec2 r = p2 - p1; + const ImVec2 s = q2 - q1; + const float rxs = cross2(r, s); + + if (fabsf(rxs) < kEpsilon) { + return p1; // parallel fallback + } + + const float t = cross2(q1 - p1, s) / rxs; + return p1 + r * t; + } + + static bool is_inside_edge(const ImVec2& p, const ImVec2& a, const ImVec2& b) { + // For counter-clockwise clip polygon, inside is on the left side of edge AB. + return cross2(b - a, p - a) >= 0.0f; + } + + static std::vector clip_polygon_convex(const std::vector& subject_polygon, + const std::vector& clip_polygon) { + std::vector result = subject_polygon; + + for (size_t edge_index = 0; edge_index < clip_polygon.size(); ++edge_index) { + const ImVec2& clip_edge_start = clip_polygon[edge_index]; + const ImVec2& clip_edge_end = clip_polygon[(edge_index + 1) % clip_polygon.size()]; + + if (result.empty()) { + break; + } + + std::vector input = std::move(result); + result.clear(); + + ImVec2 previous_point = input.back(); + bool previous_inside = is_inside_edge(previous_point, clip_edge_start, clip_edge_end); + + for (const ImVec2& current_point : input) { + const bool current_inside = is_inside_edge(current_point, clip_edge_start, clip_edge_end); + + if (current_inside) { + if (!previous_inside) { + result.push_back(line_intersection(previous_point, current_point, clip_edge_start, clip_edge_end)); + } + + result.push_back(current_point); + + } else if (previous_inside) { + result.push_back(line_intersection(previous_point, current_point, clip_edge_start, clip_edge_end)); } + + previous_point = current_point; + previous_inside = current_inside; + } + } + + return result; + } + + template + static void draw_convex_shaded_polygon(ImDrawList* draw_list, const std::vector& poly, + ColorFunc&& color_for_point) { + if (poly.size() < 3) { + return; + } + + const ImVec2 uv = draw_list->_Data->TexUvWhitePixel; + const ImDrawIdx base = draw_list->_VtxCurrentIdx; + const int vtx_count = (int)poly.size(); + const int idx_count = (vtx_count - 2) * 3; + + draw_list->PrimReserve(idx_count, vtx_count); + + for (int i = 1; i < vtx_count - 1; ++i) { + draw_list->PrimWriteIdx(base + 0); + draw_list->PrimWriteIdx(base + i); + draw_list->PrimWriteIdx(base + i + 1); + } + + for (const ImVec2& p : poly) { + draw_list->PrimWriteVtx(p, uv, color_for_point(p)); + } + } + + template + static void draw_clipped_shaded_polygon(ImDrawList* draw_list, const std::vector& poly, + const std::vector& clip_poly, ColorFunc&& color_for_point) { + std::vector clipped = clip_polygon_convex(poly, clip_poly); + if (clipped.size() < 3) { + return; + } + + draw_convex_shaded_polygon(draw_list, clipped, std::forward(color_for_point)); + } + + static void append_point_if_distinct(std::vector& pts, const ImVec2& p, float eps = 0.01f) { + if (pts.empty()) { + pts.push_back(p); + return; + } + + const ImVec2& last = pts.back(); + if (fabsf(last.x - p.x) > eps || fabsf(last.y - p.y) > eps) { + pts.push_back(p); + } + } + + static void append_arc_points(std::vector& pts, const ImVec2& center, float radius, float a_min, float a_max, + int segments, bool skip_first) { + if (radius <= 0.0f || segments <= 0) { + return; + } + + for (int i = skip_first ? 1 : 0; i <= segments; ++i) { + const float t = (float)i / (float)segments; + const float a = a_min + (a_max - a_min) * t; + append_point_if_distinct(pts, ImVec2(center.x + cosf(a) * radius, center.y + sinf(a) * radius)); + } + } + + static std::vector build_rect_polygon(const ImVec2& p_min, const ImVec2& p_max) { + return { + ImVec2(p_min.x, p_min.y), + ImVec2(p_max.x, p_min.y), + ImVec2(p_max.x, p_max.y), + ImVec2(p_min.x, p_max.y), + }; + } + + static std::vector build_rounded_rect_polygon(const ImVec2& p_min, const ImVec2& p_max, float tl, float tr, + float br, float bl, int arc_segments = 8) { + std::vector pts; + pts.reserve(4 * (arc_segments + 1)); + + const float w = p_max.x - p_min.x; + const float h = p_max.y - p_min.y; + const float max_r = ImMin(w * 0.5f, h * 0.5f); + + tl = ImClamp(tl, 0.0f, max_r); + tr = ImClamp(tr, 0.0f, max_r); + br = ImClamp(br, 0.0f, max_r); + bl = ImClamp(bl, 0.0f, max_r); + + append_point_if_distinct(pts, ImVec2(p_min.x + tl, p_min.y)); + append_point_if_distinct(pts, ImVec2(p_max.x - tr, p_min.y)); + + if (tr > 0.0f) { + append_arc_points(pts, ImVec2(p_max.x - tr, p_min.y + tr), tr, -IM_PI * 0.5f, 0.0f, arc_segments, true); + } + + append_point_if_distinct(pts, ImVec2(p_max.x, p_max.y - br)); + + if (br > 0.0f) { + append_arc_points(pts, ImVec2(p_max.x - br, p_max.y - br), br, 0.0f, IM_PI * 0.5f, arc_segments, true); + } + + append_point_if_distinct(pts, ImVec2(p_min.x + bl, p_max.y)); + + if (bl > 0.0f) { + append_arc_points(pts, ImVec2(p_min.x + bl, p_max.y - bl), bl, IM_PI * 0.5f, IM_PI, arc_segments, true); + } + + append_point_if_distinct(pts, ImVec2(p_min.x, p_min.y + tl)); + + if (tl > 0.0f) { + append_arc_points(pts, ImVec2(p_min.x + tl, p_min.y + tl), tl, IM_PI, IM_PI * 1.5f, arc_segments, true); + } + + return pts; + } + + static bool has_rounded_corners(const LayerGeometry& lgm) { + return lgm.tl > 0.0f || lgm.tr > 0.0f || lgm.br > 0.0f || lgm.bl > 0.0f; + } + + static std::vector build_layer_fill_polygon(const LayerGeometry& lgm, int arc_segments = 12) { + if (has_rounded_corners(lgm)) { + return build_rounded_rect_polygon(lgm.border_min, lgm.border_max, lgm.tl, lgm.tr, lgm.br, lgm.bl, arc_segments); + } + + return build_rect_polygon(lgm.border_min, lgm.border_max); + } + + static std::vector build_ellipse_polygon(const ImVec2& center, float rx, float ry, float t, int segments) { + std::vector pts; + pts.reserve(segments); + + const float ex = rx * t; + const float ey = ry * t; + + for (int i = 0; i < segments; ++i) { + const float a = ((float)i / (float)segments) * IM_PI * 2.0f; + pts.push_back(ImVec2(center.x + cosf(a) * ex, center.y + sinf(a) * ey)); + } + + return pts; + } + + static ImVec2 conic_point_on_circle(const ImVec2& center, float radius, float angle_deg) { + const float a = angle_deg * IM_PI / 180.0f; + + // 0 degrees at top, clockwise positive + const float x = sinf(a); + const float y = -cosf(a); + + return ImVec2(center.x + x * radius, center.y + y * radius); + } + + static std::vector build_conic_wedge_polygon(const ImVec2& center, float radius, float angle0_deg, + float angle1_deg, int arc_segments) { + std::vector pts; + pts.reserve(arc_segments + 3); + + pts.push_back(center); + + for (int i = 0; i <= arc_segments; ++i) { + const float t = (float)i / (float)arc_segments; + const float a = angle0_deg + (angle1_deg - angle0_deg) * t; + pts.push_back(conic_point_on_circle(center, radius, a)); + } + + return pts; + } + + void draw_linear_gradient_impl(const LayerGeometry& lgm, + const litehtml::background_layer::linear_gradient& gradient) { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + const ImVec2 start = screen_pos + ImVec2(gradient.start.x, gradient.start.y); + const ImVec2 end = screen_pos + ImVec2(gradient.end.x, gradient.end.y); + + const ImVec2 axis = end - start; + const float axis_len_sq = axis.x * axis.x + axis.y * axis.y; + if (axis_len_sq <= 0.0001f) { + return; + } + + const float axis_len = sqrtf(axis_len_sq); + const ImVec2 dir(axis.x / axis_len, axis.y / axis_len); + const ImVec2 normal(-dir.y, dir.x); + + const float w = lgm.border_max.x - lgm.border_min.x; + const float h = lgm.border_max.y - lgm.border_min.y; + const float extent = sqrtf(w * w + h * h) + 2.0f; + const float approx_span = ImMax(w, h); + + const int strips = (int)ImClamp(axis_len / 2.0f + approx_span / 4.0f, 16.0f, 128.0f); + const std::vector fill_poly = build_layer_fill_polygon(lgm, 8); + + auto color_for_point = [&](const ImVec2& p) -> ImU32 { + float t = ((p.x - start.x) * axis.x + (p.y - start.y) * axis.y) / axis_len_sq; + t = ImClamp(t, 0.0f, 1.0f); + return to_im_col32(sample_gradient_color(gradient.color_points, t)); + }; + + for (int i = 0; i < strips; ++i) { + const float t0 = (float)i / (float)strips; + const float t1 = (float)(i + 1) / (float)strips; + + const ImVec2 p0 = start + dir * (t0 * axis_len); + const ImVec2 p1 = start + dir * (t1 * axis_len); + + const std::vector strip_quad = { + p0 - normal * extent, + p0 + normal * extent, + p1 + normal * extent, + p1 - normal * extent, + }; + + draw_clipped_shaded_polygon(draw_list, strip_quad, fill_poly, color_for_point); + } + } + + void draw_radial_gradient_impl(const LayerGeometry& lgm, + const litehtml::background_layer::radial_gradient& gradient) { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + const ImVec2 center = screen_pos + ImVec2(gradient.position.x, gradient.position.y); + + const float rx = gradient.radius.x; + const float ry = gradient.radius.y; + + if (rx <= 0.0001f || ry <= 0.0001f || gradient.color_points.empty()) { + return; + } + + const std::vector fill_poly = build_layer_fill_polygon(lgm, 12); + + // Draw from outside to inside so smaller inner ellipses overwrite larger ones. + const int ring_count = 64; + const int ellipse_segments = 64; + + for (int i = ring_count; i >= 1; --i) { + const float t = (float)i / (float)ring_count; + const std::vector ellipse = build_ellipse_polygon(center, rx, ry, t, ellipse_segments); + + std::vector clipped = clip_polygon_convex(ellipse, fill_poly); + if (clipped.size() < 3) { + continue; } - push_bottom_right(ImVec2(bg_box.x + bg_box.width, bg_box.y + bg_box.height)); + const ImU32 col = to_im_col32(sample_gradient_color(gradient.color_points, t)); + draw_convex_shaded_polygon(draw_list, clipped, [&](const ImVec2&) -> ImU32 { return col; }); + } + + // Fill the center with t=0 color. + { + const std::vector center_poly = + build_ellipse_polygon(center, rx, ry, 1.0f / (float)ring_count, ellipse_segments); + + std::vector clipped = clip_polygon_convex(center_poly, fill_poly); + if (clipped.size() >= 3) { + const ImU32 col = to_im_col32(sample_gradient_color(gradient.color_points, 0.0f)); + draw_convex_shaded_polygon(draw_list, clipped, [&](const ImVec2&) -> ImU32 { return col; }); + } + } + } + + void draw_conic_gradient_impl(const LayerGeometry& lgm, const litehtml::background_layer::conic_gradient& gradient) { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + const ImVec2 center = screen_pos + ImVec2(gradient.position.x, gradient.position.y); + + const float radius = gradient.radius; + if (radius <= 0.0001f || gradient.color_points.empty()) { + return; + } + + const std::vector fill_poly = build_layer_fill_polygon(lgm, 12); + + const int wedge_count = 128; + const int arc_segments_per_wedge = 1; + + for (int i = 0; i < wedge_count; ++i) { + const float t0 = (float)i / (float)wedge_count; + const float t1 = (float)(i + 1) / (float)wedge_count; + + const float a0 = gradient.angle + t0 * 360.0f; + const float a1 = gradient.angle + t1 * 360.0f; + + const std::vector wedge = build_conic_wedge_polygon(center, radius, a0, a1, arc_segments_per_wedge); + + std::vector clipped = clip_polygon_convex(wedge, fill_poly); + if (clipped.size() < 3) { + continue; + } + + const ImU32 col = to_im_col32(sample_gradient_color(gradient.color_points, t0)); + + draw_convex_shaded_polygon(draw_list, clipped, [&](const ImVec2&) -> ImU32 { return col; }); + } + } + + template + void draw_gradient_common(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, const Gradient& gradient, + DrawFn&& draw_fn) { + const litehtml::position& bg_box = layer.border_box; + const litehtml::position& clip_box = layer.clip_box; + + if (bg_box.width <= 0 || bg_box.height <= 0 || clip_box.width <= 0 || clip_box.height <= 0) { + return; + } + + if (gradient.color_points.empty()) { + return; + } + + LayerGeometry lgm = this->get_layer_geometry(layer); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + draw_list->PushClipRect(lgm.clip_min, lgm.clip_max, true); + draw_fn(lgm, gradient); + draw_list->PopClipRect(); + + push_bottom_right(ImVec2((float)(bg_box.x + bg_box.width), (float)(bg_box.y + bg_box.height))); + } + + virtual void draw_linear_gradient(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, + const litehtml::background_layer::linear_gradient& gradient) override { + const ImVec2 screen_pos = ImGui::GetCursorScreenPos(); + const ImVec2 start = screen_pos + ImVec2(gradient.start.x, gradient.start.y); + const ImVec2 end = screen_pos + ImVec2(gradient.end.x, gradient.end.y); + const ImVec2 axis = end - start; + const float axis_len_sq = axis.x * axis.x + axis.y * axis.y; + + if (axis_len_sq <= 0.0001f) { + if (!gradient.color_points.empty()) { + draw_solid_fill(hdc, layer, gradient.color_points.back().color); + } + return; } + + draw_gradient_common( + hdc, layer, gradient, [&](const LayerGeometry& lgm, const auto& g) { draw_linear_gradient_impl(lgm, g); }); + } + + virtual void draw_radial_gradient(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, + const litehtml::background_layer::radial_gradient& gradient) override { + draw_gradient_common( + hdc, layer, gradient, [&](const LayerGeometry& lgm, const auto& g) { draw_radial_gradient_impl(lgm, g); }); + } + + virtual void draw_conic_gradient(litehtml::uint_ptr hdc, const litehtml::background_layer& layer, + const litehtml::background_layer::conic_gradient& gradient) override { + draw_gradient_common( + hdc, layer, gradient, [&](const LayerGeometry& lgm, const auto& g) { draw_conic_gradient_impl(lgm, g); }); } - virtual void draw_borders(litehtml::uint_ptr hdc, const litehtml::borders &borders, - const litehtml::position &draw_pos, bool root) override { + virtual void on_mouse_event(const litehtml::element::ptr& el, litehtml::mouse_event event) override { + // TODO + } + + virtual void draw_borders(litehtml::uint_ptr hdc, const litehtml::borders& borders, + const litehtml::position& draw_pos, bool root) override { ImVec2 base_pos = ImGui::GetCursorScreenPos(); ImVec2 top_left = base_pos + ImVec2(draw_pos.x, draw_pos.y); ImVec2 top_right = base_pos + ImVec2(draw_pos.x + draw_pos.width, draw_pos.y); ImVec2 bottom_right = base_pos + ImVec2(draw_pos.x + draw_pos.width, draw_pos.y + draw_pos.height); ImVec2 bottom_left = base_pos + ImVec2(draw_pos.x, draw_pos.y + draw_pos.height); - auto *draw_list = ImGui::GetWindowDrawList(); + auto* draw_list = ImGui::GetWindowDrawList(); // Check if all sides and colors are equal if (borders.top.width == borders.right.width && borders.top.width == borders.bottom.width && @@ -383,7 +958,7 @@ class BrowserContainer : public litehtml::document_container { } } else { // The Non-Uniform Path (Mitered Borders via Quads) - auto color32 = [](const litehtml::web_color &c) { return IM_COL32(c.red, c.green, c.blue, c.alpha); }; + auto color32 = [](const litehtml::web_color& c) { return IM_COL32(c.red, c.green, c.blue, c.alpha); }; // Top border if (borders.top.width > 0) { @@ -428,20 +1003,20 @@ class BrowserContainer : public litehtml::document_container { // Document related functions // - virtual void set_caption(const char *caption) override { title = caption; } - virtual void set_base_url(const char *base_url) override {} - virtual void link(const std::shared_ptr &doc, const litehtml::element::ptr &el) override {} - virtual void on_anchor_click(const char *url, const litehtml::element::ptr &el) override { + virtual void set_caption(const char* caption) override { title = caption; } + virtual void set_base_url(const char* base_url) override {} + virtual void link(const std::shared_ptr& doc, const litehtml::element::ptr& el) override {} + virtual void on_anchor_click(const char* url, const litehtml::element::ptr& el) override { history.push_back(currentUrl); loadUrl = url; } - virtual void set_cursor(const char *cursor) override { + virtual void set_cursor(const char* cursor) override { if (std::string(cursor) == "pointer" && ImGui::IsWindowHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } } - virtual void transform_text(std::string &text, litehtml::text_transform tt) override {} - virtual void import_css(std::string &text, const std::string &url, std::string &baseurl) override { + virtual void transform_text(std::string& text, litehtml::text_transform tt) override {} + virtual void import_css(std::string& text, const std::string& url, std::string& baseurl) override { if (!config.LoadCSS) { return; } @@ -452,22 +1027,22 @@ class BrowserContainer : public litehtml::document_container { // Clipping functions // - virtual void set_clip(const litehtml::position &pos, const litehtml::border_radiuses &bdr_radius) override {} + virtual void set_clip(const litehtml::position& pos, const litehtml::border_radiuses& bdr_radius) override {} virtual void del_clip() override {} // // Layout functions // - virtual void get_client_rect(litehtml::position &client) const override { + virtual void get_viewport(litehtml::position& client) const override { client.x = 0; client.y = 0; client.width = width > 0 ? width : ImGui::GetContentRegionAvail().x; client.height = ImGui::GetContentRegionAvail().y; } - virtual litehtml::element::ptr create_element(const char *tag_name, const litehtml::string_map &attributes, - const std::shared_ptr &doc) override { + virtual litehtml::element::ptr create_element(const char* tag_name, const litehtml::string_map& attributes, + const std::shared_ptr& doc) override { if (customElements.find(tag_name) != customElements.end()) { return std::make_shared(doc, tag_name, attributes); } @@ -475,7 +1050,7 @@ class BrowserContainer : public litehtml::document_container { return nullptr; } - virtual void get_media_features(litehtml::media_features &media) const override { + virtual void get_media_features(litehtml::media_features& media) const override { media.color = 8; media.resolution = 96; media.width = width > 0 ? width : ImGui::GetContentRegionAvail().x; @@ -485,48 +1060,25 @@ class BrowserContainer : public litehtml::document_container { media.type = litehtml::media_type_screen; } - virtual void get_language(litehtml::string &language, litehtml::string &culture) const override { + virtual void get_language(litehtml::string& language, litehtml::string& culture) const override { language = "en"; culture = "US"; } }; -void CustomElement::draw_background(litehtml::uint_ptr hdc, int x, int y, const litehtml::position *clip, - const std::shared_ptr &ri) { - // ri->pos() is the element's content box relative to its parent. - // x/y carry the accumulated offset from all ancestors. - // Together they give the absolute document position. - litehtml::position pos = ri->pos(); - pos.x += x; - pos.y += y; - - if (customElements.find(this->tag) != customElements.end()) { - ImVec2 cursor = ImGui::GetCursorScreenPos(); - customElements[this->tag]( - ImRect(cursor + ImVec2(pos.x, pos.y), cursor + ImVec2(pos.x + pos.width, pos.y + pos.height)), - this->attributes); - ImGui::SetCursorScreenPos(cursor); - } - - // Notify the container about the space this element occupies so that - // BrowserContainer::get_bottom_right() returns the correct total size. - auto *container = static_cast(get_document()->container()); - container->push_bottom_right(ImVec2(pos.x + pos.width, pos.y + pos.height)); -} - -Config *GetConfig() { return &config; } -void SetConfig(Config config) { config = config; } -void PushConfig(Config config) { configStack.push_back(config); } +Config* GetConfig() { return &config; } +void SetConfig(const Config& newConfig) { config = newConfig; } +void PushConfig(const Config& config) { configStack.push_back(config); } void PopConfig() { assert(!configStack.empty()); configStack.pop_back(); } -void RegisterCustomElement(const char *tagName, CustomElementDrawFunction draw) { customElements[tagName] = draw; } +void RegisterCustomElement(const char* tagName, CustomElementDrawFunction draw) { customElements[tagName] = draw; } -void UnregisterCustomElement(const char *tagName) { customElements.erase(tagName); } +void UnregisterCustomElement(const char* tagName) { customElements.erase(tagName); } -bool Canvas(const char *id, const char *html, float width, std::string *clickedURL) { +bool Canvas(const char* id, const char* html, float width, std::string* clickedURL) { struct state { std::shared_ptr container; std::shared_ptr doc; @@ -548,7 +1100,7 @@ bool Canvas(const char *id, const char *html, float width, std::string *clickedU }; } - auto &state = states[id]; + auto& state = states[id]; if (state.html != html) { state.doc = litehtml::document::createFromString(html, state.container.get()); diff --git a/vendor/imhtml/imhtml.hpp b/vendor/imhtml/imhtml.hpp index d6c784d..c024227 100644 --- a/vendor/imhtml/imhtml.hpp +++ b/vendor/imhtml/imhtml.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include #include +#include #include "imgui.h" #include "imgui_internal.h" @@ -27,15 +27,28 @@ struct ImageMeta { int height; }; +/** + * A font family, containing different styles of the same font. + */ +struct FontFamily { + ImFont *Regular = nullptr; + ImFont *Bold = nullptr; + ImFont *Italic = nullptr; + ImFont *BoldItalic = nullptr; +}; + /** * Configuration for the HTML renderer */ struct Config { float BaseFontSize = 16.0f; - ImFont *FontRegular = nullptr; - ImFont *FontBold = nullptr; - ImFont *FontItalic = nullptr; - ImFont *FontBoldItalic = nullptr; + + // fallback when not found in FontFamilies, or no specific family provided + FontFamily DefaultFont; + + // CSS font-family name -> family + std::map FontFamilies; + std::function LoadImage; std::function GetImageMeta; std::function GetImageTexture; @@ -71,14 +84,14 @@ Config *GetConfig(); * * @param config The new configuration */ -void SetConfig(Config config); +void SetConfig(const Config &config); /** * Push the configuration * * @param config The new configuration */ -void PushConfig(Config config); +void PushConfig(const Config &config); /** * Pop the configuration @@ -110,4 +123,4 @@ void UnregisterCustomElement(const char *tagName); * @return True if any link was clicked, false otherwise */ bool Canvas(const char *id, const char *html, float width = 0.0f, std::string *clickedURL = nullptr); -}; // namespace ImHTML \ No newline at end of file +}; // namespace ImHTML