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 = SVGGElement::Create(document);
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;
}
};
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()) {
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");
state.loadFromString(textEditor.
getText());
}
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) {
lastShownErrorLine = line;
lastShownErrorReason.assign(reasonSv);
}
} else if (lastShownErrorLine != kNoErrorLine) {
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:123
void resetTextChanged()
Clear the text-change flag and changed-line tracking.
Definition TextEditor.h:393
bool isTextChanged() const
Report whether text changed since reset; external source mirroring clears the flag.
Definition TextEditor.h:371
::donner::editor::ErrorMarkers ErrorMarkers
Line-keyed diagnostics shared with the core.
Definition TextEditor.h:208
void render(std::string_view title, const ImVec2 &size=ImVec2(), bool showBorder=false)
Render the editor in an ImGui child region unless child creation is disabled.
void selectAndFocus(const Coordinates &start, const Coordinates &end)
Set a new selection and scroll to it.
Definition TextEditor.h:562
std::string getText() const
Get all text in the editor.
void setLanguageDefinition(const LanguageDefinition &langDef)
Set syntax language rules and schedule recoloring.
void setText(std::string_view text, bool preserveScroll=false)
Replace the entire buffer with new text.
void setErrorMarkers(const ErrorMarkers &markers)
Replace line-keyed diagnostic markers.
Definition TextEditor.h:273
Backend-agnostic renderer that resolves to the active build backend (tiny-skia by default,...
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 size of the rendered output, in pixels.
Derived cast()
Cast this element to its derived type.
Definition SVGElement.h:649
bool isa() const
Return true if this element "is a" instance of type, i.e.
Definition SVGElement.h:625
static constexpr size_t kDefaultMaximumInputSize
Default maximum number of source or expanded SVG bytes accepted from untrusted input.
Definition SVGParser.h:27
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:22
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:397
Length< double > Lengthd
Shorthand for Length<double>.
Definition Length.h:279
Vector2< int > Vector2i
Shorthand for Vector2<int>.
Definition Vector2.h:400
Transform2< double > Transform2d
Shorthand for Transform2<double>.
Definition Transform.h:354
int main()
Run the rendering example and report its result.
Definition render_test.cc:34
A location in parser input, as a character offset and optional line information.
Definition FileOffset.h:14
std::optional< LineInfo > lineInfo
Line information for multi-line strings, if known.
Definition FileOffset.h:58
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:155
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:67
std::size_t rowBytes
Bytes between rows; allows alignment/padding differences between renderers.
Definition RendererInterface.h:73
bool empty() const
Returns true if this bitmap has no pixel data.
Definition RendererInterface.h:79
std::vector< uint8_t > pixels
Raw RGBA8 pixel data; row starts are separated by rowBytes.
Definition RendererInterface.h:71
Vector2i dimensions
Pixel dimensions of the bitmap in device pixels.
Definition RendererInterface.h:69