Minimal Donner SVG viewer with a live text pane.
Minimal Donner SVG viewer with a live text pane. A small ImGui demo: load an SVG, display it, click to select a shape, and edit the source in a text pane that re-parses on every keystroke. Selection chrome is drawn by injecting an editor-only <rect> and <path> into the document tree - no separate overlay renderer and no editor-side command queue. The only dependency on the editor tree is donner::editor::TextEditor, the syntax-aware text widget.
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include "glad/glad.h"
extern "C" {
#include "GLFW/glfw3.h"
}
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
namespace {
constexpr int kInitialWindowWidth = 1280;
constexpr int kInitialWindowHeight = 800;
constexpr float kSourcePaneWidth = 560.0f;
void GlfwErrorCallback(int error, const char* description) {
std::cerr << "GLFW error " << error << ": " << description << "\n";
}
std::string LoadFile(const std::string& filename) {
auto result =
if (const auto* contents = std::get_if<std::string>(&result)) {
return *contents;
} else {
std::cerr << donner::FileReadErrorMessage(std::get<donner::FileReadError>(result)) << ": "
return {};
}
}
struct ViewerState {
donner::svg::SVGDocument document;
std::optional<donner::svg::DonnerController> controller;
std::optional<donner::svg::SVGRectElement> boundsShape;
std::optional<donner::svg::SVGPathElement> selectedPathOutline;
std::optional<donner::svg::SVGElement> selectedElement;
std::optional<donner::ParseDiagnostic> lastParseError;
bool valid = false;
void loadFromString(std::string_view source) {
using namespace donner;
using namespace donner::svg;
using namespace donner::svg::parser;
document = SVGDocument();
valid = false;
controller.reset();
boundsShape.reset();
selectedPathOutline.reset();
selectedElement.reset();
ParseWarningSink disabled = ParseWarningSink::Disabled();
ParseResult<SVGDocument> maybe = SVGParser::ParseSVG(source, disabled);
lastParseError = std::move(maybe.
error());
return;
}
lastParseError.reset();
document = std::move(maybe.
result());
controller = DonnerController(document);
auto editorOnly = SVGUnknownElement::Create(document, "editor-only");
document.svgElement().appendChild(editorOnly);
boundsShape = SVGRectElement::Create(document);
editorOnly.appendChild(boundsShape.value());
boundsShape->setStyle(
"display: none; fill: none; stroke: deepskyblue; stroke-width: 1px; "
"pointer-events: none");
selectedPathOutline = SVGPathElement::Create(document);
editorOnly.appendChild(selectedPathOutline.value());
selectedPathOutline->setStyle(
"display: none; fill: none; stroke: deepskyblue; stroke-width: 1px; "
"pointer-events: none");
valid = true;
}
void selectNone() {
selectedElement.reset();
if (boundsShape) {
boundsShape->setStyle("display: none");
}
if (selectedPathOutline) {
selectedPathOutline->setStyle("display: none");
}
}
void selectElement(const donner::svg::SVGElement& element) {
using namespace donner::svg;
selectedElement = element;
if (!element.
isa<SVGGeometryElement>()) {
return;
}
auto geom = element.
cast<SVGGeometryElement>();
if (auto spline = geom.computedSpline()) {
if (selectedPathOutline) {
selectedPathOutline->setStyle("display: inline");
selectedPathOutline->setSpline(*spline);
selectedPathOutline->setTransform(geom.elementFromWorld());
}
if (auto bounds = geom.worldBounds()) {
if (boundsShape) {
boundsShape->setStyle("display: inline");
}
}
}
}
std::optional<donner::SourceRange> handleClick(
const donner::Vector2d& documentPoint) {
if (!controller) {
return std::nullopt;
}
auto hit = controller->findIntersecting(documentPoint);
if (!hit.has_value()) {
return std::nullopt;
}
selectElement(*hit);
return xmlNode->getNodeLocation();
}
return std::nullopt;
}
};
donner::editor::TextEditor::ErrorMarkers markers;
const int line =
markers.emplace(line, std::string(std::string_view(diag.
reason)));
return markers;
}
}
static_cast<int>(offset.
lineInfo->offsetOnLine));
}
}
int main(int argc, char** argv) {
if (const char* bwd = std::getenv("BUILD_WORKING_DIRECTORY")) {
std::filesystem::current_path(bwd);
}
if (argc != 2) {
std::cerr << "Usage: svg_viewer <filename>\n";
return 1;
}
const std::string svgPath = argv[1];
const std::string initialSource = LoadFile(svgPath);
if (initialSource.empty()) {
return 1;
}
glfwSetErrorCallback(GlfwErrorCallback);
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW\n";
return 1;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(kInitialWindowWidth, kInitialWindowHeight,
"Donner SVG Viewer", nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window\n";
glfwTerminate();
return 1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress))) {
std::cerr << "Failed to initialize OpenGL loader\n";
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330");
ViewerState state;
state.loadFromString(initialSource);
constexpr int kNoErrorLine = -1;
int lastShownErrorLine = kNoErrorLine;
std::string lastShownErrorReason;
if (state.
lastParseError.has_value()) {
textEditor.
setErrorMarkers(ParseErrorToMarkers(*state.lastParseError));
lastShownErrorLine = state.lastParseError->range.start.lineInfo.has_value()
? static_cast<int>(state.lastParseError->range.start.lineInfo->line)
: 1;
lastShownErrorReason.assign(std::string_view(state.lastParseError->reason));
}
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int textureWidth = 0;
int textureHeight = 0;
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
renderer.
draw(state.
document);
glBindTexture(GL_TEXTURE_2D, texture);
glPixelStorei(GL_UNPACK_ROW_LENGTH,
static_cast<GLint
>(bitmap.
rowBytes / 4u));
GL_RGBA, GL_UNSIGNED_BYTE, bitmap.
pixels.data());
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
}
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
int windowWidth = 0;
int windowHeight = 0;
glfwGetWindowSize(window, &windowWidth, &windowHeight);
const ImGuiWindowFlags kPaneFlags =
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse;
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(kSourcePaneWidth, static_cast<float>(windowHeight)),
ImGuiCond_Always);
ImGui::Begin("Source", nullptr, kPaneFlags);
textEditor.
render(
"##source");
if (textEditor.
isTextChanged()) {
state.loadFromString(textEditor.
getText());
textEditor.
resetTextChanged();
}
if (state.lastParseError.has_value()) {
const int line = state.lastParseError->range.start.lineInfo.has_value()
? static_cast<int>(state.lastParseError->range.start.lineInfo->line)
: 1;
const std::string_view reasonSv = state.lastParseError->reason;
if (line != lastShownErrorLine || reasonSv != lastShownErrorReason) {
textEditor.setErrorMarkers(ParseErrorToMarkers(*state.lastParseError));
lastShownErrorLine = line;
lastShownErrorReason.assign(reasonSv);
}
} else if (lastShownErrorLine != kNoErrorLine) {
textEditor.setErrorMarkers({});
lastShownErrorLine = kNoErrorLine;
lastShownErrorReason.clear();
}
ImGui::End();
ImGui::SetNextWindowPos(ImVec2(kSourcePaneWidth, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(static_cast<float>(windowWidth) - kSourcePaneWidth,
static_cast<float>(windowHeight)),
ImGuiCond_Always);
ImGui::Begin("Render", nullptr, kPaneFlags);
const ImVec2 contentRegion = ImGui::GetContentRegionAvail();
const int desiredW = static_cast<int>(contentRegion.x);
const int desiredH = static_cast<int>(contentRegion.y);
if (state.valid && desiredW > 0 && desiredH > 0) {
if (currentSize.
x != desiredW || currentSize.
y != desiredH) {
}
}
if (textureWidth > 0 && textureHeight > 0) {
const ImVec2 imageOrigin = ImGui::GetCursorScreenPos();
ImGui::Image(static_cast<ImTextureID>(static_cast<std::uintptr_t>(texture)),
ImVec2(static_cast<float>(textureWidth), static_cast<float>(textureHeight)));
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
const ImVec2 mouse = ImGui::GetMousePos();
const donner::Vector2d canvasPoint(mouse.x - imageOrigin.x, mouse.y - imageOrigin.y);
const auto sourceRange =
if (sourceRange.has_value()) {
textEditor.
selectAndFocus(FileOffsetToEditorCoordinates(sourceRange->start),
FileOffsetToEditorCoordinates(sourceRange->end));
}
}
} else {
ImGui::TextUnformatted("(no rendered image)");
}
ImGui::End();
ImGui::Render();
int displayWidth = 0;
int displayHeight = 0;
glfwGetFramebufferSize(window, &displayWidth, &displayHeight);
glViewport(0, 0, displayWidth, displayHeight);
glClearColor(0.10f, 0.10f, 0.10f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
glDeleteTextures(1, &texture);
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
bool hasError() const noexcept
Returns true if this ParseResult contains an error.
Definition ParseResult.h:110
T & result() &
Returns the contained result.
Definition ParseResult.h:51
ParseDiagnostic & error() &
Returns the contained error.
Definition ParseResult.h:81
A text editor widget for Dear ImGui that supports syntax highlighting, undo & redo,...
Definition TextEditor.h:118
void selectAndFocus(const Coordinates &start, const Coordinates &end)
Set a new selection and scroll to it.
Definition TextEditor.h:498
std::string getText() const
Get all text in the editor.
void setText(std::string_view text, bool preserveScroll=false)
Replace the entire buffer with new text.
Backend-agnostic renderer that resolves to the active build backend (Skia or tiny-skia).
Definition Renderer.h:82
RendererBitmap takeSnapshot() const override
Captures a CPU-readable snapshot of the current frame.
void draw(SVGDocument &document) override
Draws the SVG document using the active backend.
Transform2d canvasFromDocumentTransform() const
Returns the transform that maps points from the SVG document's viewBox coordinate space into the canv...
void setCanvasSize(int width, int height)
Set the canvas (output image) size to a fixed width and height, in pixels.
Vector2i canvasSize() const
Get the current canvas size, or the default size (512x512) if the canvas size has not been explicitly...
Derived cast()
Cast this element to its derived type.
Definition SVGElement.h:611
bool isa() const
Return true if this element "is a" instance of type, if it be cast to a specific type with cast.
Definition SVGElement.h:587
static constexpr size_t kDefaultMaximumInputSize
Default maximum number of source or expanded SVG bytes accepted from untrusted input.
Definition SVGParser.h:20
static std::optional< XMLNode > TryCast(EntityHandle entity)
Try to cast to an XMLNode from a raw Entity.
std::string EscapeTerminalText(std::string_view text, bool preserveNewlines=false)
Escape untrusted bytes and terminal format controls while preserving printable UTF-8.
Definition TerminalEscape.h:13
FileReadResult ReadFileBounded(const std::filesystem::path &path, size_t maximumSize)
Open and read a regular file without allocating more than maximumSize bytes.
Vector2< double > Vector2d
Shorthand for Vector2<double>.
Definition Vector2.h:394
Length< double > Lengthd
Shorthand for Length<double>.
Definition Length.h:277
Vector2< int > Vector2i
Shorthand for Vector2<int>.
Definition Vector2.h:397
Transform2< double > Transform2d
Shorthand for Transform2<double>.
Definition Transform.h:333
Error context for a failed parse, such as the error reason, line, and character offset.
Definition FileOffset.h:13
std::optional< LineInfo > lineInfo
Line information for multi-line strings, if known.
Definition FileOffset.h:57
A diagnostic message from a parser, with severity, source range, and human-readable reason.
Definition ParseDiagnostic.h:31
RcString reason
Human-readable description of the problem.
Definition ParseDiagnostic.h:36
SourceRange range
Source range that this diagnostic applies to. For point diagnostics where the end is unknown,...
Definition ParseDiagnostic.h:40
FileOffset start
Start of the range (inclusive).
Definition FileOffset.h:150
T y
The y component of the vector.
Definition Vector2.h:18
T x
The x component of the vector.
Definition Vector2.h:17
Coordinates representing a position in the text buffer, using a grid-based system where tabs are expa...
Definition TextBuffer.h:20
static const LanguageDefinition & SVG()
Get the SVG language definition.
CPU-readable bitmap produced by a renderer snapshot.
Definition RendererInterface.h:65
std::size_t rowBytes
Bytes between rows; allows alignment/padding differences between renderers.
Definition RendererInterface.h:71
bool empty() const
Returns true if this bitmap has no pixel data.
Definition RendererInterface.h:76
std::vector< uint8_t > pixels
Raw RGBA8 pixel data; row starts are separated by rowBytes.
Definition RendererInterface.h:69
Vector2i dimensions
Pixel dimensions of the bitmap in device pixels.
Definition RendererInterface.h:67